URI: 
       tStart cleaning up and adding documentation - 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 7892e89a47ccb35549b25fe558245b9e8f639daf
   DIR parent 22ffb413e9e0be6a5764de4bcc951e6a0480bad3
  HTML Author: lumidify <nobody@lumidify.org>
       Date:   Sat, 18 Dec 2021 21:44:47 +0100
       
       Start cleaning up and adding documentation
       
       Diffstat:
         M IDEAS                               |       7 +++++++
         M Makefile                            |       7 +++++--
         A NOTES                               |       3 +++
         D QUIRKS                              |      14 --------------
         M README                              |      15 ++++++++++++++-
         M TODO                                |       3 +++
         M assert.c                            |      17 -----------------
         M buffer.c                            |     138 ++++++++++++++-----------------
         M buffer.h                            |      30 +++++++++++++++---------------
         M cache.c                             |       9 +++------
         M cache.h                             |       9 +++++++++
         M cleanup.h                           |       5 +++++
         M common.h                            |      11 +++++++++--
         A draw_util.c                         |      43 ++++++++++++++++++++++++++++++
         A draw_util.h                         |      34 +++++++++++++++++++++++++++++++
         M keys.c                              |       2 +-
         M keys.h                              |      32 +++++++++----------------------
         M keys_basic.c                        |     163 +++++++++++++++++++------------
         M keys_basic.h                        |       8 ++++++++
         M keys_basic_config.h                 |      18 +++++++++---------
         M keys_command.c                      |      84 ++++++++++++++++++++-----------
         M keys_command.h                      |       8 ++++++++
         M keys_command_config.h               |       6 +++---
         A keys_config.h                       |      31 +++++++++++++++++++++++++++++++
         A ledit.1                             |     796 +++++++++++++++++++++++++++++++
         M ledit.c                             |      45 +++++++++++++------------------
         M macros.h                            |       5 +++++
         M memory.c                            |     126 ++++++++++++++++++-------------
         M memory.h                            |      16 ++++++++++++++++
         M pango-compat.h                      |       7 +++++++
         M search.c                            |      49 ++++++++++++-------------------
         M search.h                            |      22 +++++++++++++++++-----
         M theme.h                             |       8 ++++++++
         M txtbuf.c                            |      22 ++++++++--------------
         M txtbuf.h                            |      15 ++++++++-------
         M undo.c                              |      86 ++++++++++++++++++++++---------
         M undo.h                              |      25 +++++++++++++++++++++----
         M util.c                              |      77 ++++++++++++-------------------
         M util.h                              |      34 +++++++++++++------------------
         M view.c                              |     119 +++++++++++++------------------
         M view.h                              |      26 ++++++++++++++------------
         M window.c                            |      28 +++++++++++++---------------
         M window.h                            |      28 ++++++++++++++++++++++------
       
       43 files changed, 1642 insertions(+), 589 deletions(-)
       ---
   DIR diff --git a/IDEAS b/IDEAS
       t@@ -3,3 +3,10 @@
        * basic macros
        * add different (more basic) text backend
        * https://drewdevault.com/2021/06/27/You-cant-capture-the-nuance.html
       +* maybe somehow allow to define one keyboard layout that can be easily
       +  be switched to and from - this would make typing commands easier
       +  because they can't really be translated into other languages like the
       +  key bindings
       +  -> I'm not sure it that's even possible in a portable way, though,
       +     since the keyboard layouts can be set in many different ways, so
       +     the entire state would somehow have to be saved to restore it again.
   DIR diff --git a/Makefile b/Makefile
       t@@ -24,6 +24,7 @@ OBJ = \
                txtbuf.o \
                undo.o \
                util.o \
       +        draw_util.o \
                window.o \
                pango-compat.o
        
       t@@ -42,6 +43,7 @@ HDR = \
                txtbuf.h \
                undo.h \
                util.h \
       +        draw_util.h \
                window.h \
                cleanup.h \
                pango-compat.h
       t@@ -53,8 +55,9 @@ all: ${BIN}
        
        ledit.o window.o : config.h
        theme.o : theme_config.h
       -keys_basic.o : keys_basic_config.h
       -keys_command.o : keys_command_config.h
       +keys_basic.o : keys_basic_config.h keys_config.h
       +keys_command.o : keys_command_config.h keys_config.h
       +keys.o : keys_config.h
        
        ${OBJ} : ${HDR}
        
   DIR diff --git a/NOTES b/NOTES
       t@@ -0,0 +1,3 @@
       +* I originally wanted to avoid putting includes in header files, but
       +  I eventually decided that it just becomes a huge mess that way.
       +  Maybe I'll change my mind sometime, though.
   DIR diff --git a/QUIRKS b/QUIRKS
       t@@ -1,14 +0,0 @@
       -* Undo with multiple views:
       -  Since a new mode group is started each time insert is entered, when text
       -  is typed in one view in insert, then in another view, and then again in
       -  the first one, the last two inserts will be undone in one go since both
       -  views were in insert already. I'm not sure how to make this more logical,
       -  though.
       -  Maybe it could be "improved" by also saving view in undo stack, but that
       -  would cause problems because views can be added and removed, and it would
       -  maybe not even be more logical.
       -
       -* Scroll offset is stored as pixel value, so a view may scroll when text is
       -  added or deleted in another view. Additionally, when a new view is created,
       -  the scroll offset from the old view is taken, which may be weird if the
       -  window of the new view is a different size.
   DIR diff --git a/README b/README
       t@@ -1 +1,14 @@
       -Work in progress. Nothing to see here.
       +WARNING: This is work in progress! A lot of bugs still need to be fixed
       +before this can be used as a real text editor.
       +
       +ledit is a vi-like text editor for people who switch between keyboard
       +layouts frequently and/or work with languages that require complex text
       +layout.
       +
       +The documentation can be viewed in ledit.1 or at the following locations:
       +
       +gopher://lumidify.org/0/doc/ledit/ledit-current.txt
       +gopher://4kcetb7mo7hj6grozzybxtotsub5bempzo4lirzc3437amof2c2impyd.onion/0/doc/ledit/ledit-current.txt
       +http://lumidify.org/doc/ledit/ledit-current.html
       +http://4kcetb7mo7hj6grozzybxtotsub5bempzo4lirzc3437amof2c2impyd.onion/doc/ledit/ledit-current.html
       +https://lumidify.org/doc/ledit/ledit-current.html
   DIR diff --git a/TODO b/TODO
       t@@ -1,3 +1,6 @@
       +* Use proper gap buffer implementation
       +* Somehow clean up overflow handling
       +* File locking (lockf(3) or flock(2))
        * Load file in background so text is already shown while still
          loading the rest of a big file.
        * Try to copy vi behavior where the cursor jumps back to the original
   DIR diff --git a/assert.c b/assert.c
       t@@ -1,22 +1,5 @@
       -/* FIXME: sort out the stupid includes */
        #include <stdio.h>
        #include <stdlib.h>
       -
       -#include <X11/Xlib.h>
       -#include <X11/Xutil.h>
       -#include <X11/Xatom.h>
       -#include <pango/pangoxft.h>
       -#include <X11/extensions/Xdbe.h>
       -
       -#include "pango-compat.h"
       -#include "memory.h"
       -#include "common.h"
       -#include "txtbuf.h"
       -#include "undo.h"
       -#include "cache.h"
       -#include "theme.h"
       -#include "window.h"
       -#include "buffer.h"
        #include "cleanup.h"
        
        void
   DIR diff --git a/buffer.c b/buffer.c
       t@@ -1,5 +1,3 @@
       -/* FIXME: shrink buffers when text length less than a fourth of the size */
       -/* FIXME: handle all undo within buffer to keep it consistent */
        /* FIXME: maybe use separate unicode grapheme library so all functions
           that need grapheme boundaries can be included here instead of in the views */
        
       t@@ -16,16 +14,17 @@
        #include <pango/pangoxft.h>
        #include <X11/extensions/Xdbe.h>
        
       -#include "pango-compat.h"
       -#include "memory.h"
       -#include "common.h"
       -#include "txtbuf.h"
       +#include "util.h"
        #include "undo.h"
        #include "cache.h"
        #include "theme.h"
       +#include "memory.h"
       +#include "common.h"
       +#include "txtbuf.h"
        #include "window.h"
        #include "buffer.h"
        #include "assert.h"
       +#include "pango-compat.h"
        
        /*
         * Important notes:
       t@@ -43,11 +42,6 @@
         */
        
        /*
       - * Move the gap of a line so it is at byte position 'index'
       - */
       -static void move_text_gap(ledit_line *line, size_t index);
       -
       -/*
         * Move the gap of the line gap buffer to 'index'.
         */
        static void move_line_gap(ledit_buffer *buffer, size_t index);
       t@@ -150,27 +144,18 @@ static void buffer_delete_line_entries_base(ledit_buffer *buffer, size_t index1,
         */
        static void buffer_delete_line_section_base(ledit_buffer *buffer, size_t line, size_t start, size_t length);
        
       +/*
       + * Copy text range into given buffer.
       + * - dst is null-terminated
       + * - dst must be large enough to contain the text and NUL (only use this together with buffer_textlen)
       + * - the range must be sorted already
       + */
       +static void buffer_copy_text(ledit_buffer *buffer, char *dst, int line1, int byte1, int line2, int byte2);
       +
        static void marklist_destroy(ledit_buffer_marklist *marklist);
        static ledit_buffer_marklist *marklist_create(void);
        
        static void
       -swap_sz(size_t *a, size_t *b) {
       -        size_t tmp = *a;
       -        *a = *b;
       -        *b = tmp;
       -}
       -
       -static void
       -sort_range(size_t *l1, size_t *b1, size_t *l2, size_t *b2) {
       -        if (*l1 == *l2 && *b1 > *b2) {
       -                swap_sz(b1, b2);
       -        } else if (*l1 > *l2) {
       -                swap_sz(l1, l2);
       -                swap_sz(b1, b2);
       -        }
       -}
       -
       -static void
        marklist_destroy(ledit_buffer_marklist *marklist) {
                for (size_t i = 0; i < marklist->len; i++) {
                        free(marklist->marks[i].text);
       t@@ -190,7 +175,7 @@ buffer_insert_mark(ledit_buffer *buffer, char *mark, size_t len, size_t line, si
                        }
                }
                if (marklist->len == marklist->alloc) {
       -                size_t new_alloc = marklist->alloc > 0 ? marklist->alloc * 2 : 4;
       +                size_t new_alloc = ideal_array_size(marklist->alloc, add_sz(marklist->len, 1));
                        marklist->marks = ledit_reallocarray(
                            marklist->marks, new_alloc, sizeof(ledit_buffer_mark)
                        );
       t@@ -299,10 +284,8 @@ buffer_set_hard_line_based(ledit_buffer *buffer, int hl) {
        }
        
        void
       -buffer_add_view(ledit_buffer *buffer, ledit_theme *theme, enum ledit_mode mode, size_t line, size_t pos, long scroll_offset) {
       -        size_t new_num = buffer->views_num + 1;
       -        if (new_num <= buffer->views_num)
       -                err_overflow();
       +buffer_add_view(ledit_buffer *buffer, ledit_theme *theme, ledit_mode mode, size_t line, size_t pos, long scroll_offset) {
       +        size_t new_num = add_sz(buffer->views_num, 1);
                buffer->views = ledit_reallocarray(buffer->views, new_num, sizeof(ledit_view *));
                buffer->views[buffer->views_num] = view_create(buffer, theme, mode, line, pos);
                set_view_hard_line_text(buffer, buffer->views[buffer->views_num]);
       t@@ -354,10 +337,11 @@ buffer_load_file(ledit_buffer *buffer, char *filename, size_t line, char **errst
                len = ftell(file);
                if (len < 0) goto errorclose;
                if (fseek(file, 0, SEEK_SET)) goto errorclose;
       +        size_t lenz = add_sz((size_t)len, 2);
        
                ll = buffer_get_line(buffer, line);
                /* FIXME: insert in chunks instead of allocating huge buffer */
       -        file_contents = ledit_malloc(len + 2);
       +        file_contents = ledit_malloc(lenz);
                /* mimic nvi (or at least the openbsd version) - if the line
                   is empty, insert directly, otherwise insert after the line */
                if (ll->len > 0) {
       t@@ -485,8 +469,9 @@ buffer_insert_text_from_line_base(
            txtbuf *text_ret) {
                ledit_assert(dst_line != src_line);
                ledit_line *ll = buffer_get_line(buffer, src_line);
       +        ledit_assert(add_sz(src_index, src_len) <= ll->len);
                if (text_ret != NULL) {
       -                txtbuf_grow(text_ret, src_len);
       +                txtbuf_resize(text_ret, src_len);
                        text_ret->len = src_len;
                }
                if (src_index >= ll->gap) {
       t@@ -547,16 +532,6 @@ buffer_insert_text_from_line_base(
        }
        
        static void
       -move_text_gap(ledit_line *line, size_t index) {
       -        /* yes, I know sizeof(char) == 1 anyways */
       -        move_gap(
       -            line->text, sizeof(char), index,
       -            line->gap, line->cap, line->len,
       -            &line->gap
       -        );
       -}
       -
       -static void
        move_line_gap(ledit_buffer *buffer, size_t index) {
                move_gap(
                    buffer->lines, sizeof(ledit_line), index,
       t@@ -593,12 +568,9 @@ resize_and_move_line_gap(ledit_buffer *buffer, size_t min_size, size_t index) {
        static void
        buffer_insert_text_base(ledit_buffer *buffer, size_t line_index, size_t index, char *text, size_t len) {
                ledit_line *line = buffer_get_line(buffer, line_index);
       +        ledit_assert(index <= line->len);
                /* \0 is not included in line->len */
       -        /* FIXME: this if should be redundant now because resize_and_move... includes a check */
       -        if (line->len + len + 1 > line->cap || line->text == NULL)
       -                resize_and_move_text_gap(line, line->len + len + 1, index);
       -        else
       -                move_text_gap(line, index);
       +        resize_and_move_text_gap(line, add_sz3(line->len, len, 1), index);
                /* the gap is now located at 'index' and least large enough to hold the new text */
                memcpy(line->text + index, text, len);
                line->gap += len;
       t@@ -660,7 +632,7 @@ buffer_insert_text_with_newlines_base(
                        rem_len -= cur - last + 1;
                        last = cur + 1;
                }
       -        /* FIXME: check how legal this casting between pointers and ints is */
       +        /* FIXME: check how legal this casting between pointers and size_t's is */
                buffer_insert_text_base(buffer, cur_line, cur_index, last, text + len - last);
                if (end_line_ret)
                        *end_line_ret = cur_line;
       t@@ -678,15 +650,10 @@ init_line(ledit_buffer *buffer, ledit_line *line) {
                line->len = 0;
        }
        
       -/* FIXME: error checking (index out of bounds, etc.) */
        static void
        buffer_append_line_base(ledit_buffer *buffer, size_t line_index, size_t text_index, int break_text) {
       -        size_t new_len = buffer->lines_num + 1;
       -        if (new_len <= buffer->lines_num)
       -                err_overflow();
       -        size_t insert_index = line_index + 1;
       -        if (insert_index <= line_index)
       -                err_overflow();
       +        size_t new_len = add_sz(buffer->lines_num, 1);
       +        size_t insert_index = add_sz(line_index, 1);
                resize_and_move_line_gap(buffer, new_len, insert_index);
                buffer->lines_num++;
                buffer->lines_gap++;
       t@@ -721,6 +688,11 @@ buffer_delete_line_entries_base(ledit_buffer *buffer, size_t index1, size_t inde
                }
                move_line_gap(buffer, index1);
                buffer->lines_num -= index2 - index1 + 1;
       +        /* possibly decrease size of array - this needs to be after
       +           actually deleting the lines so the length is already less */
       +        size_t min_size = ideal_array_size(buffer->lines_cap, buffer->lines_num);
       +        if (min_size != buffer->lines_cap)
       +                resize_and_move_line_gap(buffer, buffer->lines_num, buffer->lines_gap);
                for (size_t i = 0; i < buffer->views_num; i++) {
                        view_notify_delete_lines(buffer->views[i], index1, index2);
                }
       t@@ -760,14 +732,24 @@ buffer_textlen(ledit_buffer *buffer, size_t line1, size_t byte1, size_t line2, s
                ledit_assert(line1 < line2 || (line1 == line2 && byte1 <= byte2));
                size_t len = 0;
                ledit_line *ll = buffer_get_line(buffer, line1);
       +        ledit_line *ll2 = buffer_get_line(buffer, line2);
       +        ledit_assert(byte1 <= ll->len);
       +        ledit_assert(byte2 <= ll2->len);
                if (line1 == line2) {
                        len = byte2 - byte1;
                } else {
                        /* + 1 for newline */
       -                len = ll->len - byte1 + byte2 + 1;
       +                len = add_sz3(ll->len - byte1, byte2, 1);
                        for (size_t i = line1 + 1; i < line2; i++) {
                                ll = buffer_get_line(buffer, i);
       -                        len += ll->len + 1;
       +                        /* ll->len + 1 should be valid anyways
       +                           because there *should* always be
       +                           space for '\0' at the end, i.e. ll->cap
       +                           should be at least ll->len + 1 */
       +                        /* FIXME: also, this overflow checking is
       +                           probably completely useless (it definitely
       +                           is really ugly) */
       +                        len += add_sz(ll->len, 1);
                        }
                }
                return len;
       t@@ -779,7 +761,7 @@ buffer_textlen(ledit_buffer *buffer, size_t line1, size_t byte1, size_t line2, s
              only done when it is re-rendered (and thus normalized because
              of pango's requirements). If a more efficient rendering
              backend is added, it would be good to optimize this, though. */
       -void
       +static void
        buffer_copy_text(ledit_buffer *buffer, char *dst, int line1, int byte1, int line2, int byte2) {
                ledit_assert(line1 < line2 || (line1 == line2 && byte1 <= byte2));
                ledit_line *ll1 = buffer_get_line(buffer, line1);
       t@@ -817,7 +799,7 @@ buffer_copy_text_to_txtbuf(
            size_t line2, size_t byte2) {
                ledit_assert(line1 < line2 || (line1 == line2 && byte1 <= byte2));
                size_t len = buffer_textlen(buffer, line1, byte1, line2, byte2);
       -        txtbuf_grow(buf, len + 1);
       +        txtbuf_resize(buf, len);
                buffer_copy_text(buffer, buf->text, line1, byte1, line2, byte2);
                buf->len = len;
        }
       t@@ -831,7 +813,6 @@ line_prev_utf8(ledit_line *line, size_t index) {
                        return 0;
                size_t i = index - 1;
                /* find valid utf8 char - this probably needs to be improved */
       -        /* FIXME: don't go off end or beginning */
                while (i > 0 && ((LINE_CHAR(line, i) & 0xC0) == 0x80))
                        i--;
                return i;
       t@@ -865,6 +846,8 @@ line_byte_to_char(ledit_line *line, size_t byte) {
        static void
        buffer_delete_line_section_base(ledit_buffer *buffer, size_t line, size_t start, size_t length) {
                ledit_line *l = buffer_get_line(buffer, line);
       +        (void)add_sz(start, length); /* just check that no overflow */
       +        ledit_assert(start + length <= l->len);
                if (start <= l->gap && start + length >= l->gap) {
                        l->gap = start;
                } else if (start < l->gap && start + length < l->gap) {
       t@@ -882,6 +865,10 @@ buffer_delete_line_section_base(ledit_buffer *buffer, size_t line, size_t start,
                        );
                }
                l->len -= length;
       +        /* possibly decrease size of line */
       +        size_t cap = ideal_array_size(l->cap, add_sz(l->len, 1));
       +        if (cap != l->cap)
       +                resize_and_move_text_gap(l, l->len + 1, l->gap);
                for (size_t i = 0; i < buffer->views_num; i++) {
                        view_notify_delete_text(buffer->views[i], line, start, length);
                }
       t@@ -915,6 +902,8 @@ delete_range_base(
                        }
                        ledit_line *line1 = buffer_get_line(buffer, line_index1);
                        ledit_line *line2 = buffer_get_line(buffer, line_index2);
       +                ledit_assert(byte_index1 <= line1->len);
       +                ledit_assert(byte_index2 <= line2->len);
                        buffer_delete_line_section_base(
                            buffer, line_index1, byte_index1, line1->len - byte_index1
                        );
       t@@ -942,26 +931,26 @@ undo_delete_helper(void *data, size_t line1, size_t byte1, size_t line2, size_t 
                delete_range_base(buffer, line1, byte1, line2, byte2, NULL);
        }
        
       -void
       -buffer_undo(ledit_buffer *buffer, enum ledit_mode mode, size_t *cur_line, size_t *cur_byte) {
       +undo_status
       +buffer_undo(ledit_buffer *buffer, ledit_mode mode, size_t *cur_line, size_t *cur_byte) {
                size_t min_line;
       -        ledit_undo(
       +        undo_status s = ledit_undo(
                    buffer->undo, mode, buffer, &undo_insert_helper,
                    &undo_delete_helper, cur_line, cur_byte, &min_line
                );
       -        /* FIXME: why is this check here? */
                if (min_line < buffer->lines_num) {
                        buffer_recalc_all_views_from_line(
                            buffer, min_line > 0 ? min_line - 1 : min_line
                        );
                }
                buffer->modified = 1;
       +        return s;
        }
        
       -void
       -buffer_redo(ledit_buffer *buffer, enum ledit_mode mode, size_t *cur_line, size_t *cur_byte) {
       +undo_status
       +buffer_redo(ledit_buffer *buffer, ledit_mode mode, size_t *cur_line, size_t *cur_byte) {
                size_t min_line;
       -        ledit_redo(
       +        undo_status s = ledit_redo(
                    buffer->undo, mode, buffer, &undo_insert_helper,
                    &undo_delete_helper, cur_line, cur_byte, &min_line
                );
       t@@ -971,12 +960,13 @@ buffer_redo(ledit_buffer *buffer, enum ledit_mode mode, size_t *cur_line, size_t
                        );
                }
                buffer->modified = 1;
       +        return s;
        }
        
        void
        buffer_delete_with_undo_base(
            ledit_buffer *buffer, ledit_range cur_range,
       -    int start_undo_group, enum ledit_mode mode, /* for undo */
       +    int start_undo_group, ledit_mode mode, /* for undo */
            size_t line_index1, size_t byte_index1,
            size_t line_index2, size_t byte_index2,
            txtbuf *text_ret) {
       t@@ -1001,7 +991,7 @@ buffer_delete_with_undo_base(
        void
        buffer_delete_with_undo(
            ledit_buffer *buffer, ledit_range cur_range,
       -    int start_undo_group, enum ledit_mode mode, /* for undo */
       +    int start_undo_group, ledit_mode mode, /* for undo */
            size_t line_index1, size_t byte_index1,
            size_t line_index2, size_t byte_index2,
            txtbuf *text_ret) {
       t@@ -1020,7 +1010,7 @@ void
        buffer_insert_with_undo_base(
            ledit_buffer *buffer,
            ledit_range cur_range, int set_range_end,
       -    int start_undo_group, enum ledit_mode mode,
       +    int start_undo_group, ledit_mode mode,
            size_t line, size_t byte,
            char *text, size_t len,
            size_t *line_ret, size_t *byte_ret) {
       t@@ -1053,7 +1043,7 @@ void
        buffer_insert_with_undo(
            ledit_buffer *buffer,
            ledit_range cur_range, int set_range_end,
       -    int start_undo_group, enum ledit_mode mode,
       +    int start_undo_group, ledit_mode mode,
            size_t line, size_t byte,
            char *text, size_t len,
            size_t *line_ret, size_t *byte_ret) {
   DIR diff --git a/buffer.h b/buffer.h
       t@@ -1,6 +1,14 @@
        #ifndef _LEDIT_BUFFER_H_
        #define _LEDIT_BUFFER_H_
        
       +#include <time.h>
       +#include <stdio.h>
       +#include <stddef.h>
       +
       +#include "common.h"
       +#include "txtbuf.h"
       +#include "undo.h"
       +
        typedef struct ledit_buffer ledit_buffer;
        
        #include "view.h"
       t@@ -72,7 +80,7 @@ void buffer_set_hard_line_based(ledit_buffer *buffer, int hl);
         */
        void buffer_add_view(
            ledit_buffer *buffer, ledit_theme *theme,
       -    enum ledit_mode mode, size_t line, size_t pos, long scroll_offset
       +    ledit_mode mode, size_t line, size_t pos, long scroll_offset
        );
        
        /*
       t@@ -165,14 +173,6 @@ void buffer_recalc_all_lines(ledit_buffer *buffer);
        size_t buffer_textlen(ledit_buffer *buffer, size_t line1, size_t byte1, size_t line2, size_t byte2);
        
        /*
       - * Copy text range into given buffer.
       - * - dst is null-terminated
       - * - dst must be large enough to contain the text and NUL (only use this together with buffer_textlen)
       - * - the range must be sorted already
       - */
       -void buffer_copy_text(ledit_buffer *buffer, char *dst, int line1, int byte1, int line2, int byte2);
       -
       -/*
         * Copy text range into given buffer and resize it if necessary.
         * - the range must be sorted already
         */
       t@@ -219,12 +219,12 @@ int buffer_get_mark(ledit_buffer *buffer, char *mark, size_t len, size_t *line_r
         * 'cur_line' and 'cur_byte' are filled with the new line and cursor
         * position after the undo.
         */
       -void buffer_undo(ledit_buffer *buffer, enum ledit_mode mode, size_t *cur_line, size_t *cur_byte);
       +undo_status buffer_undo(ledit_buffer *buffer, ledit_mode mode, size_t *cur_line, size_t *cur_byte);
        
        /*
         * Same as 'buffer_undo', but for redo.
         */
       -void buffer_redo(ledit_buffer *buffer, enum ledit_mode mode, size_t *cur_line, size_t *cur_byte);
       +undo_status buffer_redo(ledit_buffer *buffer, ledit_mode mode, size_t *cur_line, size_t *cur_byte);
        
        /*
         * Delete the given range (which does not need to be sorted yet) and
       t@@ -237,7 +237,7 @@ void buffer_redo(ledit_buffer *buffer, enum ledit_mode mode, size_t *cur_line, s
         */
        void buffer_delete_with_undo_base(
            ledit_buffer *buffer, ledit_range cur_range,
       -    int start_undo_group, enum ledit_mode mode, /* for undo */
       +    int start_undo_group, ledit_mode mode, /* for undo */
            size_t line_index1, size_t byte_index1,
            size_t line_index2, size_t byte_index2,
            txtbuf *text_ret
       t@@ -249,7 +249,7 @@ void buffer_delete_with_undo_base(
         */
        void buffer_delete_with_undo(
            ledit_buffer *buffer, ledit_range cur_range,
       -    int start_undo_group, enum ledit_mode mode, /* for undo */
       +    int start_undo_group, ledit_mode mode, /* for undo */
            size_t line_index1, size_t byte_index1,
            size_t line_index2, size_t byte_index2,
            txtbuf *text_ret
       t@@ -268,7 +268,7 @@ void buffer_delete_with_undo(
        void buffer_insert_with_undo_base(
            ledit_buffer *buffer,
            ledit_range cur_range, int set_range_end,
       -    int start_undo_group, enum ledit_mode mode,
       +    int start_undo_group, ledit_mode mode,
            size_t line, size_t byte,
            char *text, size_t len,
            size_t *line_ret, size_t *byte_ret
       t@@ -281,7 +281,7 @@ void buffer_insert_with_undo_base(
        void buffer_insert_with_undo(
            ledit_buffer *buffer,
            ledit_range cur_range, int set_range_end,
       -    int start_undo_group, enum ledit_mode mode,
       +    int start_undo_group, ledit_mode mode,
            size_t line, size_t byte,
            char *text, size_t len,
            size_t *line_ret, size_t *byte_ret
   DIR diff --git a/cache.c b/cache.c
       t@@ -6,6 +6,7 @@
        #include <pango/pangoxft.h>
        #include <X11/extensions/Xdbe.h>
        
       +#include "util.h"
        #include "common.h"
        #include "memory.h"
        #include "cache.h"
       t@@ -94,9 +95,6 @@ cache_get_layout(ledit_cache *cache, size_t index) {
                return &cache->layouts[index];
        }
        
       -/* FIXME: decide on int or size_t, but not both */
       -/* or maybe ssize_t */
       -
        /* FIXME: max pixmap cache size */
        void
        cache_assign_pixmap_index(
       t@@ -126,10 +124,9 @@ cache_assign_pixmap_index(
                }
        
                /* no free entry found, increase cache size */
       -        /* FIXME: what is the ideal size to resize to? */
       -        /* FIXME: overflow */
                /* FIXME: maybe have maximum cache size */
       -        cache->pixmaps = ledit_reallocarray(cache->pixmaps, cache->num_pixmaps * 2, sizeof(cache_pixmap));
       +        size_t new_alloc = ideal_array_size(cache->num_pixmaps, add_sz(cache->num_pixmaps, 1));
       +        cache->pixmaps = ledit_reallocarray(cache->pixmaps, new_alloc, sizeof(cache_pixmap));
                entry_index = cache->num_pixmaps;
                for (size_t i = cache->num_pixmaps; i < cache->num_pixmaps * 2; i++) {
                        cache->pixmaps[i].line = 0;
   DIR diff --git a/cache.h b/cache.h
       t@@ -1,3 +1,10 @@
       +#ifndef _CACHE_H_
       +#define _CACHE_H_
       +
       +#include <stddef.h>
       +#include <X11/Xlib.h>
       +#include <pango/pangoxft.h>
       +
        /*
         *The maximum number of layouts in the cache.
         */
       t@@ -129,3 +136,5 @@ void cache_assign_layout_index(
            void (*set_layout_line)(void *, size_t, size_t),
            void (*invalidate_layout_line)(void *, size_t)
        );
       +
       +#endif
   DIR diff --git a/cleanup.h b/cleanup.h
       t@@ -1,4 +1,9 @@
       +#ifndef _CLEANUP_H_
       +#define _CLEANUP_H_
       +
        /* This is here so it can be called from other places
           even though the function definition is in ledit.c */
        void ledit_cleanup(void);
        void ledit_emergencydump(void);
       +
       +#endif
   DIR diff --git a/common.h b/common.h
       t@@ -1,3 +1,8 @@
       +#ifndef _COMMON_H_
       +#define _COMMON_H_
       +
       +#include <X11/Xlib.h>
       +
        typedef struct {
                size_t line1;
                size_t byte1;
       t@@ -5,11 +10,11 @@ typedef struct {
                size_t byte2;
        } ledit_range;
        
       -enum ledit_mode {
       +typedef enum {
                NORMAL = 1,
                INSERT = 2,
                VISUAL = 4
       -};
       +} ledit_mode;
        
        typedef struct {
                Display *dpy;
       t@@ -19,3 +24,5 @@ typedef struct {
                int depth;
                int redraw;
        } ledit_common;
       +
       +#endif
   DIR diff --git a/draw_util.c b/draw_util.c
       t@@ -0,0 +1,43 @@
       +#include <X11/Xlib.h>
       +#include <X11/Xft/Xft.h>
       +
       +#include "memory.h"
       +#include "window.h"
       +#include "draw_util.h"
       +
       +ledit_draw *
       +draw_create(ledit_window *window, int w, int h) {
       +        ledit_draw *draw = ledit_malloc(sizeof(ledit_draw));
       +        draw->w = w;
       +        draw->h = h;
       +        draw->pixmap = XCreatePixmap(
       +            window->common->dpy, window->drawable, w, h, window->common->depth
       +        );
       +        draw->xftdraw = XftDrawCreate(
       +            window->common->dpy, draw->pixmap, window->common->vis, window->common->cm
       +        );
       +        return draw;
       +}
       +
       +void
       +draw_grow(ledit_window *window, ledit_draw *draw, int w, int h) {
       +        /* FIXME: sensible default pixmap sizes here */
       +        /* FIXME: maybe shrink the pixmaps at some point */
       +        if (draw->w < w || draw->h < h) {
       +                draw->w = w > draw->w ? w + 10 : draw->w;
       +                draw->h = h > draw->h ? h + 10 : draw->h;
       +                XFreePixmap(window->common->dpy, draw->pixmap);
       +                draw->pixmap = XCreatePixmap(
       +                    window->common->dpy, window->drawable,
       +                    draw->w, draw->h, window->common->depth
       +                );
       +                XftDrawChange(draw->xftdraw, draw->pixmap);
       +        }
       +}
       +
       +void
       +draw_destroy(ledit_window *window, ledit_draw *draw) {
       +        XFreePixmap(window->common->dpy, draw->pixmap);
       +        XftDrawDestroy(draw->xftdraw);
       +        free(draw);
       +}
   DIR diff --git a/draw_util.h b/draw_util.h
       t@@ -0,0 +1,34 @@
       +#ifndef _DRAW_UTIL_H_
       +#define _DRAW_UTIL_H_
       +
       +#include <X11/Xlib.h>
       +#include <X11/Xft/Xft.h>
       +
       +#include "window.h"
       +
       +/*
       + * This is just a basic wrapper for XftDraws and Pixmaps
       + * that is used by the window for its text display at the bottom.
       + */
       +typedef struct {
       +        XftDraw *xftdraw;
       +        Pixmap pixmap;
       +        int w, h;
       +} ledit_draw;
       +
       +/*
       + * Create a draw with the specified width and height.
       + */
       +ledit_draw *draw_create(ledit_window *window, int w, int h);
       +
       +/*
       + * Make sure the size of the draw is at least the given width and height.
       + */
       +void draw_grow(ledit_window *window, ledit_draw *draw, int w, int h);
       +
       +/*
       + * Destroy a draw.
       + */
       +void draw_destroy(ledit_window *window, ledit_draw *draw);
       +
       +#endif
   DIR diff --git a/keys.c b/keys.c
       t@@ -13,6 +13,7 @@
        #include "theme.h"
        #include "window.h"
        #include "keys.h"
       +#include "keys_config.h"
        
        KEY_LANGS;
        
       t@@ -32,7 +33,6 @@ get_language_index(char *lang) {
        /* FIXME: The Mod*Masks can be remapped, so it isn't really clear what is what */
        /* most are disabled now to avoid issues with e.g. numlock */
        static unsigned int importantmod = ShiftMask | ControlMask | Mod1Mask;
       -#define XK_ANY_MOD    UINT_MAX
        
        int
        match_key(unsigned int mask, unsigned int state)
   DIR diff --git a/keys.h b/keys.h
       t@@ -1,29 +1,13 @@
       -#define LENGTH(X) (sizeof(X) / sizeof(X[0]))
       +#ifndef _KEYS_H_
       +#define _KEYS_H_
        
       -/*
       - * These are the language strings compared with the language strings that
       - * xkb gives in order to change the key mapping on layout change events.
       - */
       -#define KEY_LANGS             \
       -static char *key_langs[] = {  \
       -        "English (US)",       \
       -        "German",             \
       -        "Urdu (Pakistan)",    \
       -        "Hindi (Bolnagri)"    \
       -}
       +#include <X11/Xlib.h>
       +#include "window.h"
        
       -#define GEN_KEY_ARRAY(key_struct, en, de, ur, hi) \
       -static struct {                                   \
       -        key_struct *keys;                         \
       -        int num_keys;                             \
       -} keys[] = {                                      \
       -        {en, LENGTH(en)},                         \
       -        {de, LENGTH(de)},                         \
       -        {ur, LENGTH(ur)},                         \
       -        {hi, LENGTH(hi)}                          \
       -}
       +#define LENGTH(X) (sizeof(X) / sizeof(X[0]))
        
       -#define LANG_KEYS(index) &keys[index]
       +#define XK_ANY_MOD    UINT_MAX
       +#define XK_NO_MOD     0
        
        /* get the index of a language with the given name, or -1 if none exists */
        int get_language_index(char *lang);
       t@@ -34,3 +18,5 @@ void preprocess_key(
            ledit_window *window, XEvent *event, KeySym *sym_ret,
            char *buf_ret, int buf_size, int *buf_len_ret
        );
       +
       +#endif
   DIR diff --git a/keys_basic.c b/keys_basic.c
       t@@ -7,8 +7,8 @@
           -> space is hidden when e.g. ltr text left and rtl text on right is wrapped */
        /* FIXME: some weird things still happen with selections staying as "ghosts"
           and being deleted at some later time even though they're not shown anymore */
       -/* FIXME: there seem to be some issues with undo, but I couldn't reproduce
       -   them reliably yet */
       +/* FIXME: delete everything concerned with selections in insert mode since
       +   they are now not allowed at all */
        #include <stdio.h>
        #include <stdlib.h>
        
       t@@ -20,6 +20,7 @@
        #include <X11/XF86keysym.h>
        #include <X11/cursorfont.h>
        
       +#include "util.h"
        #include "memory.h"
        #include "common.h"
        #include "txtbuf.h"
       t@@ -32,6 +33,7 @@
        #include "search.h"
        
        #include "keys.h"
       +#include "keys_config.h"
        #include "keys_basic.h"
        #include "keys_command.h"
        #include "keys_basic_config.h"
       t@@ -108,7 +110,7 @@ void clear_key_stack(void);
        
        static void move_cursor_left_right(ledit_view *view, int dir, int allow_illegal_index);
        static void move_cursor_up_down(ledit_view *view, int dir);
       -static void push_num(int num);
       +static void push_num(ledit_view *view, int num);
        static void delete_cb(ledit_view *view, size_t line, size_t char_pos, enum key_type type);
        static void yank_cb(ledit_view *view, size_t line, size_t char_pos, enum key_type type);
        static void get_new_line_softline(
       t@@ -129,14 +131,6 @@ view_locked_error(ledit_view *view) {
        #define CHECK_VIEW_LOCKED(view) if (view->lock_text) return view_locked_error(view)
        #define CHECK_VIEW_LOCKED_NORETURN(view) if (view->lock_text) (void)view_locked_error(view)
        
       -/* FIXME: move to common */
       -static void
       -swap_sz(size_t *a, size_t *b) {
       -        size_t tmp = *a;
       -        *a = *b;
       -        *b = tmp;
       -}
       -
        static int
        key_stack_empty(void) {
                return key_stack.len == 0;
       t@@ -146,7 +140,7 @@ static struct key_stack_elem *
        push_key_stack(void) {
                struct key_stack_elem *e;
                if (key_stack.len >= key_stack.alloc) {
       -                size_t new_alloc = key_stack.alloc > 0 ? key_stack.alloc * 2 : 4;
       +                size_t new_alloc = ideal_array_size(key_stack.alloc, add_sz(key_stack.len, 1));
                        key_stack.stack = ledit_reallocarray(
                            key_stack.stack, new_alloc, sizeof(struct key_stack_elem)
                        );
       t@@ -207,7 +201,7 @@ err_invalid_key(ledit_view *view) {
         *   possibly a second one if the top one was a number key.
         */
        static int
       -get_key_repeat_and_motion_cb(motion_callback *cb_ret) {
       +get_key_repeat_and_motion_cb(ledit_view *view, motion_callback *cb_ret) {
                int num = 1;
                struct key_stack_elem *e = pop_key_stack();
                if (e != NULL) {
       t@@ -220,7 +214,10 @@ get_key_repeat_and_motion_cb(motion_callback *cb_ret) {
                        if (e != NULL) {
                                int new_count = e->count > 0 ? e->count : 1;
                                if (INT_MAX / new_count < num) {
       -                                /* FIXME: show error */
       +                                window_show_message(
       +                                    view->window,
       +                                    "Integer overflow in key repetition", -1
       +                                );
                                        num = INT_MAX;
                                }
                                num *= new_count;
       t@@ -274,7 +271,7 @@ static struct repetition_stack_elem *
        push_repetition_stack(void) {
                struct repetition_stack_elem *e;
                if (repetition_stack.tmp_len >= repetition_stack.tmp_alloc) {
       -                size_t new_alloc = repetition_stack.tmp_alloc > 0 ? repetition_stack.tmp_alloc * 2 : 4;
       +                size_t new_alloc = ideal_array_size(repetition_stack.tmp_alloc, add_sz(repetition_stack.tmp_len, 1));
                        repetition_stack.tmp_stack = ledit_reallocarray(
                            repetition_stack.tmp_stack,
                            new_alloc, sizeof(struct repetition_stack_elem)
       t@@ -507,6 +504,7 @@ delete_chars_forwards(ledit_view *view, char *text, size_t len) {
                    view, view->cur_line, view->cur_index
                );
                view_set_line_cursor_attrs(view, view->cur_line, view->cur_index);
       +        finalize_repetition_stack();
                return (struct action){ACTION_NONE, NULL};
        }
        
       t@@ -537,6 +535,7 @@ delete_chars_backwards(ledit_view *view, char *text, size_t len) {
                    view, view->cur_line, start_index
                );
                view_set_line_cursor_attrs(view, view->cur_line, view->cur_index);
       +        finalize_repetition_stack();
                return (struct action){ACTION_NONE, NULL};
        }
        
       t@@ -615,7 +614,7 @@ move_to_line(ledit_view *view, char *text, size_t len) {
                (void)text;
                (void)len;
                motion_callback cb = NULL;
       -        int repeat = get_key_repeat_and_motion_cb(&cb);
       +        int repeat = get_key_repeat_and_motion_cb(view, &cb);
                size_t line;
                if (repeat > 0)
                        line = (size_t)repeat > view->lines_num ? view->lines_num : (size_t)repeat;
       t@@ -624,7 +623,11 @@ move_to_line(ledit_view *view, char *text, size_t len) {
                else
                        return err_invalid_key(view);
                if (cb != NULL) {
       -                cb(view, line - 1, 0, KEY_MOTION_LINE);
       +                /* this is a bit of a hack - because move_to_line always works
       +                   with hard lines, it sets the index to ll->len so e.g. the delete
       +                   callback deletes until the end of the line even in soft line mode */
       +                ledit_line *ll = buffer_get_line(view->buffer, line - 1);
       +                cb(view, line - 1, ll->len, KEY_MOTION_LINE);
                } else {
                        view_wipe_line_cursor_attrs(view, view->cur_line);
                        view->cur_line = line - 1;
       t@@ -822,6 +825,7 @@ screen_up(ledit_view *view, char *text, size_t len) {
                (void)text;
                (void)len;
                int repeat = get_key_repeat();
       +        /* FIXME: overflow */
                if (repeat >= 0)
                        move_half_screen(view, -(repeat == 0 ? 2 : repeat*2));
                else
       t@@ -903,7 +907,7 @@ change(ledit_view *view, char *text, size_t len) {
                (void)len;
                CHECK_VIEW_LOCKED(view);
                motion_callback cb = NULL;
       -        int num = get_key_repeat_and_motion_cb(&cb);
       +        int num = get_key_repeat_and_motion_cb(view, &cb);
                if (num == -1)
                        return err_invalid_key(view);
                if (view->mode == VISUAL) {
       t@@ -971,7 +975,7 @@ yank(ledit_view *view, char *text, size_t len) {
                if (!paste_buffer)
                        paste_buffer = txtbuf_new();
                if (view->mode == VISUAL) {
       -                view_sort_selection(
       +                sort_range(
                            &view->sel.line1, &view->sel.byte1, &view->sel.line2, &view->sel.byte2
                        );
                        buffer_copy_text_to_txtbuf(
       t@@ -990,7 +994,7 @@ yank(ledit_view *view, char *text, size_t len) {
                        clear_key_stack();
                } else {
                        motion_callback cb = NULL;
       -                int num = get_key_repeat_and_motion_cb(&cb);
       +                int num = get_key_repeat_and_motion_cb(view, &cb);
                        if (num == 0)
                                num = 1;
                        if (cb == &yank_cb) {
       t@@ -1086,7 +1090,7 @@ delete(ledit_view *view, char *text, size_t len) {
                (void)len;
                CHECK_VIEW_LOCKED(view);
                motion_callback cb = NULL;
       -        int num = get_key_repeat_and_motion_cb(&cb);
       +        int num = get_key_repeat_and_motion_cb(view, &cb);
                if (num == -1)
                        return err_invalid_key(view);
                if (delete_selection(view)) {
       t@@ -1255,20 +1259,27 @@ paste_normal_backwards(ledit_view *view, char *text, size_t len) {
        }
        
        static void
       -push_num(int num) {
       +push_num(ledit_view *view, int num) {
                struct key_stack_elem *e = peek_key_stack();
                if (!e || !(e->key & KEY_NUMBER)) {
                        e = push_key_stack();
                        e->key = KEY_NUMBER;
                        e->followup = KEY_NUMBER|KEY_NUMBERALLOWED;
                }
       -        /* FIXME: error messages */
                if (INT_MAX / 10 < e->count) {
       +                window_show_message(
       +                    view->window,
       +                    "Integer overflow in key repetition", -1
       +                );
                        clear_key_stack();
                        return;
                }
                e->count *= 10;
                if (INT_MAX - num < e->count) {
       +                window_show_message(
       +                    view->window,
       +                    "Integer overflow in key repetition", -1
       +                );
                        clear_key_stack();
                        return;
                }
       t@@ -1280,7 +1291,7 @@ push_0(ledit_view *view, char *text, size_t len) {
                (void)view;
                (void)text;
                (void)len;
       -        push_num(0);
       +        push_num(view, 0);
                return (struct action){ACTION_NONE, NULL};
        }
        
       t@@ -1289,7 +1300,7 @@ push_1(ledit_view *view, char *text, size_t len) {
                (void)view;
                (void)text;
                (void)len;
       -        push_num(1);
       +        push_num(view, 1);
                return (struct action){ACTION_NONE, NULL};
        }
        
       t@@ -1298,7 +1309,7 @@ push_2(ledit_view *view, char *text, size_t len) {
                (void)view;
                (void)text;
                (void)len;
       -        push_num(2);
       +        push_num(view, 2);
                return (struct action){ACTION_NONE, NULL};
        }
        
       t@@ -1307,7 +1318,7 @@ push_3(ledit_view *view, char *text, size_t len) {
                (void)view;
                (void)text;
                (void)len;
       -        push_num(3);
       +        push_num(view, 3);
                return (struct action){ACTION_NONE, NULL};
        }
        
       t@@ -1316,7 +1327,7 @@ push_4(ledit_view *view, char *text, size_t len) {
                (void)view;
                (void)text;
                (void)len;
       -        push_num(4);
       +        push_num(view, 4);
                return (struct action){ACTION_NONE, NULL};
        }
        
       t@@ -1325,7 +1336,7 @@ push_5(ledit_view *view, char *text, size_t len) {
                (void)view;
                (void)text;
                (void)len;
       -        push_num(5);
       +        push_num(view, 5);
                return (struct action){ACTION_NONE, NULL};
        }
        
       t@@ -1334,7 +1345,7 @@ push_6(ledit_view *view, char *text, size_t len) {
                (void)view;
                (void)text;
                (void)len;
       -        push_num(6);
       +        push_num(view, 6);
                return (struct action){ACTION_NONE, NULL};
        }
        
       t@@ -1343,7 +1354,7 @@ push_7(ledit_view *view, char *text, size_t len) {
                (void)view;
                (void)text;
                (void)len;
       -        push_num(7);
       +        push_num(view, 7);
                return (struct action){ACTION_NONE, NULL};
        }
        
       t@@ -1352,7 +1363,7 @@ push_8(ledit_view *view, char *text, size_t len) {
                (void)view;
                (void)text;
                (void)len;
       -        push_num(8);
       +        push_num(view, 8);
                return (struct action){ACTION_NONE, NULL};
        }
        
       t@@ -1361,7 +1372,7 @@ push_9(ledit_view *view, char *text, size_t len) {
                (void)view;
                (void)text;
                (void)len;
       -        push_num(9);
       +        push_num(view, 9);
                return (struct action){ACTION_NONE, NULL};
        }
        
       t@@ -1413,7 +1424,7 @@ move_to_eol(ledit_view *view, char *text, size_t len) {
                (void)len;
                CHECK_VIEW_LOCKED(view);
                motion_callback cb;
       -        int num = get_key_repeat_and_motion_cb(&cb);
       +        int num = get_key_repeat_and_motion_cb(view, &cb);
                if (num == -1)
                        return err_invalid_key(view);
                if (num == 0)
       t@@ -1461,7 +1472,7 @@ name(ledit_view *view, char *text, size_t len) {                                
                (void)text;                                                              \
                (void)len;                                                               \
                motion_callback cb;                                                      \
       -        int num = get_key_repeat_and_motion_cb(&cb);                             \
       +        int num = get_key_repeat_and_motion_cb(view, &cb);                       \
                if (num == -1)                                                           \
                        return err_invalid_key(view);                                    \
                if (num == 0)                                                            \
       t@@ -1510,7 +1521,7 @@ GEN_WORD_MOVEMENT(prev_bigword, view_prev_bigword)
        static void
        move_cursor_left_right(ledit_view *view, int dir, int allow_illegal_index) {
                motion_callback cb;
       -        int num = get_key_repeat_and_motion_cb(&cb);
       +        int num = get_key_repeat_and_motion_cb(view, &cb);
                if (num == -1)
                        (void)err_invalid_key(view);
                if (num == 0)
       t@@ -1540,6 +1551,8 @@ move_cursor_left_right(ledit_view *view, int dir, int allow_illegal_index) {
                        if (view->mode == VISUAL) {
                                view_set_selection(view, view->sel.line1, view->sel.byte1, view->cur_line, new_index);
                        } else if (view->mode == INSERT && view->sel_valid) {
       +                        /* FIXME: I guess this is unnecessary now that no
       +                           selection is allowed in insert mode */
                                view_wipe_selection(view);
                        } else if (view->mode == NORMAL) {
                                view_set_line_cursor_attrs(view, view->cur_line, view->cur_index);
       t@@ -1618,8 +1631,11 @@ enter_insert(ledit_view *view, char *text, size_t len) {
                (void)text;
                (void)len;
                CHECK_VIEW_LOCKED(view);
       -        if (view->mode == NORMAL)
       +        if (view->mode == NORMAL) {
                        view_wipe_line_cursor_attrs(view, view->cur_line);
       +        } else if (view->mode == VISUAL) {
       +                view_wipe_selection(view);
       +        }
                view_set_mode(view, INSERT);
                clear_key_stack();
                return (struct action){ACTION_NONE, NULL};
       t@@ -1632,7 +1648,7 @@ move_cursor_up_down(ledit_view *view, int dir) {
                int new_softline;
        
                motion_callback cb;
       -        int num = get_key_repeat_and_motion_cb(&cb);
       +        int num = get_key_repeat_and_motion_cb(view, &cb);
                if (num == -1)
                        (void)err_invalid_key(view);
                if (num == 0)
       t@@ -1649,8 +1665,6 @@ move_cursor_up_down(ledit_view *view, int dir) {
                        view_get_softline_bounds(view, new_line, new_softline, &start, &end);
                        cb(view, new_line, start, KEY_MOTION_LINE);
                } else {
       -                /* FIXME: when selecting on last line, moving down moves the cursor back
       -                   one (when it stays on the same line because it's the last one) */
                        int lineno, x;
                        view_pos_to_x_softline(view, view->cur_line, view->cur_index, &x, &lineno);
                        view->cur_index = view_x_softline_to_pos(view, new_line, x, new_softline);
       t@@ -1744,7 +1758,7 @@ cursor_to_first_non_ws(ledit_view *view, char *text, size_t len) {
                (void)text;
                (void)len;
                motion_callback cb;
       -        int num = get_key_repeat_and_motion_cb(&cb);
       +        int num = get_key_repeat_and_motion_cb(view, &cb);
                if (num != 0)
                        return err_invalid_key(view);
                size_t new_index = 0;
       t@@ -1778,7 +1792,7 @@ cursor_to_beginning(ledit_view *view, char *text, size_t len) {
                (void)text;
                (void)len;
                motion_callback cb;
       -        int num = get_key_repeat_and_motion_cb(&cb);
       +        int num = get_key_repeat_and_motion_cb(view, &cb);
                if (num != 0)
                        return err_invalid_key(view);
                /* FIXME: should anything be done with num? */
       t@@ -1831,13 +1845,11 @@ switch_selection_end(ledit_view *view, char *text, size_t len) {
                return (struct action){ACTION_NONE, NULL};
        }
        
       -#define XK_ANY_MOD    UINT_MAX
       -#define XK_NO_MOD     0
       -
        static struct action
        enter_commandedit(ledit_view *view, char *text, size_t len) {
                (void)text;
                (void)len;
       +        /* FIXME: wipe selection? */
                char *str = view->sel_valid ? ":'<,'>" : ":";
                window_set_bottom_bar_text(view->window, str, -1);
                window_set_bottom_bar_cursor(view->window, strlen(str));
       t@@ -1888,7 +1900,7 @@ static struct action
        jump_to_mark_cb(ledit_view *view, char *text, size_t len) {
                grab_char_cb = NULL;
                motion_callback cb;
       -        int num = get_key_repeat_and_motion_cb(&cb);
       +        int num = get_key_repeat_and_motion_cb(view, &cb);
                if (num > 0)
                        return err_invalid_key(view);
                size_t line = 0, index = 0;
       t@@ -1962,23 +1974,30 @@ static struct action
        show_line(ledit_view *view, char *text, size_t len) {
                (void)text;
                (void)len;
       -        int textlen = snprintf(NULL, 0, "Line %zu of %zu", view->cur_line + 1, view->lines_num);
       -        char *str = ledit_malloc(textlen + 1);
       -        snprintf(str, textlen + 1, "Line %zu of %zu", view->cur_line + 1, view->lines_num);
       -        window_show_message(view->window, str, textlen);
       +        window_show_message_fmt(
       +            view->window,
       +            "%s: %s: line %zu of %zu",
       +            view->buffer->filename ? view->buffer->filename : "(no filename)",
       +            view->buffer->modified ? "modified" : "unmodified",
       +            add_sz(view->cur_line, 1), view->lines_num
       +        );
                discard_repetition_stack();
                return (struct action){ACTION_NONE, NULL};
        }
        
       -/* FIXME: return status! */
        static struct action
        undo(ledit_view *view, char *text, size_t len) {
                (void)text;
                (void)len;
                CHECK_VIEW_LOCKED(view);
       +        int num = get_key_repeat();
       +        if (num == -1)
       +                return err_invalid_key(view);
       +        if (num == 0)
       +                num = 1;
                view_wipe_selection(view);
                view_wipe_line_cursor_attrs(view, view->cur_line);
       -        view_undo(view);
       +        view_undo(view, num);
                view_set_line_cursor_attrs(view, view->cur_line, view->cur_index);
                finalize_repetition_stack();
                return (struct action){ACTION_NONE, NULL};
       t@@ -1989,9 +2008,14 @@ redo(ledit_view *view, char *text, size_t len) {
                (void)text;
                (void)len;
                CHECK_VIEW_LOCKED(view);
       +        int num = get_key_repeat();
       +        if (num == -1)
       +                return err_invalid_key(view);
       +        if (num == 0)
       +                num = 1;
                view_wipe_selection(view);
                view_wipe_line_cursor_attrs(view, view->cur_line);
       -        view_redo(view);
       +        view_redo(view, num);
                view_set_line_cursor_attrs(view, view->cur_line, view->cur_index);
                finalize_repetition_stack();
                return (struct action){ACTION_NONE, NULL};
       t@@ -2081,7 +2105,7 @@ dummy_cursor_helper(ledit_view *view, size_t line, size_t index, int num) {
        static struct action                                                               \
        name##_cb(ledit_view *view, char *text, size_t len) {                              \
                motion_callback cb = NULL;                                                 \
       -        int num = get_key_repeat_and_motion_cb(&cb);                               \
       +        int num = get_key_repeat_and_motion_cb(view, &cb);                         \
                if (num == -1)                                                             \
                        return err_invalid_key(view);                                      \
                if (num == 0)                                                              \
       t@@ -2249,15 +2273,26 @@ repeat_command(ledit_view *view, char *text, size_t len) {
                (void)view;
                (void)text;
                (void)len;
       +        int num = get_key_repeat();
       +        if (num == -1)
       +                return err_invalid_key(view);
       +        if (num == 0)
       +                num = 1;
       +        if (repetition_stack.len == 0) {
       +                window_show_message(view->window, "No previous command", -1);
       +                discard_repetition_stack();
       +                return (struct action){ACTION_NONE, NULL};
       +        }
                int found;
                repetition_stack.replaying = 1;
       -        clear_key_stack();
       -        unwind_repetition_stack();
       -        struct repetition_stack_elem *e = get_cur_repetition_stack_elem();
       -        while (e) {
       -                (void)handle_key(view, e->key_text, e->len, e->sym, e->key_state, e->lang_index, &found);
       -                advance_repetition_stack();
       -                e = get_cur_repetition_stack_elem();
       +        for (int i = 0; i < num; i++) {
       +                unwind_repetition_stack();
       +                struct repetition_stack_elem *e = get_cur_repetition_stack_elem();
       +                while (e) {
       +                        (void)handle_key(view, e->key_text, e->len, e->sym, e->key_state, e->lang_index, &found);
       +                        advance_repetition_stack();
       +                        e = get_cur_repetition_stack_elem();
       +                }
                }
                repetition_stack.replaying = 0;
                discard_repetition_stack();
       t@@ -2290,11 +2325,13 @@ basic_key_handler(ledit_view *view, XEvent *event, int lang_index) {
                struct action act = handle_key(view, buf, (size_t)n, sym, key_state, lang_index, &found);
                if (found && n > 0 && !view->window->message_shown)
                        window_hide_message(view->window);
       -        else
       +        else if (msg_shown)
                        view->window->message_shown = msg_shown;
        
       +        /* FIXME: add attribute for this to keys - this doesn't take e.g. cursor keys into account! */
                if (found && n > 0)
                        view_ensure_cursor_shown(view);
       -        /* FIXME: maybe show error if not found */
       +        if (!found && n > 0)
       +                window_show_message(view->window, "Invalid key", -1);
                return act;
        }
   DIR diff --git a/keys_basic.h b/keys_basic.h
       t@@ -1,3 +1,11 @@
       +#ifndef _KEYS_BASIC_H_
       +#define _KEYS_BASIC_H_
       +
       +#include <X11/Xlib.h>
       +#include "view.h"
       +
        /* perform cleanup of global data */
        void basic_key_cleanup(void);
        struct action basic_key_handler(ledit_view *view, XEvent *event, int lang_index);
       +
       +#endif
   DIR diff --git a/keys_basic_config.h b/keys_basic_config.h
       t@@ -20,7 +20,7 @@ struct key {
                char *text;                                          /* for keys that correspond with text */
                unsigned int mods;                                   /* modifier mask */
                KeySym keysym;                                       /* for other keys, e.g. arrow keys */
       -        enum ledit_mode modes;                               /* modes in which this keybinding is functional */
       +        ledit_mode modes;                               /* modes in which this keybinding is functional */
                enum key_type prev_keys;                             /* allowed previous keys */
                enum key_type key_types;                             /* key types - used to determine if the key is allowed */
                struct action (*func)(ledit_view *, char *, size_t); /* callback function */
       t@@ -109,7 +109,7 @@ static struct key keys_en[] = {
                {NULL, 0, XK_Right, VISUAL|INSERT|NORMAL, KEY_ANY, KEY_ANY, &cursor_right},
                {NULL, 0, XK_Up, VISUAL|INSERT|NORMAL, KEY_ANY, KEY_ANY, &cursor_up},
                {NULL, 0, XK_Down, VISUAL|INSERT|NORMAL, KEY_ANY, KEY_ANY, &cursor_down},
       -        {NULL, 0, XK_Return, INSERT, KEY_ANY, KEY_ANY, &return_key},
       +        {NULL, XK_ANY_MOD, XK_Return, INSERT, KEY_ANY, KEY_ANY, &return_key},
                {NULL, 0, XK_Delete, INSERT, KEY_ANY, KEY_ANY, &delete_key},
                {NULL, 0, XK_Escape, NORMAL|VISUAL|INSERT, KEY_ANY, KEY_ANY, &escape_key},
                {"i",  0, 0, NORMAL|VISUAL, KEY_ANY, KEY_ANY, &enter_insert},
       t@@ -142,16 +142,16 @@ static struct key keys_en[] = {
                {"c",  0, 0, NORMAL|VISUAL, KEY_ANY, KEY_MOTION|KEY_NUMBERALLOWED, &change},
                {"v",  0, 0, NORMAL, KEY_ANY, KEY_ANY, &enter_visual},
                {"o",  0, 0, VISUAL, KEY_ANY, KEY_ANY, &switch_selection_end},
       -        {"c",  ControlMask, 0, INSERT|VISUAL, KEY_ANY, KEY_ANY, &clipcopy},
       +        {"c",  ControlMask, 0, VISUAL, KEY_ANY, KEY_ANY, &clipcopy},
                {"v",  ControlMask, 0, INSERT, KEY_ANY, KEY_ANY, &clippaste},
                {"g",  ControlMask, 0, NORMAL|VISUAL, KEY_ANY, KEY_ANY, &show_line},
                {":",  0, 0, NORMAL|VISUAL, KEY_ANY, KEY_ANY, &enter_commandedit},
       -        {"?",  0, 0, NORMAL|VISUAL, KEY_ANY, KEY_ANY, &enter_searchedit_backward},
       -        {"/",  0, 0, NORMAL|VISUAL, KEY_ANY, KEY_ANY, &enter_searchedit_forward},
       -        {"n",  0, 0, NORMAL|VISUAL, KEY_ANY, KEY_ANY, &key_search_next},
       -        {"N",  0, 0, NORMAL|VISUAL, KEY_ANY, KEY_ANY, &key_search_prev},
       -        {"u",  0, 0, NORMAL|VISUAL, KEY_ANY, KEY_ANY, &undo},
       -        {"U",  0, 0, NORMAL|VISUAL, KEY_ANY, KEY_ANY, &redo},
       +        {"?",  0, 0, NORMAL, KEY_ANY, KEY_ANY, &enter_searchedit_backward},
       +        {"/",  0, 0, NORMAL, KEY_ANY, KEY_ANY, &enter_searchedit_forward},
       +        {"n",  0, 0, NORMAL, KEY_ANY, KEY_ANY, &key_search_next},
       +        {"N",  0, 0, NORMAL, KEY_ANY, KEY_ANY, &key_search_prev},
       +        {"u",  0, 0, NORMAL, KEY_ANY, KEY_ANY, &undo},
       +        {"U",  0, 0, NORMAL, KEY_ANY, KEY_ANY, &redo},
                {".",  0, 0, NORMAL, KEY_ANY, KEY_ANY, &repeat_command}, /* FIXME: only allow after finished key sequence */
                {"z",  ControlMask, 0, INSERT, KEY_ANY, KEY_ANY, &undo},
                {"y",  ControlMask, 0, INSERT, KEY_ANY, KEY_ANY, &redo}, /* FIXME: this is confusing with ctrl+y in normal mode */
   DIR diff --git a/keys_command.c b/keys_command.c
       t@@ -27,6 +27,7 @@
        #include "util.h"
        
        #include "keys.h"
       +#include "keys_config.h"
        #include "keys_command.h"
        #include "keys_command_config.h"
        
       t@@ -57,9 +58,7 @@ history searchhistory = {0, 0, 0, NULL};
        static void
        push_history(history *hist, char *cmd, size_t len) {
                if (hist->len >= hist->cap) {
       -                size_t cap = hist->cap * 2 > hist->cap + 2 ? hist->cap * 2 : hist->cap + 2;
       -                if (cap <= hist->len)
       -                        exit(1); /* FIXME: overflow */
       +                size_t cap = ideal_array_size(hist->cap, add_sz(hist->cap, 1));
                        hist->cmds = ledit_reallocarray(hist->cmds, cap, sizeof(char *));
                        hist->cap = cap;
                }
       t@@ -100,15 +99,17 @@ view_locked_error(ledit_view *view) {
        
        #define CHECK_VIEW_LOCKED(view) if (view->lock_text) return view_locked_error(view)
        
       -/* FIXME: history for search and commands */
       -
        static int create_view(ledit_view *view, char *cmd, size_t l1, size_t l2);
        static int close_view(ledit_view *view, char *cmd, size_t l1, size_t l2);
        static int handle_write(ledit_view *view, char *cmd, size_t l1, size_t l2);
        static int handle_quit(ledit_view *view, char *cmd, size_t l1, size_t l2);
        static int handle_write_quit(ledit_view *view, char *cmd, size_t l1, size_t l2);
        static int handle_substitute(ledit_view *view, char *cmd, size_t l1, size_t l2);
       -static int parse_range(ledit_view *view, char *cmd, size_t len, char **cmd_ret, size_t *line1_ret, size_t *line2_ret, int *l1_valid, int *l2_valid);
       +static int parse_range(
       +    ledit_view *view, char *cmd, size_t len, char **cmd_ret,
       +    size_t *line1_ret, size_t *line2_ret, int *l1_valid, int *l2_valid,
       +    char **errstr_ret
       +);
        static int handle_cmd(ledit_view *view, char *cmd, size_t len);
        
        /* FIXME: remove command name before passing to handlers */
       t@@ -217,12 +218,7 @@ handle_write_quit(ledit_view *view, char *cmd, size_t l1, size_t l2) {
        
        static void
        show_num_substituted(ledit_view *view) {
       -        char buf[30];
       -        if (snprintf(buf, sizeof(buf), "%d substitution(s)", sub_state.num) < 0) {
       -                window_show_message(view->window, "This message is a bug, tell lumidify about it", -1);
       -        } else {
       -                window_show_message(view->window, buf, -1);
       -        }
       +        window_show_message_fmt(view->window, "%d substitution(s)", sub_state.num);
        }
        
        /* returns 1 when match was found, 0 otherwise */
       t@@ -311,12 +307,11 @@ substitute_all_remaining(ledit_view *view) {
                }
                if (min_line < view->lines_num)
                        buffer_recalc_all_views_from_line(view->buffer, min_line);
       -        /* FIXME: show number replaced */
       +        window_show_message_fmt(view->window, "Replaced %d occurrences", sub_state.num);
                /* this doesn't need to be added to the undo stack since it's called on undo/redo anyways */
                view->cur_index = view_get_legal_normal_pos(view, view->cur_line, view->cur_index);
                view_set_line_cursor_attrs(view, view->cur_line, view->cur_index);
                view_ensure_cursor_shown(view);
       -        show_num_substituted(view);
                buffer_unlock_all_views(view->buffer);
        }
        
       t@@ -369,6 +364,11 @@ handle_substitute(ledit_view *view, char *cmd, size_t l1, size_t l2) {
                sub_state.num = 0;
                sub_state.start_group = 1;
        
       +        /* trying to perform substitution in visual mode would make
       +           it unnecessarily complicated */
       +        if (view->mode == VISUAL)
       +                view_wipe_selection(view);
       +        view_set_mode(view, NORMAL);
                if (confirm) {
                        buffer_lock_all_views_except(view->buffer, view, "Ongoing substitution in other view.");
                        view->cur_command_type = CMD_SUBSTITUTE;
       t@@ -412,8 +412,12 @@ $ last line
        /* NOTE: Marks are only recognized here if they are one unicode character! */
        /* NOTE: Only the line range of the selection is used at the moment. */
        static int
       -parse_range(ledit_view *view, char *cmd, size_t len, char **cmd_ret, size_t *line1_ret, size_t *line2_ret, int *l1_valid, int *l2_valid) {
       +parse_range(
       +    ledit_view *view, char *cmd, size_t len, char **cmd_ret,
       +    size_t *line1_ret, size_t *line2_ret, int *l1_valid, int *l2_valid,
       +    char **errstr_ret) {
                (void)len;
       +        *errstr_ret = "";
                enum {
                        START_LINENO = 1,
                        START_RANGE = 2,
       t@@ -426,14 +430,22 @@ parse_range(ledit_view *view, char *cmd, size_t len, char **cmd_ret, size_t *lin
                char *c = cmd;
                while (*c != '\0') {
                        if (isdigit(*c)) {
       -                        /* FIXME: integer overflow */
                                if (s & IN_LINENO) {
       -                                if (*l2_valid) {
       -                                        l2 = l2 * 10 + (*c - '0');
       -                                } else {
       -                                        l1 = l1 * 10 + (*c - '0');
       +                                size_t *final = &l2;
       +                                if (!*l2_valid) {
       +                                        final = &l1;
                                                *l1_valid = 1;
                                        }
       +                                if (SIZE_MAX / 10 < *final) {
       +                                        *errstr_ret = "Integer overflow in range";
       +                                        return 1;
       +                                }
       +                                *final *= 10;
       +                                if (SIZE_MAX - (*c - '0') < *final) {
       +                                        *errstr_ret = "Integer overflow in range";
       +                                        return 1;
       +                                }
       +                                *final += (*c - '0');
                                } else if ((s & START_LINENO) && (s & START_RANGE)) {
                                        l1 = *c - '0';
                                        *l1_valid = 1;
       t@@ -444,8 +456,10 @@ parse_range(ledit_view *view, char *cmd, size_t len, char **cmd_ret, size_t *lin
                                        s = IN_LINENO;
                                }
                        } else if (*c == '\'' && (s & START_LINENO)) {
       -                        if (c[1] == '\0' || c[2] == '\0')
       +                        if (c[1] == '\0' || c[2] == '\0') {
       +                                *errstr_ret = "Invalid range";
                                        return 1;
       +                        }
                                char *aftermark = next_utf8(c + 2);
                                size_t marklen = aftermark - (c + 1);
                                size_t l, b;
       t@@ -455,7 +469,7 @@ parse_range(ledit_view *view, char *cmd, size_t len, char **cmd_ret, size_t *lin
                                        l = view->sel.line1 > view->sel.line2 ? view->sel.line1 : view->sel.line2;
                                } else {
                                        if (buffer_get_mark(view->buffer, c + 1, marklen, &l, &b)) {
       -                                        /* FIXME: show better error message */
       +                                        *errstr_ret = "Invalid mark";
                                                return 1;
                                        }
                                }
       t@@ -471,6 +485,7 @@ parse_range(ledit_view *view, char *cmd, size_t len, char **cmd_ret, size_t *lin
                                continue;
                        } else if (*c == ',' && !(s & START_RANGE)) {
                                if (*l1_valid && *l2_valid) {
       +                                *errstr_ret = "Invalid range";
                                        return 1;
                                } else {
                                        s = START_LINENO;
       t@@ -481,11 +496,13 @@ parse_range(ledit_view *view, char *cmd, size_t len, char **cmd_ret, size_t *lin
                                        l2 = view->lines_num;
                                        *l1_valid = *l2_valid = 1;
                                        c++;
       +                                s = 0;
                                        break;
                                } else {
       +                                *errstr_ret = "Invalid range";
                                        return 1;
                                }
       -                } else if (*c == '$') {
       +                } else if (*c == '.') {
                                if (s & START_LINENO) {
                                        if (!*l1_valid) {
                                                l1 = view->cur_line + 1;
       t@@ -496,6 +513,7 @@ parse_range(ledit_view *view, char *cmd, size_t len, char **cmd_ret, size_t *lin
                                        }
                                        s = 0;
                                } else {
       +                                *errstr_ret = "Invalid range";
                                        return 1;
                                }
                        } else if (*c == '$') {
       t@@ -509,6 +527,7 @@ parse_range(ledit_view *view, char *cmd, size_t len, char **cmd_ret, size_t *lin
                                        }
                                        s = 0;
                                } else {
       +                                *errstr_ret = "Invalid range";
                                        return 1;
                                }
                        } else {
       t@@ -516,10 +535,14 @@ parse_range(ledit_view *view, char *cmd, size_t len, char **cmd_ret, size_t *lin
                        }
                        c++;
                }
       -        if ((!*l1_valid || !*l2_valid) && !(s & START_RANGE))
       +        if ((!*l1_valid || !*l2_valid) && !(s & START_RANGE)) {
       +                *errstr_ret = "Invalid range";
                        return 1;
       -        if ((*l1_valid || *l2_valid) && (l1 == 0 || l2 == 0 || l1 > view->lines_num || l2 > view->lines_num))
       -                return 1; /* FIXME: better error messages */
       +        }
       +        if ((*l1_valid || *l2_valid) && (l1 == 0 || l2 == 0 || l1 > view->lines_num || l2 > view->lines_num)) {
       +                *errstr_ret = "Invalid line number in range";
       +                return 1;
       +        }
                *cmd_ret = c;
                /* ranges are given 1-indexed by user */
                *line1_ret = l1 - 1;
       t@@ -535,8 +558,9 @@ handle_cmd(ledit_view *view, char *cmd, size_t len) {
                char *c;
                size_t l1, l2;
                int l1_valid, l2_valid;
       -        if (parse_range(view, cmd, len, &c, &l1, &l2, &l1_valid, &l2_valid)) {
       -                window_show_message(view->window, "Error parsing command", -1);
       +        char *errstr;
       +        if (parse_range(view, cmd, len, &c, &l1, &l2, &l1_valid, &l2_valid, &errstr)) {
       +                window_show_message(view->window, errstr, -1);
                        return 0;
                }
                int range_given = l1_valid && l2_valid;
       t@@ -743,7 +767,7 @@ edit_nextsearch(ledit_view *view, char *key_text, size_t len) {
        void
        search_next(ledit_view *view) {
                view_wipe_line_cursor_attrs(view, view->cur_line);
       -        enum ledit_search_state ret = ledit_search_next(view, &view->cur_line, &view->cur_index);
       +        search_state ret = ledit_search_next(view, &view->cur_line, &view->cur_index);
                view_set_line_cursor_attrs(view, view->cur_line, view->cur_index);
                view_ensure_cursor_shown(view);
                if (ret != SEARCH_NORMAL)
       t@@ -753,7 +777,7 @@ search_next(ledit_view *view) {
        void
        search_prev(ledit_view *view) {
                view_wipe_line_cursor_attrs(view, view->cur_line);
       -        enum ledit_search_state ret = ledit_search_prev(view, &view->cur_line, &view->cur_index);
       +        search_state ret = ledit_search_prev(view, &view->cur_line, &view->cur_index);
                view_set_line_cursor_attrs(view, view->cur_line, view->cur_index);
                view_ensure_cursor_shown(view);
                if (ret != SEARCH_NORMAL)
   DIR diff --git a/keys_command.h b/keys_command.h
       t@@ -1,6 +1,14 @@
       +#ifndef _KEYS_COMMAND_H_
       +#define _KEYS_COMMAND_H_
       +
       +#include <X11/Xlib.h>
       +#include "view.h"
       +
        /* these are only here so they can also be used by keys_basic */
        void search_next(ledit_view *view);
        void search_prev(ledit_view *view);
        
        void command_key_cleanup(void);
        struct action command_key_handler(ledit_view *view, XEvent *event, int lang_index);
       +
       +#endif
   DIR diff --git a/keys_command_config.h b/keys_command_config.h
       t@@ -38,9 +38,9 @@ static struct key keys_en[] = {
                {"Y", 0, 0, CMD_SUBSTITUTE, &substitute_yes_all},
                {"n", 0, 0, CMD_SUBSTITUTE, &substitute_no},
                {"N", 0, 0, CMD_SUBSTITUTE, &substitute_no_all},
       -        {NULL, 0, XK_Return, CMD_EDIT, &edit_submit},
       -        {NULL, 0, XK_Return, CMD_EDITSEARCH, &editsearch_submit},
       -        {NULL, 0, XK_Return, CMD_EDITSEARCHB, &editsearchb_submit},
       +        {NULL, XK_ANY_MOD, XK_Return, CMD_EDIT, &edit_submit},
       +        {NULL, XK_ANY_MOD, XK_Return, CMD_EDITSEARCH, &editsearch_submit},
       +        {NULL, XK_ANY_MOD, XK_Return, CMD_EDITSEARCHB, &editsearchb_submit},
                {NULL, 0, XK_Left, CMD_EDIT, &edit_cursor_left},
                {NULL, 0, XK_Left, CMD_EDITSEARCH, &edit_cursor_left},
                {NULL, 0, XK_Left, CMD_EDITSEARCHB, &edit_cursor_left},
   DIR diff --git a/keys_config.h b/keys_config.h
       t@@ -0,0 +1,31 @@
       +#ifndef _KEYS_CONFIG_H_
       +#define _KEYS_CONFIG_H_
       +
       +#include "keys.h"
       +
       +/*
       + * These are the language strings compared with the language strings that
       + * xkb gives in order to change the key mapping on layout change events.
       + */
       +#define KEY_LANGS             \
       +static char *key_langs[] = {  \
       +        "English (US)",       \
       +        "German",             \
       +        "Urdu (Pakistan)",    \
       +        "Hindi (Bolnagri)"    \
       +}
       +
       +#define GEN_KEY_ARRAY(key_struct, en, de, ur, hi) \
       +static struct {                                   \
       +        key_struct *keys;                         \
       +        int num_keys;                             \
       +} keys[] = {                                      \
       +        {en, LENGTH(en)},                         \
       +        {de, LENGTH(de)},                         \
       +        {ur, LENGTH(ur)},                         \
       +        {hi, LENGTH(hi)}                          \
       +}
       +
       +#define LANG_KEYS(index) &keys[index]
       +
       +#endif
   DIR diff --git a/ledit.1 b/ledit.1
       t@@ -0,0 +1,796 @@
       +.\" WARNING: Some parts of this are stolen shamelessly from OpenBSD's
       +.\" vi(1) manpage!
       +.Dd December 18, 2021
       +.Dt LEDIT 1
       +.Os
       +.Sh NAME
       +.Nm ledit
       +.Nd weird text editor
       +.Sh SYNOPSIS
       +.Nm
       +.Op Ar file
       +.Sh DESCRIPTION
       +.Nm
       +is a vi-like text editor for people who switch between keyboard layouts
       +frequently and/or work with languages that require complex text layout.
       +.Pp
       +It is assumed that readers of this manual page are already familiar
       +with
       +.Xr vi 1 .
       +Differences with
       +.Xr vi 1
       +are documented, but it is very likely that many have been missed.
       +If you find an important difference that is not documented, please
       +contact me.
       +.Sh ANTI-DESCRIPTION
       +.Bl -tag -width Ds
       +.It Nm
       +is not a code editor.
       +Features for code editing may be added in the future, but the main
       +purpose is currently to edit other text.
       +.It Nm
       +is not a general-purpose text editor.
       +It is content to be useful for some tasks and does not feel insulted when
       +other editors are used for other tasks.
       +.It Nm
       +is not a minimalistic text editor.
       +It probably counts as reasonably minimalistic in the modern world, but
       +that is not the main goal.
       +.It Nm
       +is not a good text editor.
       +.El
       +.Sh KEY BINDINGS
       +The key bindings listed here are given as the default English bindings.
       +These will, of course, not be accurate for other languages or if
       +the configuration is changed.
       +.Pp
       +Some commands change their behavior depending on whether the mode is set
       +to soft line or hard line.
       +This is sometimes a bit inconsistent, but at least it might help when
       +editing longer paragraphs with no manual line breaking.
       +.Pp
       +Note that this list is currently not sorted in any logical way.
       +That will hopefully be fixed in the future.
       +.Ss NORMAL MODE
       +.Bl -tag -width Ds -compact
       +.It Xo
       +.Op Ar count
       +.Aq Cm arrow down
       +.Xc
       +.It Xo
       +.Op Ar count
       +.Aq Cm control-j
       +.Xc
       +.It Xo
       +.Op Ar count
       +.Aq Cm control-n
       +.Xc
       +.It Xo
       +.Op Ar count
       +.Cm j
       +.Xc
       +Move the cursor down
       +.Ar count
       +lines.
       +This changes behavior depending on the hard/soft line mode.
       +.Pp
       +.It Xo
       +.Op Ar count
       +.Aq Cm arrow up
       +.Xc
       +.It Xo
       +.Op Ar count
       +.Aq Cm control-p
       +.Xc
       +.It Xo
       +.Op Ar count
       +.Cm k
       +.Xc
       +Move the cursor up
       +.Ar count
       +lines.
       +This changes behavior depending on the hard/soft line mode.
       +.Pp
       +.It Xo
       +.Op Ar count
       +.Aq Cm arrow right
       +.Xc
       +.It Xo
       +.Op Ar count
       +.Aq Cm space
       +.Xc
       +.It Xo
       +.Op Ar count
       +.Cm l
       +.Xc
       +Move the cursor right
       +.Ar count
       +characters in the current line.
       +Note that this is a visual operation, i.e. the cursor will still move right
       +if the text is right-to-left.
       +.Pp
       +.It Xo
       +.Op Ar count
       +.Aq Cm arrow left
       +.Xc
       +.It Xo
       +.Op Ar count
       +.Aq Cm control-h
       +.Xc
       +.It Xo
       +.Op Ar count
       +.Cm h
       +.Xc
       +Move the cursor left
       +.Ar count
       +characters in the current line.
       +Note that this is a visual operation, i.e. the cursor will still move left
       +if the text is right-to-left.
       +.Pp
       +.It Aq Cm escape
       +Clear the key stack (i.e. cancel multi-key command).
       +.Pp
       +.It Cm i
       +Enter insert mode.
       +.Pp
       +.It Cm v
       +Enter visual mode.
       +.Pp
       +.It Aq Cm control-t
       +Toggle mode between hard line and soft line based.
       +.Pp
       +.It Cm 0
       +Move cursor to beginning of line.
       +This changes behavior depending on the hard/soft line mode.
       +.Pp
       +.It Xo
       +.Op Ar count
       +.Cm x
       +.Xc
       +Delete
       +.Ar count
       +characters after the cursor on the current line and copy the
       +deleted text into the paste buffer.
       +.Pp
       +.It Xo
       +.Op Ar count
       +.Cm X
       +.Xc
       +Delete
       +.Ar count
       +characters before the cursor on the current line and copy the
       +deleted text into the paste buffer.
       +.Pp
       +.It Xo
       +.Op Ar count
       +.Cm d
       +.Ar motion
       +.Xc
       +Delete the region of text described by
       +.Ar count
       +and
       +.Ar motion
       +and copy the deleted text into the paste buffer.
       +If
       +.Ar motion
       +is
       +.Cm d
       +again,
       +.Ar count
       +lines are deleted, starting with the current line.
       +This changes behavior depending on the hard/soft line mode.
       +.Pp
       +.It Cm D
       +Delete all text from the current cursor position to the end of
       +the line and copy the deleted text into the paste buffer.
       +This changes behavior depending on the hard/soft line mode.
       +.Pp
       +.It Xo
       +.Op Ar count
       +.Cm y
       +.Ar motion
       +.Xc
       +Copy the region of text described by
       +.Ar count
       +and
       +.Ar motion
       +into the paste buffer.
       +If
       +.Ar motion
       +is
       +.Cm y
       +again,
       +.Ar count
       +lines are copied, starting with the current line.
       +This changes behavior depending on the hard/soft line mode.
       +.Pp
       +.It Xo
       +.Op Ar count
       +.Cm Y
       +.Xc
       +Copy
       +.Ar count
       +lines into the paste buffer.
       +This changes behavior depending on the hard/soft line mode.
       +.Pp
       +.It Xo
       +.Op Ar count
       +.Cm c
       +.Ar motion
       +.Xc
       +Change the region described by
       +.Ar count
       +and
       +.Ar motion
       +and copy the changed text into the paste buffer.
       +.Pp
       +.It Cm C
       +Change all text from the current cursor position to the end of
       +the line and copy the changed text into the paste buffer.
       +This changes behavior depending on the hard/soft line mode.
       +.Pp
       +.It Aq Cm control-g
       +Show the current filename, whether the buffer has been modified since the
       +last write, and the current line.
       +.Pp
       +.It Cm \&:
       +Enter the line-editing mode for running a command.
       +.Pp
       +.It Cm /
       +Search forward for a search term.
       +Note that no regex is currently supported.
       +.Pp
       +.It Cm \&?
       +Search backwards for a search term.
       +Note that no regex is currently supported.
       +.Pp
       +.It Cm n
       +Move to the next match of the last search term in the direction of the
       +last search.
       +.Pp
       +.It Cm N
       +Move to the next match of the last search term in the direction opposite
       +to the direction of the last search.
       +.Pp
       +.It Xo
       +.Op Ar count
       +.Cm u
       +.Xc
       +Undo
       +.Ar count
       +operations.
       +Note that an entire session in insert mode is considered as one operation
       +when in normal mode.
       +.Pp
       +.It Xo
       +.Op Ar count
       +.Cm U
       +.Xc
       +Redo
       +.Ar count
       +operations.
       +Note that an entire session in insert mode is considered as one operation
       +when in normal mode.
       +.Pp
       +.It Xo
       +.Op Ar count
       +.Cm \&.
       +.Xc
       +Repeat the last command
       +.Ar count
       +times.
       +.Pp
       +.It Xo
       +.Op Ar count
       +.Aq Cm control-b
       +.Xc
       +Move
       +.Ar count
       +screens up.
       +.Pp
       +.It Xo
       +.Op Ar count
       +.Aq Cm control-f
       +.Xc
       +Move
       +.Ar count
       +screens down.
       +.Pp
       +.It Xo
       +.Op Ar count
       +.Aq Cm control-y
       +.Xc
       +Move
       +.Ar count
       +lines up, attemting to leave the cursor in its current line and
       +character position.
       +Note that this command works with soft lines, regardless of the
       +current mode.
       +.Pp
       +.It Xo
       +.Op Ar count
       +.Aq Cm control-e
       +.Xc
       +Move
       +.Ar count
       +lines down, attemting to leave the cursor in its current line and
       +character position.
       +Note that this command works with soft lines, regardless of the
       +current mode.
       +.Pp
       +.It Xo
       +.Op Ar count
       +.Aq Cm control-u
       +.Xc
       +Move
       +.Ar count
       +lines up.
       +If
       +.Ar count
       +is not given, scroll up the number of lines specified by the last
       +.Aq Cm control-d
       +or
       +.Aq Cm control-u
       +command.
       +If this is the first such command, scroll up half a screen.
       +Note that this command works with soft lines, regardless of the
       +current mode.
       +.Pp
       +.It Xo
       +.Op Ar count
       +.Aq Cm control-d
       +.Xc
       +Move
       +.Ar count
       +lines down.
       +If
       +.Ar count
       +is not given, scroll down the number of lines specified by the last
       +.Aq Cm control-d
       +or
       +.Aq Cm control-u
       +command.
       +If this is the first such command, scroll down half a screen.
       +Note that this command works with soft lines, regardless of the
       +current mode.
       +.Pp
       +.It Cm $
       +Move to the last cursor position on the current line.
       +This changes behavior depending on the hard/soft line mode.
       +.Pp
       +.It Xo
       +.Op Ar count
       +.Cm w
       +.Xc
       +Move forward
       +.Ar count
       +words.
       +.Pp
       +.It Xo
       +.Op Ar count
       +.Cm W
       +.Xc
       +Move forward
       +.Ar count
       +bigwords.
       +.Pp
       +.It Xo
       +.Op Ar count
       +.Cm e
       +.Xc
       +Move forward
       +.Ar count
       +end-of-words.
       +.Pp
       +.It Xo
       +.Op Ar count
       +.Cm E
       +.Xc
       +Move forward
       +.Ar count
       +end-of-bigwords.
       +.Pp
       +.It Xo
       +.Op Ar count
       +.Cm b
       +.Xc
       +Move backwards
       +.Ar count
       +words.
       +.Pp
       +.It Xo
       +.Op Ar count
       +.Cm B
       +.Xc
       +Move backwards
       +.Ar count
       +bigwords.
       +.Pp
       +.It Xo
       +.Op Ar count
       +.Cm G
       +.Xc
       +Move to the line number given by
       +.Ar count .
       +If
       +.Ar count
       +is not given, move to the last line in the buffer.
       +.Pp
       +.It Xo
       +.Op Ar count
       +.Cm J
       +.Xc
       +Join the current line with the next one
       +.Ar count
       +times.
       +Note that this command always works on hard lines, regardless
       +of the current mode.
       +Also note that this currently does not compress whitespace between
       +the lines as other vi-like editors do.
       +This is due to the author's laziness.
       +.Pp
       +.It Cm I
       +Move cursor to beginning of line and enter insert mode.
       +This changes behavior depending on the hard/soft line mode.
       +.Pp
       +.It Cm p
       +Paste text from the paste buffer after the current cursor position if the
       +buffer is character-based and after the current line if it is line-based.
       +Note that this does take into account the hard line/soft line mode, but
       +it behaves a bit weirdly when in soft line mode - it inserts the text
       +after the current soft line but adds newlines on both sides.
       +This behavior may be changed in the future if it turns out there's a more
       +logical behavior for soft line mode.
       +.Pp
       +.It Cm P
       +Paste text from the paste buffer before the current cursor position if the
       +buffer is character-based and before the current line if it is line-based.
       +The quirk for
       +.Cm p
       +applies here as well.
       +.Pp
       +.It Cm A
       +Append text after the current line.
       +This changes behavior depending on the hard/soft line mode.
       +.Pp
       +.It Cm a
       +Append text after the current cursor position.
       +.Pp
       +.It Cm o
       +Append a new line after the current line and enter insert mode there.
       +This changes behavior depending on the hard/soft line mode.
       +.Pp
       +.It Cm O
       +Append a new line before the current line and enter insert mode there.
       +This changes behavior depending on the hard/soft line mode.
       +.Pp
       +.It Xo
       +.Cm m
       +.Aq Cm character
       +.Xc
       +Mark the current current cursor position as
       +.Aq Cm character .
       +.Pp
       +.It Xo
       +.Cm '
       +.Aq Cm character
       +.Xc
       +Jump to a position previously marked as
       +.Aq Cm character
       +with
       +.Cm m .
       +.Pp
       +.It Xo
       +.Cm r
       +.Aq Cm character
       +.Xc
       +Replace the character at the current cursor position with
       +.Aq Cm character .
       +.Pp
       +.It Cm ^
       +Move to the first non-whitespace character on the current line.
       +This changes behavior depending on the hard/soft line mode.
       +.Pp
       +.It Xo
       +.Op Ar count
       +.Cm t
       +.Aq Cm character
       +.Xc
       +Search forward,
       +.Op Ar count
       +times, through the current line for the cursor position immediately before
       +.Aq Cm character .
       +.Pp
       +.It Xo
       +.Op Ar count
       +.Cm T
       +.Aq Cm character
       +.Xc
       +Search backwards,
       +.Op Ar count
       +times, through the current line for the cursor position after
       +.Aq Cm character .
       +.Pp
       +.It Xo
       +.Op Ar count
       +.Cm f
       +.Aq Cm character
       +.Xc
       +Search forward,
       +.Op Ar count
       +times, through the current line for
       +.Aq Cm character .
       +.Pp
       +.It Xo
       +.Op Ar count
       +.Cm F
       +.Aq Cm character
       +.Xc
       +Search backwards,
       +.Op Ar count
       +times, through the current line for
       +.Aq Cm character .
       +.El
       +.Ss VISUAL MODE
       +The movement keys generally work the same in visual mode but change the
       +selection instead of just moving to the new position, so they are not
       +listed here separately.
       +.Pp
       +The
       +.Cm d ,
       +.Cm y ,
       +and
       +.Cm c
       +keys also work similarly, but operate on the range given by the selection.
       +.Pp
       +The
       +.Cm \&:
       +key automatically pastes the selection range into the line editor so a
       +command can be run over the range specified by the selection.
       +.Pp
       +Additionally, these keys are supported:
       +.Pp
       +.Bl -tag -width Ds -compact
       +.It Cm o
       +Switch the end of the selection that is modified by the movement keys.
       +.Pp
       +.It Aq Cm control-c
       +Copy the current selection to the clipboard.
       +.Pp
       +.It Aq Cm escape
       +Return to normal mode.
       +.El
       +.Ss INSERT MODE
       +All regular keys simply insert the corresponding text at the current cursor
       +position.
       +.Pp
       +The cursor keys, backspace, delete, and return all work as expected.
       +.Pp
       +Additionally, the following keys are supported:
       +.Pp
       +.Bl -tag -width Ds -compact
       +.It Aq Cm control-v
       +Paste text from the clipboard at the current cursor position.
       +.Pp
       +.It Aq Cm control-z
       +Undo one operation.
       +Note that this, in contrast to the undo in normal mode, does not consider
       +an entire insert session to be one operation.
       +.Pp
       +.It Aq Cm control-y
       +Redo one operation.
       +Note that this, in contrast to the redo in normal mode, does not consider
       +an entire insert session to be one operation.
       +.Pp
       +.It Aq Cm escape
       +Return to normal mode.
       +.El
       +Note that many keys that are common in other editors are not recognized currently.
       +That will hopefully be fixed in the future.
       +.Ss LINE EDITING MODE
       +These key bindings work in the line editor that is used for searching or
       +running commands.
       +.Pp
       +.Bl -tag -width DS -compact
       +.It Aq Cm return
       +Submit the search or command.
       +.Pp
       +.It Aq Cm arrow left
       +Move the cursor one to the left.
       +.Pp
       +.It Aq Cm arrow right
       +Move the cursor one to the right.
       +.Pp
       +.It Aq Cm arrow up
       +.It Aq Cm arrow down
       +Move through the search or command history.
       +Note that the search and command histories are separate.
       +.Pp
       +.It Aq Cm backspace
       +Delete one unicode character before the cursor.
       +.Pp
       +.It Aq Cm delete
       +Delete one unicode character after the cursor.
       +.Pp
       +.It Aq Cm end
       +Move the cursor to the end of the line.
       +.Pp
       +.It Aq Cm home
       +Move the cursor to the beginning of the line.
       +.Pp
       +.It Aq Cm escape
       +Cancel the search or command.
       +.El
       +.Ss MISCELLANEOUS
       +.Bl -tag -width DS
       +.It Keys while performing substitution with confirmation:
       +.Bl -tag -width Ds
       +.It Cm y
       +Confirm the current substitution.
       +.It Cm n
       +Reject the current substitution.
       +.It Cm Y
       +Confirm the current substitution and all further ones.
       +.It Cm N
       +Reject the current substitution and all further ones.
       +.El
       +.Pp
       +Note that these keys are also displayed during the substitution, but only
       +the default English bindings are shown because implementing anything else
       +would require work.
       +.El
       +.Sh COMMANDS
       +Note: The terminology is currently a bit inconsistent.
       +Sometimes,
       +.Dq commands
       +refers to the key commands, sometimes to the commands
       +written in the line editor, which are documented in this section.
       +.Pp
       +Note that the commands which take filenames currently use the entire rest of
       +the line as the filename instead of doing any string parsing.
       +This may be changed in the future.
       +.Pp
       +.Bl -tag -width Ds -compact
       +.It Xo
       +.Cm :w
       +.Op Ar filename
       +.Xc
       +Write the buffer to
       +.Op Ar filename ,
       +or, if no filename is given, to the file the buffer was read from.
       +.Pp
       +.It Xo
       +.Cm :w\&!
       +.Op Ar filename
       +.Xc
       +Same as
       +.Cm :w ,
       +but the file will be attempted to be written to even if there
       +is something blocking it (e.g. the modified date of the file is newer
       +than it was when it was opened).
       +.Pp
       +.It Cm :q
       +Quit.
       +.Pp
       +.It Cm :q\&!
       +Quit, even if there are unsaved changes.
       +.Pp
       +.It Xo
       +.Cm :wq
       +.Op Ar filename
       +.Xc
       +.It Xo
       +.Cm :wq\&!
       +.Op Ar filename
       +.Xc
       +Write and quit afterwards.
       +The
       +.Cm \&!
       +is interpreted as for normal writing.
       +.Pp
       +.It Xo
       +.Sm off
       +.Op Ar range
       +.Cm s / Ar pattern Cm / Ar replace Cm /
       +.Op Ar options
       +.Sm on
       +.Xc
       +Substitute
       +.Ar pattern
       +with
       +.Ar replace
       +in the given line range.
       +If no range is given, substitution is only performed on the current line.
       +Note that no regex is currently supported.
       +.Pp
       +The range consists of two line numbers separated by a comma or the special value
       +.Cm % ,
       +which refers to the entire file.
       +The following special values are possible instead of writing a line number directly:
       +.Bl -tag -width Ds
       +.It Cm $
       +The last line in the file.
       +.It Xo
       +.Sm off
       +.Cm ' Aq Cm mark
       +.Sm on
       +.Xc
       +The line of the previously set mark
       +.Aq Cm mark .
       +Note that even though marks can theoretically be any string of characters,
       +they are only allowed to be one unicode character if they are used in a range.
       +The special values
       +.Cm <
       +and
       +.Cm >
       +are possible, which refer to the first and last line, respectively, in the
       +current selection.
       +.It Cm \&.
       +The current line.
       +.El
       +.Pp
       +The
       +.Ar options
       +may be a combination of the following:
       +.Bl -tag -width Ds
       +.It Cm g
       +Perform substitution for all occurrences in the given lines instead of just
       +the first one on each line.
       +.It Cm c
       +Confirm each substitution before performing it.
       +.El
       +.Pp
       +.It Cm :v
       +Open a new view.
       +Each view is a window that shows the text in the current buffer,
       +which is synced between the views.
       +.El
       +.Sh MOUSE ACTIONS
       +There currently are not many mouse actions.
       +Clicking and dragging with the left mouse button enters visual mode and
       +selects text, which is always copied into the X11 primary selection.
       +.Pp
       +Note that text selection currently does not work in the line editor
       +because the author is too lazy to implement that.
       +.Sh CONFIGURATION
       +(Todo) - also document weirdness with xkb language strings
       +.Sh MISCELLANEOUS
       +(Todo) - document emergency dumps
       +.Sh EXIT STATUS
       +.Ex -std
       +.Sh QUIRKS
       +The cursor movement commands try to move left/right visually instead of moving
       +through the text logically.
       +This causes weird cursor jumps when working with bidirectional text in normal mode.
       +This may be fixed in the future, but it currently is not clear how to make the
       +behavior more logical.
       +.Pp
       +Since a new mode group is started each time insert is entered, when text
       +is typed in one view in insert, then in another view, and then again in
       +the first one, the last two inserts will be undone in one go since both
       +views were in insert already.
       +I'm not sure how to make this more logical, though.
       +Maybe it could be
       +.Dq improved
       +by also saving the view in the undo stack,
       +but that would cause problems because views can be added and removed,
       +and it would maybe not even be more logical.
       +.Pp
       +Scroll offset is stored as a pixel value, so a view may scroll when text is
       +added or deleted in another view.
       +Additionally, when a new view is created, the scroll offset from the old view
       +is taken, which may be weird if the window of the new view is a different size.
       +.Pp
       +(Todo) - document weirdness with spaces at end of line in normal mode
       +.Sh SEE ALSO
       +.Xr ed 1 ,
       +.Xr vi 1 ,
       +.Xr vim 1
       +.Sh AUTHORS
       +.An lumidify Aq Mt nobody@lumidify.org
       +.Sh BUGS
       +Too many to count.
       +See
       +.Sx TINY SUBSET OF BUGS .
       +.Sh TINY SUBSET OF BUGS
       +(Todo)
   DIR diff --git a/ledit.c b/ledit.c
       t@@ -1,4 +1,3 @@
       -/* FIXME: clean up asserts a bit; clean up includes */
        /* FIXME: On large files, expose event takes a long time for some reason
           -> but somehow only sometimes... */
        /* FIXME: generally optimize redrawing */
       t@@ -7,50 +6,38 @@
        /* FIXME: Document that everything is assumed to be utf8 */
        /* FIXME: Only redraw part of screen if needed */
        /* FIXME: overflow in repeated commands */
       -/* 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 */
        /* FIXME: horizontal scrolling (also need cache to avoid too large pixmaps) */
       -/* FIXME: sort out types for indices (currently just int, but that might overflow) */
        /* TODO: allow extending selection with shift+mouse like in e.g. gtk */
       -#include <math.h>
       +
        #include <time.h>
       -#include <stdio.h>
        #include <errno.h>
       -#include <string.h>
       +#include <stdio.h>
        #include <stdlib.h>
       -#include <limits.h>
       -#include <unistd.h>
       +#include <string.h>
        #include <locale.h>
       +#include <unistd.h>
       +#include <sys/stat.h>
        
        #include <X11/Xlib.h>
       -#include <X11/Xatom.h>
       -#include <X11/Xutil.h>
       -#include <X11/keysym.h>
       -#include <X11/XF86keysym.h>
       -#include <X11/cursorfont.h>
       -#include <pango/pangoxft.h>
        #include <X11/XKBlib.h>
       -#include <X11/extensions/XKBrules.h>
        #include <X11/extensions/Xdbe.h>
       +#include <X11/extensions/XKBrules.h>
        
       -#include "config.h"
       -#include "memory.h"
       -#include "common.h"
       -#include "txtbuf.h"
       +#include "view.h"
        #include "theme.h"
       -#include "window.h"
       -#include "cache.h"
       -#include "undo.h"
        #include "buffer.h"
       -#include "view.h"
       +#include "common.h"
       +#include "window.h"
        #include "search.h"
       +#include "macros.h"
       +#include "memory.h"
       +#include "config.h"
       +#include "cleanup.h"
        #include "keys.h"
        #include "keys_basic.h"
        #include "keys_command.h"
       -#include "macros.h"
       -#include "cleanup.h"
        
        static void mainloop(void);
        static void setup(int argc, char *argv[]);
       t@@ -326,6 +313,12 @@ ledit_emergencydump(void) {
                        /* FIXME: maybe just leave the file in case at
                           least part of it was written? */
                        unlink(template);
       +        } else {
       +                fprintf(
       +                    stderr,
       +                    "Wrote emergency dump to %s\n",
       +                    template
       +                );
                }
                free(template);
        }
   DIR diff --git a/macros.h b/macros.h
       t@@ -1,3 +1,6 @@
       +#ifndef _MACROS_H_
       +#define _MACROS_H_
       +
        /* stolen from OpenBSD */
        #define        ledit_timespecsub(tsp, usp, vsp)                                \
                do {                                                            \
       t@@ -8,3 +11,5 @@
                                (vsp)->tv_nsec += 1000000000L;                  \
                        }                                                       \
                } while (0)
       +
       +#endif
   DIR diff --git a/memory.c b/memory.c
       t@@ -3,8 +3,9 @@
        #include <stdlib.h>
        #include <string.h>
        
       -#include "cleanup.h"
        #include "assert.h"
       +#include "memory.h"
       +#include "cleanup.h"
        
        static void
        fatal_err(const char *msg) {
       t@@ -15,14 +16,18 @@ fatal_err(const char *msg) {
        
        void
        err_overflow(void) {
       -        fprintf(stderr, "Integer overflow.\n");
       -        ledit_cleanup();
       -        exit(1);
       +        (void)fprintf(stderr, "Integer overflow.\n");
       +        ledit_emergencydump();
       +        abort();
        }
        
       +/* FIXME: should these perform emergencydump instead of just
       +   fatal_err? It probably isn't of much use when there isn't
       +   even any memory left. */
        char *
        ledit_strdup(const char *s) {
                char *str = strdup(s);
       +        ledit_assert(str && "Out of memory.");
                if (!str)
                        fatal_err("Out of memory.\n");
                return str;
       t@@ -93,7 +98,7 @@ ledit_reallocarray(void *optr, size_t nmemb, size_t size)
        {
                if ((nmemb >= MUL_NO_OVERFLOW || size >= MUL_NO_OVERFLOW) &&
                    nmemb > 0 && SIZE_MAX / nmemb < size) {
       -                fatal_err("Integer overflow in reallocarray.\n");
       +                err_overflow();
                }
                return realloc(optr, size * nmemb);
        }
       t@@ -141,57 +146,76 @@ resize_and_move_gap(
            size_t *new_gap_ret, size_t *new_cap_ret) {
                ledit_assert(index <= len);
                ledit_assert(len <= old_cap);
       +        ledit_assert(old_gap <= len);
                size_t gap_size = old_cap - len;
       -        size_t new_cap = old_cap;
       -        /* FIXME: read up on what the best values are here */
       -        if (new_cap < min_size)
       -                new_cap = old_cap * 2 > min_size ? old_cap * 2 : min_size;
       -        if (new_cap < min_size)
       -                err_overflow();
       -        if (new_cap != old_cap)
       -                array = ledit_reallocarray(array, new_cap, elem_size);
       -        char *carray = (char*)array; /* cast to char to do pointer arithmetic */
       -        /* we already know new_cap * elem_size does not wrap around because array
       -           is of that size, so all the other multiplications here should be safe
       -           (at least that's what I think, but I may be wrong) */
       -        if (index > old_gap) {
       -                /* move piece between end of original gap and index to
       -                   beginning of original gap */
       -                memmove(
       -                    carray + old_gap * elem_size,
       -                    carray + (old_gap + gap_size) * elem_size,
       -                    (index - old_gap) * elem_size
       -                );
       -                /* move piece after index to end of buffer */
       -                memmove(
       -                    carray + (new_cap - (len - index)) * elem_size,
       -                    carray + (index + gap_size) * elem_size,
       -                    (len - index) * elem_size
       -                );
       -        } else if (index < old_gap) {
       -                /* move piece after original gap to end of buffer */
       -                memmove(
       -                    carray + (new_cap - (len - old_gap)) * elem_size,
       -                    carray + (old_gap + gap_size) * elem_size,
       -                    (len - old_gap) * elem_size
       -                );
       -                /* move piece between index and original gap to end */
       -                memmove(
       -                    carray + (new_cap - len + index) * elem_size,
       -                    carray + index * elem_size,
       -                    (old_gap - index) * elem_size
       -                );
       +        size_t new_cap = ideal_array_size(old_cap, min_size);;
       +        if (new_cap >= old_cap) {
       +                if (new_cap > old_cap)
       +                        array = ledit_reallocarray(array, new_cap, elem_size);
       +                char *carray = (char*)array; /* cast to char to do pointer arithmetic */
       +                /* we already know new_cap * elem_size does not wrap around because array
       +                   is of that size, so all the other multiplications here should be safe
       +                   (at least that's what I think, but I may be wrong) */
       +                if (index > old_gap) {
       +                        /* move piece between end of original gap and index to
       +                           beginning of original gap */
       +                        memmove(
       +                            carray + old_gap * elem_size,
       +                            carray + (old_gap + gap_size) * elem_size,
       +                            (index - old_gap) * elem_size
       +                        );
       +                        /* move piece after index to end of buffer */
       +                        memmove(
       +                            carray + (new_cap - (len - index)) * elem_size,
       +                            carray + (index + gap_size) * elem_size,
       +                            (len - index) * elem_size
       +                        );
       +                } else if (index < old_gap) {
       +                        /* move piece after original gap to end of buffer */
       +                        memmove(
       +                            carray + (new_cap - (len - old_gap)) * elem_size,
       +                            carray + (old_gap + gap_size) * elem_size,
       +                            (len - old_gap) * elem_size
       +                        );
       +                        /* move piece between index and original gap to end */
       +                        memmove(
       +                            carray + (new_cap - len + index) * elem_size,
       +                            carray + index * elem_size,
       +                            (old_gap - index) * elem_size
       +                        );
       +                } else {
       +                        /* move piece after original gap to end of buffer */
       +                        memmove(
       +                            carray + (new_cap - (len - old_gap)) * elem_size,
       +                            carray + (old_gap + gap_size) * elem_size,
       +                            (len - old_gap) * elem_size
       +                        );
       +                }
                } else {
       -                /* move piece after original gap to end of buffer */
       -                memmove(
       -                    carray + (new_cap - (len - old_gap)) * elem_size,
       -                    carray + (old_gap + gap_size) * elem_size,
       -                    (len - old_gap) * elem_size
       -                );
       +                /* otherwise, parts may be cut off */
       +                ledit_assert(min_size >= len);
       +                /* FIXME: optimize this */
       +                move_gap(array, elem_size, len, old_gap, old_cap, len, NULL);
       +                array = ledit_reallocarray(array, new_cap, elem_size);
       +                move_gap(array, elem_size, index, len, new_cap, len, NULL);
                }
                if (new_gap_ret)
                        *new_gap_ret = index;
                if (new_cap_ret)
                        *new_cap_ret = new_cap;
       -        return carray;
       +        return array;
       +}
       +
       +/* FIXME: maybe don't double when already very large? */
       +/* FIXME: better start size when old == 0? */
       +size_t
       +ideal_array_size(size_t old, size_t needed) {
       +        size_t ret = old;
       +        if (old < needed)
       +                ret = old * 2 > needed ? old * 2 : needed;
       +        else if (needed * 4 < old)
       +                ret = old / 2;
       +        if (ret == 0)
       +                ret = 1; /* not sure if this is necessary */
       +        return ret;
        }
   DIR diff --git a/memory.h b/memory.h
       t@@ -1,3 +1,9 @@
       +#ifndef _MEMORY_H_
       +#define _MEMORY_H_
       +
       +#include <stddef.h>
       +#include <stdint.h>
       +
        /*
         * These functions all wrap the regular functions but exit on error.
         */
       t@@ -28,6 +34,7 @@ void move_gap(
        /*
         * Resize a generic gap buffer with elements of size 'elem_size' to hold at least
         * 'min_size' elements and move the gap to element position 'index'.
       + * The array size may be increased or decreased.
         * 'old_gap' is the old index of the gap buffer, 'old_cap' is the total length of the
         * array (number of elements, not bytes), and 'len' is the number of valid elements.
         * 'index' is also written to 'new_gap_ret' if it is not NULL. This is just
       t@@ -45,3 +52,12 @@ void *resize_and_move_gap(
        
        /* FIXME: not sure if this really belongs here */
        void err_overflow(void);
       +
       +/*
       + * Return the ideal new size for an array of size 'old' when resizing it
       + * so it fits at least 'needed' elements. The return value may be smaller
       + * than 'old' if 'needed' is smaller.
       + */
       +size_t ideal_array_size(size_t old, size_t needed);
       +
       +#endif
   DIR diff --git a/pango-compat.h b/pango-compat.h
       t@@ -1,4 +1,11 @@
       +#ifndef _PANGO_COMPAT_H_
       +#define _PANGO_COMPAT_H_
       +
       +#include <pango/pangoxft.h>
       +
        //#if !PANGO_VERSION_CHECK(1, 46, 0)
        #if 1
        PangoDirection ledit_pango_layout_get_direction(PangoLayout *layout, int index);
        #endif
       +
       +#endif
   DIR diff --git a/search.c b/search.c
       t@@ -1,22 +1,9 @@
       -/* FIXME: split buffer into pure text part and graphical part so this
       - * doesn't depend on all the graphics stuff */
       -
        #include <string.h>
        
       -#include <X11/Xlib.h>
       -#include <X11/Xutil.h>
       -#include <pango/pangoxft.h>
       -#include <X11/extensions/Xdbe.h>
       -
       -#include "memory.h"
       -#include "common.h"
       -#include "txtbuf.h"
       -#include "undo.h"
       -#include "cache.h"
       -#include "theme.h"
       -#include "window.h"
       +#include "view.h"
        #include "buffer.h"
        #include "search.h"
       +#include "memory.h"
        
        /* FIXME: make sure only whole utf8 chars are matched */
        static char *last_search = NULL;
       t@@ -44,7 +31,7 @@ set_search_backward(char *pattern) {
                last_search = ledit_strdup(pattern);
        }
        
       -static enum ledit_search_state
       +static search_state
        search_forward(ledit_view *view, size_t *line_ret, size_t *byte_ret) {
                *line_ret = view->cur_line;
                *byte_ret = view->cur_index;
       t@@ -93,7 +80,7 @@ search_forward(ledit_view *view, size_t *line_ret, size_t *byte_ret) {
        
        /* FIXME: this is insanely inefficient */
        /* FIXME: just go backwards char-by-char and compare */
       -static enum ledit_search_state
       +static search_state
        search_backward(ledit_view *view, size_t *line_ret, size_t *byte_ret) {
                *line_ret = view->cur_line;
                *byte_ret = view->cur_index;
       t@@ -157,7 +144,7 @@ search_backward(ledit_view *view, size_t *line_ret, size_t *byte_ret) {
                return SEARCH_NOT_FOUND;
        }
        
       -enum ledit_search_state
       +search_state
        ledit_search_next(ledit_view *view, size_t *line_ret, size_t *byte_ret) {
                if (last_dir == FORWARD)
                        return search_forward(view, line_ret, byte_ret);
       t@@ -165,7 +152,7 @@ ledit_search_next(ledit_view *view, size_t *line_ret, size_t *byte_ret) {
                        return search_backward(view, line_ret, byte_ret);
        }
        
       -enum ledit_search_state
       +search_state
        ledit_search_prev(ledit_view *view, size_t *line_ret, size_t *byte_ret) {
                if (last_dir == FORWARD)
                        return search_backward(view, line_ret, byte_ret);
       t@@ -174,16 +161,18 @@ ledit_search_prev(ledit_view *view, size_t *line_ret, size_t *byte_ret) {
        }
        
        char *
       -search_state_to_str(enum ledit_search_state state) {
       -        switch (state) {
       -                case SEARCH_WRAPPED:
       -                        return "Search wrapped";
       -                case SEARCH_NOT_FOUND:
       -                        return "Pattern not found";
       -                case SEARCH_NO_PATTERN:
       -                        return "No previous search pattern";
       -                default:
       -                        return "This message should not be shown. "
       -                               "Please bug lumidify about it.";
       +search_state_to_str(search_state s) {
       +        switch (s) {
       +        case SEARCH_NORMAL:
       +                return "Found match";
       +        case SEARCH_WRAPPED:
       +                return "Search wrapped";
       +        case SEARCH_NOT_FOUND:
       +                return "Pattern not found";
       +        case SEARCH_NO_PATTERN:
       +                return "No previous search pattern";
       +        default:
       +                return "This message should not be shown. "
       +                       "Please bug lumidify about it.";
                }
        }
   DIR diff --git a/search.h b/search.h
       t@@ -1,13 +1,25 @@
       -enum ledit_search_state {
       +#ifndef _SEARCH_H_
       +#define _SEARCH_H_
       +
       +#include <stddef.h>
       +
       +typedef enum {
                SEARCH_NORMAL,
                SEARCH_WRAPPED,
                SEARCH_NOT_FOUND,
                SEARCH_NO_PATTERN
       -};
       +} search_state;
        
        void search_cleanup(void);
        void set_search_forward(char *pattern);
        void set_search_backward(char *pattern);
       -enum ledit_search_state ledit_search_next(ledit_view *view, size_t *line_ret, size_t *byte_ret);
       -enum ledit_search_state ledit_search_prev(ledit_view *view, size_t *line_ret, size_t *byte_ret);
       -char *search_state_to_str(enum ledit_search_state state);
       +search_state ledit_search_next(ledit_view *view, size_t *line_ret, size_t *byte_ret);
       +search_state ledit_search_prev(ledit_view *view, size_t *line_ret, size_t *byte_ret);
       +
       +/*
       + * Get a string corresponding to a search_state.
       + * This string should not be freed.
       + */
       +char *search_state_to_str(search_state s);
       +
       +#endif
   DIR diff --git a/theme.h b/theme.h
       t@@ -1,3 +1,9 @@
       +#ifndef _THEME_H_
       +#define _THEME_H_
       +
       +#include <X11/Xft/Xft.h>
       +#include "common.h"
       +
        typedef struct {
                int scrollbar_width;
                int scrollbar_step;
       t@@ -14,3 +20,5 @@ typedef struct {
        
        ledit_theme *theme_create(ledit_common *common);
        void theme_destroy(ledit_common *common, ledit_theme *theme);
       +
       +#endif
   DIR diff --git a/txtbuf.c b/txtbuf.c
       t@@ -1,6 +1,7 @@
        #include <stdlib.h>
        #include <string.h>
        
       +#include "util.h"
        #include "memory.h"
        #include "txtbuf.h"
        
       t@@ -13,20 +14,13 @@ txtbuf_new(void) {
        }
        
        void
       -txtbuf_grow(txtbuf *buf, size_t sz) {
       +txtbuf_resize(txtbuf *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
       -txtbuf_shrink(txtbuf *buf) {
       -        if ((buf->len + 1) * 4 < buf->cap) {
       -                buf->cap /= 2;
       -                buf->text = ledit_realloc(buf->text, buf->cap);
       +        /* FIXME: '\0' isn't actually used anywhere */
       +        size_t cap = ideal_array_size(buf->cap, add_sz(sz, 1));
       +        if (cap != buf->cap) {
       +                buf->text = ledit_realloc(buf->text, cap);
       +                buf->cap = cap;
                }
        }
        
       t@@ -40,7 +34,7 @@ txtbuf_destroy(txtbuf *buf) {
        
        void
        txtbuf_copy(txtbuf *dst, txtbuf *src) {
       -        txtbuf_grow(dst, src->len);
       +        txtbuf_resize(dst, src->len);
                memcpy(dst->text, src->text, src->len);
                dst->len = src->len;
        }
   DIR diff --git a/txtbuf.h b/txtbuf.h
       t@@ -1,3 +1,8 @@
       +#ifndef _TXTBUF_H_
       +#define _TXTBUF_H_
       +
       +#include <stddef.h>
       +
        /*
         * txtbuf is really just a string data type that is badly named.
         */
       t@@ -16,13 +21,7 @@ txtbuf *txtbuf_new(void);
         * Make sure the txtbuf has space for at least the given size,
         * plus '\0' at the end.
         */
       -void txtbuf_grow(txtbuf *buf, size_t sz);
       -
       -/*
       - * Shrink a textbuf, if the allocated space is much larger than the text.
       - */
       -/* FIXME: actually use this */
       -void txtbuf_shrink(txtbuf *buf);
       +void txtbuf_resize(txtbuf *buf, size_t sz);
        
        /*
         * Destroy a txtbuf.
       t@@ -38,3 +37,5 @@ void txtbuf_copy(txtbuf *dst, txtbuf *src);
         * Duplicate txtbuf 'src'.
         */
        txtbuf *txtbuf_dup(txtbuf *src);
       +
       +#endif
   DIR diff --git a/undo.c b/undo.c
       t@@ -6,6 +6,7 @@
        #include <X11/Xutil.h>
        #include <pango/pangoxft.h>
        
       +#include "util.h"
        #include "memory.h"
        #include "common.h"
        #include "txtbuf.h"
       t@@ -33,7 +34,7 @@ enum operation {
        typedef struct {
                txtbuf *text;
                enum operation type;
       -        enum ledit_mode mode;
       +        ledit_mode mode;
                ledit_range op_range;
                ledit_range cursor_range;
                int group;
       t@@ -41,9 +42,9 @@ typedef struct {
        } undo_elem;
        
        struct undo_stack {
       -        /* FIXME: size_t? */
       -        int len, cur, cap;
       +        size_t len, cur, cap;
                undo_elem *stack;
       +        int cur_valid;
                int change_mode_group;
        };
        
       t@@ -51,7 +52,8 @@ undo_stack *
        undo_stack_create(void) {
                undo_stack *undo = ledit_malloc(sizeof(undo_stack));
                undo->len = undo->cap = 0;
       -        undo->cur = -1;
       +        undo->cur = 0;
       +        undo->cur_valid = 0;
                undo->stack = NULL;
                undo->change_mode_group = 0;
                return undo;
       t@@ -59,6 +61,10 @@ undo_stack_create(void) {
        
        void
        undo_stack_destroy(undo_stack *undo) {
       +        for (size_t i = 0; i < undo->cap; i++) {
       +                if (undo->stack[i].text != NULL)
       +                        txtbuf_destroy(undo->stack[i].text);
       +        }
                free(undo->stack);
                free(undo);
        }
       t@@ -66,12 +72,14 @@ undo_stack_destroy(undo_stack *undo) {
        /* FIXME: resize text buffers when they aren't needed anymore */
        static undo_elem *
        push_undo_elem(undo_stack *undo) {
       -        ledit_assert(undo->cur >= -1);
       -        undo->cur++;
       -        undo->len = undo->cur + 1;
       +        if (undo->cur_valid)
       +                undo->cur++;
       +        else
       +                undo->cur = 0;
       +        undo->cur_valid = 1;
       +        undo->len = add_sz(undo->cur, 1);
                if (undo->len > undo->cap) {
       -                /* FIXME: wait, why is it size_t here already? */
       -                size_t cap = undo->len * 2;
       +                size_t cap = ideal_array_size(undo->cap, undo->len);
                        undo->stack = ledit_reallocarray(undo->stack, cap, sizeof(undo_elem));
                        for (size_t i = undo->cap; i < cap; i++) {
                                undo->stack[i].text = NULL;
       t@@ -83,7 +91,7 @@ push_undo_elem(undo_stack *undo) {
        
        static undo_elem *
        peek_undo_elem(undo_stack *undo) {
       -        if (undo->cur < 0)
       +        if (!undo->cur_valid)
                        return NULL;
                return &undo->stack[undo->cur];
        }
       t@@ -99,7 +107,7 @@ push_undo(
            ledit_range insert_range, /* maybe not the best name */
            ledit_range cursor_range,
            int start_group,
       -    enum operation type, enum ledit_mode mode) {
       +    enum operation type, ledit_mode mode) {
                undo_elem *old = peek_undo_elem(undo);
                int last_group = old == NULL ? 0 : old->group;
                int last_mode_group = old == NULL ? 0 : old->mode_group;
       t@@ -121,7 +129,7 @@ void
        undo_push_insert(
            undo_stack *undo, txtbuf *text,
            ledit_range insert_range, ledit_range cursor_range,
       -    int start_group, enum ledit_mode mode) {
       +    int start_group, ledit_mode mode) {
                push_undo(
                    undo, text, insert_range, cursor_range,
                    start_group, UNDO_INSERT, mode
       t@@ -132,7 +140,7 @@ void
        undo_push_delete(
            undo_stack *undo, txtbuf *text,
            ledit_range delete_range, ledit_range cursor_range,
       -    int start_group, enum ledit_mode mode) {
       +    int start_group, ledit_mode mode) {
                push_undo(
                    undo, text, delete_range, cursor_range,
                    start_group, UNDO_DELETE, mode
       t@@ -140,15 +148,18 @@ undo_push_delete(
        }
        
        undo_status
       -ledit_undo(undo_stack *undo, enum ledit_mode mode, void *callback_data,
       +ledit_undo(undo_stack *undo, ledit_mode mode, void *callback_data,
            undo_insert_callback insert_cb, undo_delete_callback delete_cb,
            size_t *cur_line_ret, size_t *cur_index_ret, size_t *min_line_ret) {
                undo_elem *e;
                /* skip empty elements */
       -        while (undo->cur >= 0 && undo->stack[undo->cur].text->len == 0) {
       -                undo->cur--;
       +        while (undo->cur_valid && undo->stack[undo->cur].text->len == 0) {
       +                if (undo->cur == 0)
       +                        undo->cur_valid = 0;
       +                else
       +                        undo->cur--;
                }
       -        if (undo->cur < 0)
       +        if (!undo->cur_valid)
                        return UNDO_OLDEST_CHANGE;
                int group = undo->stack[undo->cur].group;
                int mode_group = undo->stack[undo->cur].mode_group;
       t@@ -156,7 +167,7 @@ ledit_undo(undo_stack *undo, enum ledit_mode mode, void *callback_data,
                int mode_group_same = 0;
                size_t cur_line = 0;
                size_t cur_index = 0;
       -        while (undo->cur >= 0 &&
       +        while (undo->cur_valid &&
                       (undo->stack[undo->cur].group == group || (mode_group_same =
                        ((mode == NORMAL ||
                          mode == VISUAL) &&
       t@@ -193,7 +204,10 @@ ledit_undo(undo_stack *undo, enum ledit_mode mode, void *callback_data,
                        /* FIXME: make sure this is always sorted already */
                        if (e->op_range.line1 < min_line)
                                min_line = e->op_range.line1;
       -                undo->cur--;
       +                if (undo->cur == 0)
       +                        undo->cur_valid = 0;
       +                else
       +                        undo->cur--;
                        cur_line = e->cursor_range.line1;
                        cur_index = e->cursor_range.byte1;
                }
       t@@ -210,17 +224,24 @@ ledit_undo(undo_stack *undo, enum ledit_mode mode, void *callback_data,
        }
        
        undo_status
       -ledit_redo(undo_stack *undo, enum ledit_mode mode, void *callback_data,
       +ledit_redo(undo_stack *undo, ledit_mode mode, void *callback_data,
            undo_insert_callback insert_cb, undo_delete_callback delete_cb,
            size_t *cur_line_ret, size_t *cur_index_ret, size_t *min_line_ret) {
                undo_elem *e;
       +        if (undo->len == 0)
       +                return UNDO_NEWEST_CHANGE;
                /* skip elements where no text is changed */
                while (undo->cur < undo->len - 1 && undo->stack[undo->cur + 1].text->len == 0) {
                        undo->cur++;
                }
       -        if (undo->cur >= undo->len - 1)
       +        if (undo->cur_valid && undo->cur >= undo->len - 1)
                        return UNDO_NEWEST_CHANGE;
       -        undo->cur++;
       +        if (!undo->cur_valid) {
       +                undo->cur_valid = 1;
       +                undo->cur = 0;
       +        } else {
       +                undo->cur++;
       +        }
                int group = undo->stack[undo->cur].group;
                int mode_group = undo->stack[undo->cur].mode_group;
                size_t min_line = SIZE_MAX;
       t@@ -263,7 +284,9 @@ ledit_redo(undo_stack *undo, enum ledit_mode mode, void *callback_data,
                }
                *cur_line_ret = cur_line;
                *cur_index_ret = cur_index;
       -        undo->cur--;
       +        /* it should theoretically never be 0 anyways, but whatever */
       +        if (undo->cur > 0)
       +                undo->cur--;
                *min_line_ret = min_line;
                return UNDO_NORMAL;
        }
       t@@ -271,5 +294,20 @@ ledit_redo(undo_stack *undo, enum ledit_mode mode, void *callback_data,
        void
        undo_change_last_cur_range(undo_stack *undo, ledit_range cur_range) {
                undo_elem *e = peek_undo_elem(undo);
       -        e->cursor_range = cur_range;
       +        if (e != NULL)
       +                e->cursor_range = cur_range;
       +}
       +
       +char *
       +undo_state_to_str(undo_status s) {
       +        switch (s) {
       +        case UNDO_NORMAL:
       +                return "Performed undo/redo";
       +        case UNDO_OLDEST_CHANGE:
       +                return "Already at oldest change";
       +        case UNDO_NEWEST_CHANGE:
       +                return "Already at newest change";
       +        default:
       +                return "This is a bug. Tell lumidify about it.";
       +        }
        }
   DIR diff --git a/undo.h b/undo.h
       t@@ -1,3 +1,10 @@
       +#ifndef _UNDO_H_
       +#define _UNDO_H_
       +
       +#include <stddef.h>
       +#include "common.h"
       +#include "txtbuf.h"
       +
        /*
         * This handles undo and redo.
         *
       t@@ -70,7 +77,7 @@ void undo_change_mode_group(undo_stack *undo);
        void undo_push_insert(
            undo_stack *undo, txtbuf *text,
            ledit_range insert_range, ledit_range cursor_range,
       -    int start_group, enum ledit_mode mode
       +    int start_group, ledit_mode mode
        );
        
        /*
       t@@ -80,7 +87,7 @@ void undo_push_insert(
        void undo_push_delete(
            undo_stack *undo, txtbuf *text,
            ledit_range delete_range, ledit_range cursor_range,
       -    int start_group, enum ledit_mode mode
       +    int start_group, ledit_mode mode
        );
        
        /*
       t@@ -88,9 +95,10 @@ void undo_push_delete(
         * 'cur_line_ret' and 'cur_index_ret' are set to the new cursor position.
         * 'min_line_ret' is set to the minimum line that was touched so the lines
         * can be recalculated properly.
       + * WARNING: If nothing was changed, 'min_line_ret' will be SIZE_MAX.
         */
        undo_status ledit_undo(
       -    undo_stack *undo, enum ledit_mode mode,
       +    undo_stack *undo, ledit_mode mode,
            void *callback_data, undo_insert_callback insert_cb, undo_delete_callback delete_cb,
            size_t *cur_line_ret, size_t *cur_index_ret, size_t *min_line_ret
        );
       t@@ -100,9 +108,10 @@ undo_status ledit_undo(
         * 'cur_line_ret' and 'cur_index_ret' are set to the new cursor position.
         * 'min_line_ret' is set to the minimum line that was touched so the lines
         * can be recalculated properly.
       + * WARNING: If nothing was changed, 'min_line_ret' will be SIZE_MAX.
         */
        undo_status ledit_redo(
       -    undo_stack *undo, enum ledit_mode mode,
       +    undo_stack *undo, ledit_mode mode,
            void *callback_data, undo_insert_callback insert_cb, undo_delete_callback delete_cb,
            size_t *cur_line_ret, size_t *cur_index_ret, size_t *min_line_ret
        );
       t@@ -115,3 +124,11 @@ undo_status ledit_redo(
         * Fails silently if the stack is empty.
         */
        void undo_change_last_cur_range(undo_stack *undo, ledit_range cur_range);
       +
       +/*
       + * Get a string corresponding to an undo_status.
       + * This string should not be freed.
       + */
       +char *undo_state_to_str(undo_status s);
       +
       +#endif
   DIR diff --git a/util.c b/util.c
       t@@ -1,57 +1,40 @@
       -#include <stdlib.h>
       -
       -#include <X11/Xlib.h>
       -#include <X11/Xutil.h>
       -#include <pango/pangoxft.h>
       -#include <X11/extensions/Xdbe.h>
       -
       +#include <stddef.h>
        #include "memory.h"
       -#include "common.h"
       -#include "txtbuf.h"
       -#include "theme.h"
       -#include "window.h"
       -#include "util.h"
        
       -ledit_draw *
       -draw_create(ledit_window *window, int w, int h) {
       -        ledit_draw *draw = ledit_malloc(sizeof(ledit_draw));
       -        draw->w = w;
       -        draw->h = h;
       -        draw->pixmap = XCreatePixmap(
       -            window->common->dpy, window->drawable, w, h, window->common->depth
       -        );
       -        draw->xftdraw = XftDrawCreate(
       -            window->common->dpy, draw->pixmap, window->common->vis, window->common->cm
       -        );
       -        return draw;
       +char *
       +next_utf8(char *str) {
       +        while ((*str & 0xC0) == 0x80)
       +                str++;
       +        return str;
        }
        
       -void
       -draw_grow(ledit_window *window, ledit_draw *draw, int w, int h) {
       -        /* FIXME: sensible default pixmap sizes here */
       -        /* FIXME: maybe shrink the pixmaps at some point */
       -        if (draw->w < w || draw->h < h) {
       -                draw->w = w > draw->w ? w + 10 : draw->w;
       -                draw->h = h > draw->h ? h + 10 : draw->h;
       -                XFreePixmap(window->common->dpy, draw->pixmap);
       -                draw->pixmap = XCreatePixmap(
       -                    window->common->dpy, window->drawable,
       -                    draw->w, draw->h, window->common->depth
       -                );
       -                XftDrawChange(draw->xftdraw, draw->pixmap);
       -        }
       +size_t
       +add_sz(size_t a, size_t b) {
       +        if (a > SIZE_MAX - b)
       +                err_overflow();
       +        return a + b;
       +}
       +
       +size_t
       +add_sz3(size_t a, size_t b, size_t c) {
       +        if (b > SIZE_MAX - c || a > SIZE_MAX - (b + c))
       +                err_overflow();
       +        return a + b + c;
        }
        
        void
       -draw_destroy(ledit_window *window, ledit_draw *draw) {
       -        XFreePixmap(window->common->dpy, draw->pixmap);
       -        XftDrawDestroy(draw->xftdraw);
       -        free(draw);
       +swap_sz(size_t *a, size_t *b) {
       +        size_t tmp = *a;
       +        *a = *b;
       +        *b = tmp;
        }
        
       -char *
       -next_utf8(char *str) {
       -        while ((*str & 0xC0) == 0x80)
       -                str++;
       -        return str;
       +void
       +sort_range(size_t *l1, size_t *b1, size_t *l2, size_t *b2) {
       +        if (*l1 == *l2 && *b1 > *b2) {
       +                swap_sz(b1, b2);
       +        } else if (*l1 > *l2) {
       +                swap_sz(l1, l2);
       +                swap_sz(b1, b2);
       +        }
        }
   DIR diff --git a/util.h b/util.h
       t@@ -1,28 +1,22 @@
       -/* FIXME: rename this to draw_util.h and rename macros.h to util.h */
       +#ifndef _UTIL_H_
       +#define _UTIL_H_
        
        /*
       - * This is just a basic wrapper for XftDraws and Pixmaps
       - * that is used by the window for its text display at the bottom.
       + * Return the position of the next start of a utf8 character.
       + * If there is none, the position of the terminating NUL is
       + * returned.
         */
       -typedef struct {
       -        XftDraw *xftdraw;
       -        Pixmap pixmap;
       -        int w, h;
       -} ledit_draw;
       +char *next_utf8(char *str);
        
        /*
       - * Create a draw with the specified width and height.
       + * Add size_t values and abort if overflow would occur.
       + * FIXME: Maybe someone with actual experience could tell me
       + * if this overflow checking actually works.
         */
       -ledit_draw *draw_create(ledit_window *window, int w, int h);
       +size_t add_sz(size_t a, size_t b);
       +size_t add_sz3(size_t a, size_t b, size_t c);
        
       -/*
       - * Make sure the size of the draw is at least the given width and height.
       - */
       -void draw_grow(ledit_window *window, ledit_draw *draw, int w, int h);
       +void swap_sz(size_t *a, size_t *b);
       +void sort_range(size_t *l1, size_t *b1, size_t *l2, size_t *b2);
        
       -/*
       - * Destroy a draw.
       - */
       -void draw_destroy(ledit_window *window, ledit_draw *draw);
       -
       -char *next_utf8(char *str);
       +#endif
   DIR diff --git a/view.c b/view.c
       t@@ -1,6 +1,3 @@
       -/* FIXME: shrink buffers when text length less than a fourth of the size */
       -/* FIXME: handle all undo within buffer to keep it consistent */
       -
        #include <stdio.h>
        #include <errno.h>
        #include <string.h>
       t@@ -13,6 +10,7 @@
        #include <pango/pangoxft.h>
        #include <X11/extensions/Xdbe.h>
        
       +#include "util.h"
        #include "pango-compat.h"
        #include "memory.h"
        #include "common.h"
       t@@ -75,9 +73,6 @@ static PangoAttrList *get_pango_attributes(ledit_view *view, size_t start_byte, 
         */
        static void set_line_layout_attrs(ledit_view *view, size_t line, PangoLayout *layout);
        
       -/* Swap size_t values. */
       -static void swap_sz(size_t *a, size_t *b);
       -
        /* Move the gap of the line gap buffer to index 'index'. */
        static void move_line_gap(ledit_view *view, size_t index);
        
       t@@ -90,14 +85,14 @@ static void resize_and_move_line_gap(ledit_view *view, size_t min_size, size_t i
        /* FIXME: This is weird because mode is per-view but the undo mode group
           is changed for the entire buffer. */
        void
       -view_set_mode(ledit_view *view, enum ledit_mode mode) {
       +view_set_mode(ledit_view *view, ledit_mode mode) {
                view->mode = mode;
                undo_change_mode_group(view->buffer->undo);
                window_set_mode(view->window, mode);
        }
        
        ledit_view *
       -view_create(ledit_buffer *buffer, ledit_theme *theme, enum ledit_mode mode, size_t line, size_t pos) {
       +view_create(ledit_buffer *buffer, ledit_theme *theme, ledit_mode mode, size_t line, size_t pos) {
                if (basic_attrs == NULL) {
                        basic_attrs = pango_attr_list_new();
                        #if PANGO_VERSION_CHECK(1, 44, 0)
       t@@ -179,7 +174,6 @@ move_line_gap(ledit_view *view, size_t index) {
        
        static void
        resize_and_move_line_gap(ledit_view *view, size_t min_size, size_t index) {
       -        /* FIXME: Add to common bug list: used sizeof(ledit_line) instead of sizeof(ledit_view_line) */
                view->lines = resize_and_move_gap(
                    view->lines, sizeof(ledit_view_line),
                    view->lines_gap, view->lines_cap, view->lines_num,
       t@@ -235,10 +229,7 @@ view_notify_delete_text(ledit_view *view, size_t line, size_t index, size_t len)
        
        void
        view_notify_append_line(ledit_view *view, size_t line) {
       -        size_t new_len = view->lines_num + 1;
       -        if (new_len <= view->lines_num)
       -                err_overflow();
       -        resize_and_move_line_gap(view, new_len, line + 1);
       +        resize_and_move_line_gap(view, add_sz(view->lines_num, 1), line + 1);
                if (line < view->cur_line)
                        view->cur_line++;
                if (view->sel_valid)
       t@@ -278,6 +269,11 @@ view_notify_delete_lines(ledit_view *view, size_t index1, size_t index2) {
                );
                move_line_gap(view, index1);
                view->lines_num -= index2 - index1 + 1;
       +        /* possibly decrease size of array - this needs to be after
       +           actually deleting the lines so the length is already less */
       +        size_t min_size = ideal_array_size(view->lines_cap, view->lines_num);
       +        if (min_size != view->lines_cap)
       +                resize_and_move_line_gap(view, view->lines_num, view->lines_gap);
                /* force first entry to offset 0 if first line was deleted */
                if (index1 == 0) {
                        ledit_view_line *vl = view_get_line(view, 0);
       t@@ -329,7 +325,7 @@ set_line_layout_attrs(ledit_view *view, size_t line, PangoLayout *layout) {
                PangoAttrList *list = NULL;
                if (view->sel_valid) {
                        ledit_range sel = view->sel;
       -                view_sort_selection(&sel.line1, &sel.byte1, &sel.line2, &sel.byte2);
       +                sort_range(&sel.line1, &sel.byte1, &sel.line2, &sel.byte2);
                        if (sel.line1 < line && sel.line2 > line) {
                                list = get_pango_attributes(view, 0, ll->len);
                        } else if (sel.line1 == line && sel.line2 == line) {
       t@@ -486,6 +482,7 @@ view_recalc_line(ledit_view *view, size_t line) {
                 * and adjust offsets of all lines following it */
                if (l->h_dirty) {
                        l->h_dirty = 0;
       +                /* FIXME: maybe also check overflow for offset? */
                        long off = l->y_offset + l->h;
                        for (size_t i = line + 1; i < view->lines_num; i++) {
                                l = view_get_line(view, i);
       t@@ -554,10 +551,8 @@ view_next_cursor_pos(ledit_view *view, size_t line, size_t byte, int num) {
                PangoLayout *layout = get_pango_layout(view, line);
                const PangoLogAttr *attrs =
                    pango_layout_get_log_attrs_readonly(layout, &nattrs);
       -        if (num < 0)
       -                return 0; /* FIXME: error */
                for (size_t i = 0; i < (size_t)num; i++) {
       -                cur_byte = line_next_utf8(ll, byte);
       +                cur_byte = line_next_utf8(ll, cur_byte);
                        for (c++; c < (size_t)nattrs; c++) {
                                if (attrs[c].is_cursor_position)
                                        break;
       t@@ -578,8 +573,6 @@ view_prev_cursor_pos(ledit_view *view, size_t line, size_t byte, int num) {
                PangoLayout *layout = get_pango_layout(view, line);
                const PangoLogAttr *attrs =
                    pango_layout_get_log_attrs_readonly(layout, &nattrs);
       -        if (num < 0)
       -                return 0; /* FIXME: error */
                for (int i = 0; i < num; i++) {
                        cur_byte = line_prev_utf8(ll, cur_byte);
                        for (; c > 0; c--) {
       t@@ -1069,7 +1062,6 @@ get_pango_layout(ledit_view *view, size_t line) {
                return cl->layout;
        }
        
       -/* FIXME: document what works with pango units and what not */
        void
        view_pos_to_x_softline(ledit_view *view, size_t line, size_t pos, int *x_ret, int *softline_ret) {
                ledit_view_line *vl = view_get_line(view, line);
       t@@ -1114,9 +1106,9 @@ view_x_softline_to_pos(ledit_view *view, size_t line, int x, int softline) {
                    pango_line, x_relative, &tmp_pos, &trailing
                );
                size_t pos = (size_t)tmp_pos;
       -        /* if in insert mode, snap to the nearest border between graphemes */
       +        /* if in insert or visual mode, snap to the nearest border between graphemes */
                /* FIXME: add parameter for this instead of checking mode */
       -        if (view->mode == INSERT) {
       +        if (view->mode == INSERT || view->mode == VISUAL) {
                        ledit_line *ll = buffer_get_line(view->buffer, line);
                        while (trailing > 0) {
                                trailing--;
       t@@ -1176,9 +1168,6 @@ view_delete_range(
        /* line_index1, byte_index1 are used as the cursor position in order
           to determine the new cursor position */
        /* FIXME: use at least somewhat sensible variable names */
       -/* FIXME: I once noticed a bug where using 'dG' to delete to the end of
       -   the file caused a line index way larger than buffer->lines_num to be
       -   given, but I couldn't reproduce this bug */
        void
        view_delete_range_base(
            ledit_view *view,
       t@@ -1194,7 +1183,7 @@ view_delete_range_base(
                   -> FIXME: why not just use view->cur_line, view->cur_index here? */
                size_t cur_line = line_index1;
                size_t cur_byte = byte_index1;
       -        view_sort_selection(&line_index1, &byte_index1, &line_index2, &byte_index2);
       +        sort_range(&line_index1, &byte_index1, &line_index2, &byte_index2);
                size_t new_line = 0, new_byte = 0;
                ledit_assert(line_index1 < view->lines_num);
                ledit_assert(line_index2 < view->lines_num);
       t@@ -1682,24 +1671,6 @@ view_ensure_cursor_shown(ledit_view *view) {
                }
        }
        
       -static void
       -swap_sz(size_t *a, size_t *b) {
       -        size_t tmp = *a;
       -        *a = *b;
       -        *b = tmp;
       -}
       -
       -/* FIXME: this is generic, so it doesn't need to be in view.c */
       -void
       -view_sort_selection(size_t *line1, size_t *byte1, size_t *line2, size_t *byte2) {
       -        if (*line1 > *line2) {
       -                swap_sz(line1, line2);
       -                swap_sz(byte1, byte2);
       -        } else if (*line1 == *line2 && *byte1 > *byte2) {
       -                swap_sz(byte1, byte2);
       -        }
       -}
       -
        /* FIXME: don't reset selection when selection is clicked away */
        /* FIXME: when selecting with mouse, only call this when button is released */
        /* lines and bytes need to be sorted already! */
       t@@ -1740,8 +1711,8 @@ view_set_selection(ledit_view *view, size_t line1, size_t byte1, size_t line2, s
                }
                size_t l1_new = line1, l2_new = line2;
                size_t b1_new = byte1, b2_new = byte2;
       -        view_sort_selection(&l1_new, &b1_new, &l2_new, &b2_new);
       -        view_sort_selection(&view->sel.line1, &view->sel.byte1, &view->sel.line2, &view->sel.byte2);
       +        sort_range(&l1_new, &b1_new, &l2_new, &b2_new);
       +        sort_range(&view->sel.line1, &view->sel.byte1, &view->sel.line2, &view->sel.byte2);
                /* FIXME: make this a bit nicer and optimize it */
                if (view->sel.line1 > l2_new || view->sel.line2 < l1_new) {
                        for (size_t i = view->sel.line1; i <= view->sel.line2; i++) {
       t@@ -1772,6 +1743,7 @@ view_set_selection(ledit_view *view, size_t line1, size_t byte1, size_t line2, s
                view->sel.line2 = line2;
                view->sel.byte2 = byte2;
                view->sel_valid = 1;
       +        view->redraw = 1;
        }
        
        static void
       t@@ -1815,8 +1787,8 @@ view_button_handler(void *data, XEvent *event) {
                                        view_wipe_line_cursor_attrs(view, view->cur_line);
                                        /* FIXME: return to old mode afterwards? */
                                        /* should change_mode_group even be called here? */
       -                                view_set_mode(view, VISUAL);
                                }
       +                        view_set_mode(view, VISUAL);
                                if (!view->sel_valid) {
                                        /* the selection has just started, so the current
                                           position is already set to the beginning of the
       t@@ -1962,32 +1934,43 @@ view_redraw(ledit_view *view) {
        }
        
        void
       -view_undo(ledit_view *view) {
       -        /* FIXME: maybe wipe selection */
       -        size_t old_line = view->cur_line;
       -        buffer_undo(view->buffer, view->mode, &view->cur_line, &view->cur_index);
       -        if (view->mode == NORMAL) {
       -                view->cur_index = view_get_legal_normal_pos(
       -                    view, view->cur_line, view->cur_index
       -                );
       +view_undo(ledit_view *view, int num) {
       +        /* FIXME: maybe wipe selection (although I guess this
       +           currently isn't possible in visual mode anyways) */
       +        for (int i = 0; i < num; i++) {
       +                size_t old_line = view->cur_line;
       +                undo_status s = buffer_undo(view->buffer, view->mode, &view->cur_line, &view->cur_index);
       +                if (view->mode == NORMAL) {
       +                        view->cur_index = view_get_legal_normal_pos(
       +                            view, view->cur_line, view->cur_index
       +                        );
       +                }
       +                view_wipe_line_cursor_attrs(view, old_line);
       +                view_set_line_cursor_attrs(view, view->cur_line, view->cur_index);
       +                if (s != UNDO_NORMAL) {
       +                        window_show_message(view->window, undo_state_to_str(s), -1);
       +                        break;
       +                }
                }
       -        view_wipe_line_cursor_attrs(view, old_line);
       -        view_set_line_cursor_attrs(view, view->cur_line, view->cur_index);
       -        /* FIXME: show undo message */
        }
        
        void
       -view_redo(ledit_view *view) {
       -        size_t old_line = view->cur_line;
       -        buffer_redo(view->buffer, view->mode, &view->cur_line, &view->cur_index);
       -        if (view->mode == NORMAL) {
       -                view->cur_index = view_get_legal_normal_pos(
       -                    view, view->cur_line, view->cur_index
       -                );
       +view_redo(ledit_view *view, int num) {
       +        for (int i = 0; i < num; i++) {
       +                size_t old_line = view->cur_line;
       +                undo_status s = buffer_redo(view->buffer, view->mode, &view->cur_line, &view->cur_index);
       +                if (view->mode == NORMAL) {
       +                        view->cur_index = view_get_legal_normal_pos(
       +                            view, view->cur_line, view->cur_index
       +                        );
       +                }
       +                view_wipe_line_cursor_attrs(view, old_line);
       +                view_set_line_cursor_attrs(view, view->cur_line, view->cur_index);
       +                if (s != UNDO_NORMAL) {
       +                        window_show_message(view->window, undo_state_to_str(s), -1);
       +                        break;
       +                }
                }
       -        view_wipe_line_cursor_attrs(view, old_line);
       -        view_set_line_cursor_attrs(view, view->cur_line, view->cur_index);
       -        /* FIXME: show undo message */
        }
        
        static void
   DIR diff --git a/view.h b/view.h
       t@@ -6,6 +6,13 @@
        #ifndef _LEDIT_VIEW_H_
        #define _LEDIT_VIEW_H_
        
       +#include <stddef.h>
       +#include "common.h"
       +#include "txtbuf.h"
       +#include "window.h"
       +#include "theme.h"
       +#include "cache.h"
       +
        typedef struct ledit_view ledit_view;
        
        #include "buffer.h"
       t@@ -69,7 +76,7 @@ struct ledit_view {
                long total_height;        /* total pixel height of all lines */
                long display_offset;      /* current pixel offset of viewport */
                ledit_range sel;          /* current selection */
       -        enum ledit_mode mode;     /* current mode of this view */
       +        ledit_mode mode;     /* current mode of this view */
                char selecting;           /* whether user is currently selecting text with mouse */
                char sel_valid;           /* whether there is currently a valid selection */
                char redraw;              /* whether something has changed so the view needs to be redrawn */
       t@@ -86,7 +93,7 @@ enum delete_mode {
         * This changes the mode group of the associated buffer's
         * undo stack and changes the mode display in the window.
         */
       -void view_set_mode(ledit_view *view, enum ledit_mode mode);
       +void view_set_mode(ledit_view *view, ledit_mode mode);
        
        /*
         * Create a view with associated buffer 'buffer' and theme 'theme'.
       t@@ -95,7 +102,7 @@ void view_set_mode(ledit_view *view, enum ledit_mode mode);
         */
        ledit_view *view_create(
            ledit_buffer *buffer, ledit_theme *theme,
       -    enum ledit_mode mode, size_t line, size_t pos
       +    ledit_mode mode, size_t line, size_t pos
        );
        
        /*
       t@@ -442,11 +449,6 @@ void view_scroll_to_pos_bottom(ledit_view *view, size_t line, size_t byte);
        void view_ensure_cursor_shown(ledit_view *view);
        
        /*
       - * Sort the given range so that (*line1 < *line2) or (*line1 == *line2 && *byte1 <= *byte2).
       - */
       -void view_sort_selection(size_t *line1, size_t *byte1, size_t *line2, size_t *byte2);
       -
       -/*
         * Clear the selection.
         */
        void view_wipe_selection(ledit_view *view);
       t@@ -465,20 +467,20 @@ void view_set_selection(ledit_view *view, size_t line1, size_t byte1, size_t lin
        void view_redraw(ledit_view *view);
        
        /*
       - * Perform an undo step.
       + * Perform up to num undo steps.
         * The cursor position of the view is set to the stored position
         * in the undo stack.
         * The line heights and offsets are recalculated.
         */
       -void view_undo(ledit_view *view);
       +void view_undo(ledit_view *view, int num);
        
        /*
       - * Perform a redo step.
       + * Perform up to num redo steps.
         * The cursor position of the view is set to the stored position
         * in the undo stack.
         * The line heights and offsets are recalculated.
         */
       -void view_redo(ledit_view *view);
       +void view_redo(ledit_view *view, int num);
        
        /*
         * Paste the X11 clipboard at the current cursor position.
   DIR diff --git a/window.c b/window.c
       t@@ -9,19 +9,18 @@
        #include <X11/Xatom.h>
        #include <X11/Xutil.h>
        #include <pango/pangoxft.h>
       -#include <X11/XKBlib.h>
       -#include <X11/extensions/XKBrules.h>
        #include <X11/extensions/Xdbe.h>
        
       +#include "util.h"
       +#include "theme.h"
        #include "memory.h"
        #include "common.h"
        #include "txtbuf.h"
       -#include "theme.h"
        #include "window.h"
       -#include "util.h"
        #include "macros.h"
        #include "config.h"
        #include "assert.h"
       +#include "draw_util.h"
        
        /* FIXME: Everything to do with the bottom bar is extremely hacky */
        struct bottom_bar {
       t@@ -106,14 +105,14 @@ recalc_text_size(ledit_window *window) {
        
        static void
        resize_line_text(ledit_window *window, int min_size) {
       -        if (min_size > window->bb->line_alloc || window->bb->line_text == NULL) {
       -                /* FIXME: read up on what the best values are here */
       -                /* FIXME: overflow */
       -                window->bb->line_alloc =
       -                    window->bb->line_alloc * 2 > min_size ?
       -                    window->bb->line_alloc * 2 :
       -                    min_size;
       -                window->bb->line_text = ledit_realloc(window->bb->line_text, window->bb->line_alloc);
       +        /* FIXME: use size_t everywhere */
       +        ledit_assert(min_size >= 0);
       +        size_t cap = ideal_array_size(window->bb->line_alloc, min_size);
       +        if (cap > INT_MAX)
       +                err_overflow();
       +        if (cap != (size_t)window->bb->line_alloc) {
       +                window->bb->line_alloc = (int)cap;
       +                window->bb->line_text = ledit_realloc(window->bb->line_text, cap);
                }
        }
        
       t@@ -337,7 +336,7 @@ window_hide_message(ledit_window *window) {
        }
        
        void
       -window_set_mode(ledit_window *window, enum ledit_mode mode) {
       +window_set_mode(ledit_window *window, ledit_mode mode) {
                window->mode = mode;
                char *text;
                switch (mode) {
       t@@ -516,7 +515,7 @@ xximspot(ledit_window *window, int x, int y) {
        }
        
        ledit_window *
       -window_create(ledit_common *common, ledit_theme *theme, enum ledit_mode mode) {
       +window_create(ledit_common *common, ledit_theme *theme, ledit_mode mode) {
                XSetWindowAttributes attrs;
                XGCValues gcv;
        
       t@@ -659,7 +658,6 @@ window_destroy(ledit_window *window) {
                /*g_object_unref(window->context);*/
                g_object_unref(window->fontmap);
        
       -        /* FIXME: is gc, etc. destroyed automatically when destroying window? */
                if (window->spotlist)
                        XFree(window->spotlist);
                XDestroyWindow(window->common->dpy, window->xwin);
   DIR diff --git a/window.h b/window.h
       t@@ -1,3 +1,6 @@
       +#ifndef _WINDOW_H_
       +#define _WINDOW_H_
       +
        /*
         * A window is associated with exactly one view and is responsible everything
         * other than the actual text handling (of the text in the buffer).
       t@@ -5,10 +8,17 @@
         * partially here and partially in keys_command, but that's the way it is for now.
         */
        
       -#ifndef _WINDOW_H_
       -#define _WINDOW_H_
       -
       +#include <time.h>
        #include <stdarg.h>
       +#include <X11/Xlib.h>
       +#include <X11/Xatom.h>
       +#include <X11/Xutil.h>
       +#include <X11/extensions/Xdbe.h>
       +#include <pango/pangoxft.h>
       +
       +#include "theme.h"
       +#include "common.h"
       +#include "txtbuf.h"
        
        typedef struct bottom_bar bottom_bar;
        
       t@@ -42,7 +52,7 @@ typedef struct {
                int message_shown;       /* whether a readonly message is shown at the bottom */
                bottom_bar *bb;          /* encapsulates the text at the bottom */
                int redraw;              /* whether something has changed and the window has to be redrawn */
       -        enum ledit_mode mode;    /* mode of the view - a bit ugly to duplicate this here... */
       +        ledit_mode mode;    /* mode of the view - a bit ugly to duplicate this here... */
        
                /* stuff for filtering events so not too many have to be handled */
                struct timespec last_scroll;
       t@@ -83,7 +93,7 @@ typedef struct {
        /*
         * Create a window with initial mode 'mode'.
         */
       -ledit_window *window_create(ledit_common *common, ledit_theme *theme, enum ledit_mode mode);
       +ledit_window *window_create(ledit_common *common, ledit_theme *theme, ledit_mode mode);
        
        /*
         * Destroy a window.
       t@@ -171,6 +181,8 @@ void window_set_bottom_bar_realtext(ledit_window *window, char *text, int len);
        
        /*
         * Get the text of the editable line.
       + * WARNING: this is a direct pointer to the internal storage,
       + * it is not copied.
         */
        char *window_get_bottom_bar_text(ledit_window *window);
        
       t@@ -183,6 +195,10 @@ void window_show_message(ledit_window *window, char *text, int len);
         * Show a non-editable message that is given as a
         * format string and arguments as interpreted by
         * vsnprintf(3)
       + * WARNING: This may reallocate the internal text storage for the
       + * bottom bar before actually using the format arguments, so don't
       + * ever pass parts of the text returned by window_get_bottom_bar_text
       + * to this function without first copying it.
         */
        void window_show_message_fmt(ledit_window *window, char *fmt, ...);
        
       t@@ -199,7 +215,7 @@ int window_message_shown(ledit_window *window);
        /*
         * Set the displayed mode of the window.
         */
       -void window_set_mode(ledit_window *window, enum ledit_mode mode);
       +void window_set_mode(ledit_window *window, ledit_mode mode);
        
        /*
         * Set extra text that is shown to the right of the mode.