URI: 
       tAdd initial support for deletion - 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 52cea73bf0e43483210cb94e5177989bc524a30c
   DIR parent 174e6af4e28e5dec7b1402de9b15174357898865
  HTML Author: lumidify <nobody@lumidify.org>
       Date:   Wed, 12 May 2021 21:47:17 +0200
       
       Add initial support for deletion
       
       Diffstat:
         M buffer.c                            |     275 ++++++++++++++++++++++++++++++-
         M buffer.h                            |      10 ++++++++++
         M ledit.c                             |     342 ++++++++++++++++---------------
       
       3 files changed, 452 insertions(+), 175 deletions(-)
       ---
   DIR diff --git a/buffer.c b/buffer.c
       t@@ -138,8 +138,9 @@ init_line(ledit_buffer *buffer, ledit_line *line) {
                line->parent_buffer = buffer;
                line->layout = pango_layout_new(buffer->state->context);
                pango_layout_set_width(line->layout, (buffer->state->w - 10) * PANGO_SCALE);
       -        pango_layout_set_font_description(line->layout, buffer->state->font);
       -        pango_layout_set_wrap(line->layout, PANGO_WRAP_WORD_CHAR);
       +        pango_layout_set_font_description(line->layout, buffer->state->font);
       +        pango_layout_set_wrap(line->layout, PANGO_WRAP_WORD_CHAR);
       +        pango_layout_set_attributes(line->layout, basic_attrs);
                line->text = NULL;
                line->cap = line->len = 0;
                line->cache_index = -1;
       t@@ -185,18 +186,27 @@ ledit_append_line(ledit_buffer *buffer, int line_index, int text_index) {
        }
        
        void
       -ledit_delete_line_entry(ledit_buffer *buffer, int index) {
       -        g_object_unref(buffer->lines[index].layout);
       -        free(buffer->lines[index].text);
       -        if (index < buffer->lines_num - 1)
       +ledit_delete_line_entries(ledit_buffer *buffer, int index1, int index2) {
       +        for (int i = index1; i <= index2; i++) {
       +                g_object_unref(buffer->lines[i].layout);
       +                free(buffer->lines[i].text);
       +        }
       +        if (index2 < buffer->lines_num - 1) {
                        memmove(
       -                    buffer->lines + index, buffer->lines + index + 1,
       -                    (buffer->lines_num - index - 1) * sizeof(ledit_line)
       +                    buffer->lines + index1, buffer->lines + index2 + 1,
       +                    (buffer->lines_num - index2 - 1) * sizeof(ledit_line)
                        );
       -        buffer->lines_num--;
       +        }
       +        buffer->lines_num -= index2 - index1 + 1;
       +        /* FIXME: avoid this by just subtracting the heights */
                recalc_line_size_absolute(buffer);
        }
        
       +void
       +ledit_delete_line_entry(ledit_buffer *buffer, int index) {
       +        ledit_delete_line_entries(buffer, index, index);
       +}
       +
        /* FIXME: use some sort of gap buffer (that would make this function
           slightly more useful...) */
        ledit_line *
       t@@ -270,3 +280,250 @@ ledit_delete_unicode_char(ledit_buffer *buffer, int line_index, int byte_index, 
                recalc_single_line_size(buffer, line_index);
                return new_index;
        }
       +
       +static void
       +delete_line_section(ledit_buffer *buffer, int line, int start, int length) {
       +        ledit_line *l = &buffer->lines[line];
       +        memmove(l->text + start, l->text + start + length, l->len - start - length);
       +        l->len -= length;
       +        pango_layout_set_text(l->layout, l->text, l->len);
       +        recalc_single_line_size(buffer, line);
       +        l->dirty = 1;
       +}
       +
       +void
       +ledit_pos_to_x_softline(ledit_line *line, int pos, int *x_ret, int *softline_ret) {
       +        pango_layout_index_to_line_x(line->layout, pos, 0, softline_ret, x_ret);
       +        /* FIXME: do these lines need to be unref'd? */
       +        PangoLayoutLine *pango_line = pango_layout_get_line_readonly(line->layout, *softline_ret);
       +        /* add left margin to x position if line is aligned right */
       +        if (pango_line->resolved_dir == PANGO_DIRECTION_RTL) {
       +                PangoRectangle rect;
       +                pango_layout_line_get_extents(pango_line, NULL, &rect);
       +                *x_ret += (line->w * PANGO_SCALE - rect.width);
       +        }
       +        /* if in normal mode, change position to the middle of the
       +           current rectangle so that moving around won't jump weirdly */
       +        /* FIXME: also in visual? */
       +        if (line->parent_buffer->state->mode == NORMAL) {
       +                PangoRectangle rect;
       +                pango_layout_index_to_pos(line->layout, pos, &rect);
       +                *x_ret += rect.width / 2;
       +        }
       +}
       +
       +/* FIXME: change this to return int */
       +void
       +ledit_x_softline_to_pos(ledit_line *line, int x, int softline, int *pos_ret) {
       +        int trailing = 0;
       +        int x_relative = x;
       +        PangoLayoutLine *pango_line =
       +            pango_layout_get_line_readonly(line->layout, softline);
       +        /* x is absolute, so the margin at the left needs to be subtracted */
       +        if (pango_line->resolved_dir == PANGO_DIRECTION_RTL) {
       +                PangoRectangle rect;
       +                pango_layout_line_get_extents(pango_line, NULL, &rect);
       +                x_relative -= (line->w * PANGO_SCALE - rect.width);
       +        }
       +        pango_layout_line_x_to_index(
       +            pango_line, x_relative, pos_ret, &trailing
       +        );
       +        /* if in insert mode, snap to the nearest border between graphemes */
       +        if (line->parent_buffer->state->mode == INSERT) {
       +                while (trailing > 0) {
       +                        trailing--;
       +                        (*pos_ret)++;
       +                        /* utf8 stuff */
       +                        while (*pos_ret < line->len &&
       +                               ((line->text[*pos_ret] & 0xC0) == 0x80))
       +                                (*pos_ret)++;
       +                }
       +        }
       +}
       +
       +/* FIXME: cursor jumps weirdly */
       +/* FIXME: use at least somewhat sensible variable names */
       +void
       +ledit_delete_range(
       +    ledit_buffer *buffer, int line_based,
       +    int line_index1, int byte_index1,
       +    int line_index2, int byte_index2,
       +    int *new_line_ret, int *new_byte_ret) {
       +        if (line_based) {
       +                int x, softline1, softline2;
       +                ledit_line *line1 = ledit_get_line(buffer, line_index1);
       +                ledit_pos_to_x_softline(line1, byte_index1, &x, &softline1);
       +                if (line_index1 == line_index2) {
       +                        int x_useless;
       +                        pango_layout_index_to_line_x(line1->layout, byte_index2, 0, &softline2, &x_useless);
       +                        int l1 = softline1 < softline2 ? softline1 : softline2;
       +                        int l2 = softline1 < softline2 ? softline2 : softline1;
       +                        int softlines = pango_layout_get_line_count(line1->layout);
       +                        PangoLayoutLine *pl1 = pango_layout_get_line_readonly(line1->layout, l1);
       +                        PangoLayoutLine *pl2 = pango_layout_get_line_readonly(line1->layout, l2);
       +                        /* don't delete entire line of it is the last one remaining */
       +                        if (l1 == 0 && l2 == softlines - 1 && buffer->lines_num > 1) {
       +                                ledit_delete_line_entry(buffer, line_index1);
       +                                /* note: line_index1 is now the index of the next
       +                                   line since the current one was just deleted */
       +                                if (line_index1 < buffer->lines_num) {
       +                                        *new_line_ret = line_index1;
       +                                        ledit_x_softline_to_pos(
       +                                            ledit_get_line(buffer, line_index1),
       +                                            x, 0, new_byte_ret
       +                                        );
       +                                } else {
       +                                        /* note: logically, this must be >= 0 because
       +                                           buffer->lines_num > 1 && line_index1 >= buffer->lines_num */
       +                                        *new_line_ret = line_index1 - 1;
       +                                        ledit_line *prevline = ledit_get_line(buffer, line_index1 - 1);
       +                                        softlines = pango_layout_get_line_count(prevline->layout);
       +                                        ledit_x_softline_to_pos(prevline, x, softlines - 1, new_byte_ret);
       +                                }
       +                        } else {
       +                                /* FIXME: sanity checks that the length is actually positive, etc. */
       +                                delete_line_section(
       +                                    buffer, line_index1, pl1->start_index,
       +                                    pl2->start_index + pl2->length - pl1->start_index
       +                                );
       +                                if (l2 == softlines - 1 && line_index1 < buffer->lines_num - 1) {
       +                                        *new_line_ret = line_index1 + 1;
       +                                        ledit_x_softline_to_pos(
       +                                            ledit_get_line(buffer, line_index1 + 1),
       +                                            x, 0, new_byte_ret
       +                                        );
       +                                } else if (l2 < softlines - 1) {
       +                                        *new_line_ret = line_index1;
       +                                        ledit_x_softline_to_pos(
       +                                            ledit_get_line(buffer, line_index1),
       +                                            x, l1, new_byte_ret
       +                                        );
       +                                } else if (l1 > 0) {
       +                                        *new_line_ret = line_index1;
       +                                        ledit_x_softline_to_pos(
       +                                            ledit_get_line(buffer, line_index1),
       +                                            x, l1 - 1, new_byte_ret
       +                                        );
       +                                } else {
       +                                        /* the line has been emptied and is the last line remaining */
       +                                        *new_line_ret = 0;
       +                                        *new_byte_ret = 0;
       +                                }
       +                        }
       +                } else {
       +                        int x_useless, sl1, sl2;
       +                        int l1, l2, b1, b2;
       +                        if (line_index1 < line_index2) {
       +                                l1 = line_index1;
       +                                b1 = byte_index1;
       +                                l2 = line_index2;
       +                                b2 = byte_index2;
       +                        } else {
       +                                l1 = line_index2;
       +                                b1 = byte_index2;
       +                                l2 = line_index1;
       +                                b2 = byte_index1;
       +                        }
       +                        ledit_line *ll1 = ledit_get_line(buffer, l1);
       +                        ledit_line *ll2 = ledit_get_line(buffer, l2);
       +                        pango_layout_index_to_line_x(ll1->layout, b1, 0, &sl1, &x_useless);
       +                        pango_layout_index_to_line_x(ll2->layout, b2, 0, &sl2, &x_useless);
       +                        PangoLayoutLine *pl1 = pango_layout_get_line_readonly(ll1->layout, sl1);
       +                        PangoLayoutLine *pl2 = pango_layout_get_line_readonly(ll2->layout, sl2);
       +                        int softlines = pango_layout_get_line_count(ll2->layout);
       +                        if (sl1 == 0 && sl2 == softlines - 1) {
       +                                if (l1 == 0 && l2 == buffer->lines_num - 1) {
       +                                        delete_line_section(buffer, l1, 0, ll1->len);
       +                                        ledit_delete_line_entries(buffer, l1 + 1, l2);
       +                                        *new_line_ret = 0;
       +                                        *new_byte_ret = 0;
       +                                } else {
       +                                        ledit_delete_line_entries(buffer, l1, l2);
       +                                        if (l1 >= buffer->lines_num) {
       +                                                *new_line_ret = buffer->lines_num - 1;
       +                                                ledit_line *new_lline = ledit_get_line(buffer, *new_line_ret);
       +                                                int new_softlines = pango_layout_get_line_count(new_lline->layout);
       +                                                ledit_x_softline_to_pos(new_lline, x, new_softlines - 1, new_byte_ret);
       +                                        } else {
       +                                                *new_line_ret = l1;
       +                                                ledit_x_softline_to_pos(
       +                                                    ledit_get_line(buffer, l1),
       +                                                    x, 0, new_byte_ret
       +                                                );
       +                                        }
       +                                }
       +                        } else if (sl1 == 0) {
       +                                delete_line_section(buffer, l2, 0, pl2->start_index + pl2->length);
       +                                ledit_delete_line_entries(buffer, l1, l2 - 1);
       +                                *new_line_ret = l1;
       +                                ledit_x_softline_to_pos(ledit_get_line(buffer, l1), x, 0, new_byte_ret);
       +                        } else if (sl2 == softlines - 1) {
       +                                delete_line_section(buffer, l1, pl1->start_index, ll1->len - pl1->start_index);
       +                                ledit_delete_line_entries(buffer, l1 + 1, l2);
       +                                if (l1 + 1 >= buffer->lines_num) {
       +                                        *new_line_ret = buffer->lines_num - 1;
       +                                        ledit_line *new_lline = ledit_get_line(buffer, *new_line_ret);
       +                                        int new_softlines = pango_layout_get_line_count(new_lline->layout);
       +                                        ledit_x_softline_to_pos(new_lline, x, new_softlines - 1, new_byte_ret);
       +                                } else {
       +                                        *new_line_ret = l1 + 1;
       +                                        ledit_x_softline_to_pos(
       +                                            ledit_get_line(buffer, l1 + 1),
       +                                            x, 0, new_byte_ret
       +                                        );
       +                                }
       +                        } else {
       +                                /* FIXME: should this join the two lines? */
       +                                delete_line_section(buffer, l1, pl1->start_index, ll1->len - pl1->start_index);
       +                                delete_line_section(buffer, l2, 0, pl2->start_index + pl2->length);
       +                                if (l2 > l1 + 1)
       +                                        ledit_delete_line_entries(buffer, l1 + 1, l2 - 1);
       +                                *new_line_ret = l1 + 1;
       +                                ledit_x_softline_to_pos(ledit_get_line(buffer, l1 + 1), x, 0, new_byte_ret);
       +                        }
       +                }
       +        } else {
       +                if (line_index1 == line_index2) {
       +                        int b1, b2;
       +                        if (byte_index1 < byte_index2) {
       +                                b1 = byte_index1;
       +                                b2 = byte_index2;
       +                        } else {
       +                                b1 = byte_index2;
       +                                b2 = byte_index1;
       +                        }
       +                        delete_line_section(buffer, line_index1, b1, b2 - b1);
       +                        *new_line_ret = line_index1;
       +                        *new_byte_ret = b1;
       +                        /* FIXME: this needs to be checked by calling code to
       +                           move cursor one back if in normal mode and at end
       +                           of line */
       +                } else {
       +                        int l1, l2, b1, b2;
       +                        if (line_index1 < line_index2) {
       +                                l1 = line_index1;
       +                                b1 = byte_index1;
       +                                l2 = line_index2;
       +                                b2 = byte_index2;
       +                        } else {
       +                                l1 = line_index2;
       +                                b1 = byte_index2;
       +                                l2 = line_index1;
       +                                b2 = byte_index1;
       +                        }
       +                        ledit_line *line1 = ledit_get_line(buffer, l1);
       +                        ledit_line *line2 = ledit_get_line(buffer, l2);
       +                        line1->len = b1;
       +                        if (b2 > 0) {
       +                                ledit_insert_text(
       +                                    buffer, l1, b1,
       +                                    line2->text + b2,
       +                                    line2->len - b2
       +                                );
       +                        }
       +                        *new_line_ret = l1;
       +                        *new_byte_ret = b1;
       +                        ledit_delete_line_entries(buffer, l1 + 1, l2);
       +                }
       +        }
       +}
   DIR diff --git a/buffer.h b/buffer.h
       t@@ -1,5 +1,6 @@
        typedef struct ledit_buffer ledit_buffer;
        
       +/* FIXME: size_t for len, etc. */
        typedef struct {
                PangoLayout *layout;
                char *text;
       t@@ -36,7 +37,16 @@ void ledit_wipe_line_cursor_attrs(ledit_buffer *buffer, int line);
        void ledit_insert_text(ledit_buffer *buffer, int line_index, int index, char *text, int len);
        void ledit_render_line(ledit_buffer *buffer, int line_index);
        void ledit_append_line(ledit_buffer *buffer, int line_index, int text_index);
       +void ledit_delete_line_entries(ledit_buffer *buffer, int index1, int index2);
        void ledit_delete_line_entry(ledit_buffer *buffer, int index);
        ledit_line *ledit_get_line(ledit_buffer *buffer, int index);
        int ledit_line_visible(ledit_buffer *buffer, int index);
        int ledit_delete_unicode_char(ledit_buffer *buffer, int line_index, int byte_index, int dir);
       +void ledit_delete_range(
       +    ledit_buffer *buffer, int line_based,
       +    int line_index1, int byte_index1,
       +    int line_index2, int byte_index2,
       +    int *new_line_ret, int *new_byte_ret
       +);
       +void ledit_pos_to_x_softline(ledit_line *line, int pos, int *x_ret, int *softline_ret);
       +void ledit_x_softline_to_pos(ledit_line *line, int x, int softline, int *pos_ret);
   DIR diff --git a/ledit.c b/ledit.c
       t@@ -31,24 +31,32 @@ enum key_type {
                KEY_NONE = 0,
                KEY_MISC = 1,
                KEY_CHAR = 2,
       -        KEY_MOTION = 4,
       -        KEY_NUMBER = 8,
       -        KEY_NUMBERALLOWED = 16,
       +        KEY_MOTION_CHAR = 4,
       +        KEY_MOTION_LINE = 8,
       +        KEY_MOTION = 4|8,
       +        KEY_NUMBER = 16,
       +        KEY_NUMBERALLOWED = 32,
                KEY_ANY = 0xFF
        };
        
        struct key {
       -        char *text;            /* for keys that correspond with text */
       -        KeySym keysym;         /* for other keys, e.g. arrow keys */
       -        enum ledit_mode modes; /* modes in which this keybinding is functional */
       +        char *text;              /* for keys that correspond with text */
       +        KeySym keysym;           /* for other keys, e.g. arrow keys */
       +        enum 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 */
       -        void (*func)(void);    /* callback function */
       +        void (*func)(void); /* callback function */
        };
        
        struct key_stack_elem {
       -        enum key_type key; /* key type */
       +        enum key_type key;
                enum key_type followup; /* allowed keys to complete the keybinding */
       -        void (*func)(void); /* function to call if an allowed key is entered */
       +        /* callback function that motion commands call to complete a command -
       +         * line and char_pos already include the repetition stored in this stack
       +         * element; type is the type of motion command (used to determine if
       +         * the command should operate on lines or chars) */
       +        void (*motion_cb)(int line, int char_pos, enum key_type type);
       +        int count; /* number of repetitions */
                int data1; /* misc. data 1 */
                int data2; /* misc. data 2 */
        };
       t@@ -63,12 +71,7 @@ static ledit_buffer *buffer;
        
        /* TODO: protect against overflow, especially on repeating commands */
        
       -static struct key_stack_elem *push_key_stack(
       -    enum key_type key,
       -    enum key_type followup,
       -    void (*func)(void),
       -    int data1, int data2
       -);
       +static struct key_stack_elem *push_key_stack(void);
        static struct key_stack_elem *peek_key_stack(void);
        static struct key_stack_elem *pop_key_stack(void);
        void clear_key_stack(void);
       t@@ -109,11 +112,81 @@ static void push_6(void);
        static void push_7(void);
        static void push_8(void);
        static void push_9(void);
       +static void key_d(void);
       +static void key_d_cb(int line, int char_pos, enum key_type type);
        
        static void change_keyboard(char *lang);
        static void key_press(XEvent event);
        
        static void
       +key_d(void) {
       +        int num = 0;
       +        struct key_stack_elem *e = pop_key_stack();
       +        if (e != NULL) {
       +                if (e->key & KEY_NUMBER) {
       +                        num = e->count;
       +                        e = pop_key_stack();
       +                }
       +                /* FIXME: checking equality of the function pointer may be a bit risky */
       +                if (e != NULL && e->motion_cb == &key_d_cb) {
       +                        int prevnum = e->count > 0 ? e->count : 1;
       +                        num = num > 0 ? num : 1;
       +                        int lines = num * prevnum;
       +
       +                        ledit_line *line = ledit_get_line(buffer, buffer->cur_line);
       +                        int x, softline;
       +                        pango_layout_index_to_line_x(line->layout, buffer->cur_index, 0, &softline, &x);
       +                        int softlines = pango_layout_get_line_count(line->layout);
       +                        if (softlines - softline >= lines) {
       +                                PangoLayoutLine *l = pango_layout_get_line_readonly(line->layout, softline + lines - 1);
       +                                e->motion_cb(buffer->cur_line, l->start_index, KEY_MOTION_LINE);
       +                        } else {
       +                                lines -= (softlines - softline);
       +                                int endline = buffer->cur_line + 1;
       +                                while (lines > 0 && endline < buffer->lines_num) {
       +                                        line = ledit_get_line(buffer, endline);
       +                                        softlines = pango_layout_get_line_count(line->layout);
       +                                        lines -= softlines;
       +                                        endline++;
       +                                }
       +                                endline--;
       +                                int endsoftline = 0;
       +                                if (lines <= 0) {
       +                                        endsoftline = lines + softlines - 1;
       +                                } else {
       +                                        endsoftline = softlines - 1;
       +                                }
       +                                PangoLayoutLine *l = pango_layout_get_line_readonly(line->layout, endsoftline);
       +                                e->motion_cb(endline, l->start_index, KEY_MOTION_LINE);
       +                        }
       +                        clear_key_stack();
       +                } else if (e != NULL) {
       +                        clear_key_stack();
       +                }
       +        }
       +        if (e == NULL) {
       +                e = push_key_stack();
       +                e->key = KEY_MOTION; /* ? */
       +                e->count = num;
       +                e->motion_cb = &key_d_cb;
       +        }
       +}
       +
       +/* FIXME: should this get number of lines to remove or actual end line? */
       +static void
       +key_d_cb(int line, int char_pos, enum key_type type) {
       +        printf("%d, %d\n", line, char_pos);
       +        int line_based = type == KEY_MOTION_LINE ? 1 : 0;
       +        ledit_delete_range(
       +            buffer, line_based,
       +            buffer->cur_line, buffer->cur_index,
       +            line, char_pos,
       +            &buffer->cur_line, &buffer->cur_index
       +        );
       +        ledit_set_line_cursor_attrs(buffer, buffer->cur_line, buffer->cur_index);
       +}
       +
       +static void
        key_x(void) {
                struct key_stack_elem *e = pop_key_stack();
                int num = 1;
       t@@ -121,7 +194,7 @@ key_x(void) {
                if (e && !(e->key & KEY_NUMBER))
                        return;
                if (e)
       -                num = e->data1;
       +                num = e->count;
                if (num <= 0)
                        num = 1;
                printf("delete %d\n", num);
       t@@ -130,14 +203,16 @@ key_x(void) {
        static void
        push_num(int num) {
                struct key_stack_elem *e = peek_key_stack();
       -        if (!e || !(e->key & KEY_NUMBER))
       -                e = push_key_stack(KEY_NUMBER, KEY_NUMBERALLOWED, NULL, 0, 0);
       +        if (!e || !(e->key & KEY_NUMBER)) {
       +                e = push_key_stack();
       +                e->key = KEY_NUMBER;
       +                e->followup = KEY_NUMBER|KEY_NUMBERALLOWED;
       +        }
                /* FIXME: error checking */
       -        e->data1 *= 10;
       -        e->data1 += num;
       +        e->count *= 10;
       +        e->count += num;
        }
        
       -/* FIXME: CHANGE BEHAVIOR TO MOVEMENT */
        static void
        push_0(void) {
                push_num(0);
       t@@ -198,11 +273,7 @@ main(int argc, char *argv[]) {
        }
        
        static struct key_stack_elem *
       -push_key_stack(
       -    enum key_type key,
       -    enum key_type followup,
       -    void (*func)(void),
       -    int data1, int data2) {
       +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;
       t@@ -212,11 +283,12 @@ push_key_stack(
                        key_stack.alloc = new_alloc;
                }
                e = &key_stack.stack[key_stack.len];
       -        e->key = key;
       -        e->followup = followup;
       -        e->func = func;
       -        e->data1 = data1;
       -        e->data2 = data2;
       +        e->key = KEY_NONE;
       +        e->followup = KEY_NONE;
       +        e->motion_cb = NULL;
       +        e->count = 0;
       +        e->data1 = 0;
       +        e->data2 = 0;
                key_stack.len++;
                return &key_stack.stack[key_stack.len - 1];
        }
       t@@ -757,21 +829,20 @@ move_cursor(int dir) {
                ledit_line *cur_line = ledit_get_line(buffer, buffer->cur_line);
                pango_layout_move_cursor_visually(
                    cur_line->layout, TRUE,
       -            buffer->cur_index, buffer->trailing, dir,
       -            &buffer->cur_index, &buffer->trailing
       +            buffer->cur_index, 0, dir,
       +            &buffer->cur_index, &trailing
                );
                /* FIXME: Allow cursor to be at end of soft line */
                /* we don't currently support a difference between the cursor being at
                   the end of a soft line and the beginning of the next line */
       -        /*
       -        while (buffer->trailing > 0) {
       -                buffer->trailing--;
       +        /* FIXME: spaces at end of softlines are weird in normal mode */
       +        while (trailing > 0) {
       +                trailing--;
                        buffer->cur_index++;
                        while (buffer->cur_index < cur_line->len &&
                               ((cur_line->text[buffer->cur_index] & 0xC0) == 0x80))
                                buffer->cur_index++;
                }
       -        */
                if (buffer->cur_index < 0)
                        buffer->cur_index = 0;
                /* when in normal mode, the cursor cannot be at the very end
       t@@ -832,23 +903,10 @@ enter_insert(void) {
        
        static void
        cursor_down(void) {
       -        int lineno, x, trailing = 0;
       +        int lineno, x;
                ledit_line *cur_line = ledit_get_line(buffer, buffer->cur_line);
       -        pango_layout_index_to_line_x(
       -            cur_line->layout, buffer->cur_index, 0, &lineno, &x
       -        );
       -        PangoLayoutLine *cur_pango_line =
       -            pango_layout_get_line_readonly(cur_line->layout, lineno);
       -        if (cur_pango_line->resolved_dir == PANGO_DIRECTION_RTL) {
       -                PangoRectangle rect;
       -                pango_layout_line_get_extents(cur_pango_line, NULL, &rect);
       -                x += (cur_line->w * PANGO_SCALE - rect.width);
       -        }
       -        if (state.mode == NORMAL) {
       -                PangoRectangle pos;
       -                pango_layout_index_to_pos(cur_line->layout, buffer->cur_index, &pos);
       -                x += pos.width / 2;
       -        }
       +        ledit_pos_to_x_softline(cur_line, buffer->cur_index, &x, &lineno);
       +
                int maxlines = pango_layout_get_line_count(cur_line->layout);
                if (lineno == maxlines - 1) {
                        ledit_wipe_line_cursor_attrs(buffer, buffer->cur_line);
       t@@ -856,59 +914,21 @@ cursor_down(void) {
                        if (buffer->cur_line < buffer->lines_num - 1) {
                                buffer->cur_line++;
                                cur_line = ledit_get_line(buffer, buffer->cur_line);
       -                        PangoLayoutLine *nextline =
       -                            pango_layout_get_line_readonly(cur_line->layout, 0);
       -                        if (nextline->resolved_dir == PANGO_DIRECTION_RTL) {
       -                                PangoRectangle rect;
       -                                pango_layout_line_get_extents(nextline, NULL, &rect);
       -                                x -= (cur_line->w * PANGO_SCALE - rect.width);
       -                        }
       -                        pango_layout_line_x_to_index(
       -                            nextline, x, &buffer->cur_index, &trailing
       -                        );
       -                        if (state.mode == INSERT)
       -                                buffer->cur_index += trailing;
       +                        ledit_x_softline_to_pos(cur_line, x, 0, &buffer->cur_index);
                        }
                } else {
                        /* move to the next soft line */
       -                PangoLayoutLine *nextline =
       -                    pango_layout_get_line_readonly(cur_line->layout, lineno + 1);
       -                if (nextline->resolved_dir == PANGO_DIRECTION_RTL) {
       -                        PangoRectangle rect;
       -                        pango_layout_line_get_extents(nextline, NULL, &rect);
       -                        x -= (cur_line->w * PANGO_SCALE - rect.width);
       -                }
       -                pango_layout_line_x_to_index(
       -                    nextline, x, &buffer->cur_index, &trailing
       -                );
       -                if (state.mode == INSERT)
       -                        buffer->cur_index += trailing;
       +                ledit_x_softline_to_pos(cur_line, x, lineno + 1, &buffer->cur_index);
                }
                ledit_set_line_cursor_attrs(buffer, buffer->cur_line, buffer->cur_index);
        }
        
        static void
        cursor_up(void) {
       -        int lineno, x, trailing = 0;
       +        int lineno, x;
                ledit_line *cur_line = ledit_get_line(buffer, buffer->cur_line);
       -        pango_layout_index_to_line_x(
       -            cur_line->layout, buffer->cur_index, 0, &lineno, &x
       -        );
       -        PangoLayoutLine *cur_pango_line =
       -            pango_layout_get_line_readonly(cur_line->layout, lineno);
       -        /* FIXME: do these lines need to be unref'd? */
       -        if (cur_pango_line->resolved_dir == PANGO_DIRECTION_RTL) {
       -                PangoRectangle rect;
       -                pango_layout_line_get_extents(cur_pango_line, NULL, &rect);
       -                /* FIXME: don't store w in each line because it is now the same for all */
       -                x += (cur_line->w * PANGO_SCALE - rect.width);
       -        }
       -        if (state.mode == NORMAL) {
       -                PangoRectangle pos;
       -                pango_layout_index_to_pos(cur_line->layout, buffer->cur_index, &pos);
       -                x += pos.width / 2;
       -        }
       -        /* FIXME: clean this up (if and else are very similar) */
       +        ledit_pos_to_x_softline(cur_line, buffer->cur_index, &x, &lineno);
       +        buffer->trailing = 0;
                if (lineno == 0) {
                        ledit_wipe_line_cursor_attrs(buffer, buffer->cur_line);
                        /* move to the previous hard line */
       t@@ -916,34 +936,11 @@ cursor_up(void) {
                                buffer->cur_line--;
                                cur_line = ledit_get_line(buffer, buffer->cur_line);
                                int maxlines = pango_layout_get_line_count(cur_line->layout);
       -                        PangoLayoutLine *prevline =
       -                            pango_layout_get_line_readonly(cur_line->layout, maxlines - 1);
       -                        if (prevline->resolved_dir == PANGO_DIRECTION_RTL) {
       -                                PangoRectangle rect;
       -                                pango_layout_line_get_extents(prevline, NULL, &rect);
       -                                x -= (cur_line->w * PANGO_SCALE - rect.width);
       -                        }
       -                        pango_layout_line_x_to_index(
       -                            prevline, x, &buffer->cur_index, &trailing
       -                        );
       -                        /* FIXME: also in visual? */
       -                        if (state.mode == INSERT)
       -                                buffer->cur_index += trailing;
       +                        ledit_x_softline_to_pos(cur_line, x, maxlines - 1, &buffer->cur_index);
                        }
                } else {
                        /* move to the previous soft line */
       -                PangoLayoutLine *prevline =
       -                    pango_layout_get_line_readonly(cur_line->layout, lineno - 1);
       -                if (prevline->resolved_dir == PANGO_DIRECTION_RTL) {
       -                        PangoRectangle rect;
       -                        pango_layout_line_get_extents(prevline, NULL, &rect);
       -                        x -= (cur_line->w * PANGO_SCALE - rect.width);
       -                }
       -                pango_layout_line_x_to_index(
       -                    prevline, x, &buffer->cur_index, &trailing
       -                );
       -                if (state.mode == INSERT)
       -                        buffer->cur_index += trailing;
       +                ledit_x_softline_to_pos(cur_line, x, lineno - 1, &buffer->cur_index);
                }
                ledit_set_line_cursor_attrs(buffer, buffer->cur_line, buffer->cur_index);
        }
       t@@ -955,57 +952,66 @@ cursor_to_beginning(void) {
        }
        
        static struct key keys_en[] = {
       -        {NULL, XK_BackSpace, INSERT, KEY_ANY, &backspace},
       -        {NULL, XK_Left, INSERT|NORMAL, KEY_ANY, &cursor_left},
       -        {NULL, XK_Right, INSERT|NORMAL, KEY_ANY, &cursor_right},
       -        {NULL, XK_Up, INSERT|NORMAL, KEY_ANY, &cursor_up},
       -        {NULL, XK_Down, INSERT|NORMAL, KEY_ANY, &cursor_down},
       -        {NULL, XK_Return, INSERT, KEY_ANY, &return_key},
       -        {NULL, XK_Delete, INSERT, KEY_ANY, &delete_key},
       -        {NULL, XK_Escape, INSERT, KEY_ANY, &escape_key},
       -        {"i",  0, NORMAL, KEY_ANY, &enter_insert},
       -        {"h",  0, NORMAL, KEY_MOTION | KEY_NUMBERALLOWED, &cursor_left},
       -        {"l",  0, NORMAL, KEY_MOTION | KEY_NUMBERALLOWED, &cursor_right},
       -        {"j",  0, NORMAL, KEY_MOTION | KEY_NUMBERALLOWED, &cursor_down},
       -        {"k",  0, NORMAL, KEY_MOTION | KEY_NUMBERALLOWED, &cursor_up},
       -        {"0",  0, NORMAL, KEY_ANY, &cursor_to_beginning},
       -        {"1",  0, NORMAL, KEY_NUMBER, &push_1},
       -        {"2",  0, NORMAL, KEY_NUMBER, &push_2},
       -        {"x",  0, NORMAL, KEY_NUMBERALLOWED, &key_x}
       +        {NULL, XK_BackSpace, INSERT, KEY_ANY, KEY_ANY, &backspace},
       +        {NULL, XK_Left, INSERT|NORMAL, KEY_ANY, KEY_ANY, &cursor_left},
       +        {NULL, XK_Right, INSERT|NORMAL, KEY_ANY, KEY_ANY, &cursor_right},
       +        {NULL, XK_Up, INSERT|NORMAL, KEY_ANY, KEY_ANY, &cursor_up},
       +        {NULL, XK_Down, INSERT|NORMAL, KEY_ANY, KEY_ANY, &cursor_down},
       +        {NULL, XK_Return, INSERT, KEY_ANY, KEY_ANY, &return_key},
       +        {NULL, XK_Delete, INSERT, KEY_ANY, KEY_ANY, &delete_key},
       +        {NULL, XK_Escape, INSERT, KEY_ANY, KEY_ANY, &escape_key},
       +        {"i",  0, NORMAL, KEY_ANY, KEY_ANY, &enter_insert},
       +        {"h",  0, NORMAL, KEY_ANY, KEY_MOTION | KEY_NUMBERALLOWED, &cursor_left},
       +        {"l",  0, NORMAL, KEY_ANY, KEY_MOTION | KEY_NUMBERALLOWED, &cursor_right},
       +        {"j",  0, NORMAL, KEY_ANY, KEY_MOTION | KEY_NUMBERALLOWED, &cursor_down},
       +        {"k",  0, NORMAL, KEY_ANY, KEY_MOTION | KEY_NUMBERALLOWED, &cursor_up},
       +        {"0",  0, NORMAL, ~KEY_NUMBER, KEY_ANY, &cursor_to_beginning},
       +        {"0",  0, NORMAL, KEY_NUMBER, KEY_NUMBER, &push_0},
       +        {"1",  0, NORMAL, KEY_ANY, KEY_NUMBER, &push_1},
       +        {"2",  0, NORMAL, KEY_ANY, KEY_NUMBER, &push_2},
       +        {"3",  0, NORMAL, KEY_ANY, KEY_NUMBER, &push_3},
       +        {"4",  0, NORMAL, KEY_ANY, KEY_NUMBER, &push_4},
       +        {"5",  0, NORMAL, KEY_ANY, KEY_NUMBER, &push_5},
       +        {"6",  0, NORMAL, KEY_ANY, KEY_NUMBER, &push_6},
       +        {"7",  0, NORMAL, KEY_ANY, KEY_NUMBER, &push_7},
       +        {"8",  0, NORMAL, KEY_ANY, KEY_NUMBER, &push_8},
       +        {"9",  0, NORMAL, KEY_ANY, KEY_NUMBER, &push_9},
       +        {"x",  0, NORMAL, KEY_ANY, KEY_NUMBERALLOWED, &key_x},
       +        {"d",  0, NORMAL, KEY_ANY, KEY_MOTION|KEY_NUMBERALLOWED, &key_d}
        };
        
        static struct key keys_ur[] = {
       -        {NULL, XK_BackSpace, INSERT, KEY_ANY, &backspace},
       -        {NULL, XK_Left, INSERT|NORMAL, KEY_ANY, &cursor_left},
       -        {NULL, XK_Right, INSERT|NORMAL, KEY_ANY, &cursor_right},
       -        {NULL, XK_Up, INSERT|NORMAL, KEY_ANY, &cursor_up},
       -        {NULL, XK_Down, INSERT|NORMAL, KEY_ANY, &cursor_down},
       -        {NULL, XK_Return, INSERT, KEY_ANY, &return_key},
       -        {NULL, XK_Delete, INSERT, KEY_ANY, &delete_key},
       -        {NULL, XK_Escape, INSERT, KEY_ANY, &escape_key},
       -        {"ی",  0, NORMAL, KEY_ANY, &enter_insert},
       -        {"ح",  0, NORMAL, KEY_MOTION | KEY_NUMBERALLOWED, &cursor_left},
       -        {"ل",  0, NORMAL, KEY_MOTION | KEY_NUMBERALLOWED, &cursor_right},
       -        {"ج",  0, NORMAL, KEY_MOTION | KEY_NUMBERALLOWED, &cursor_down},
       -        {"ک",  0, NORMAL, KEY_MOTION | KEY_NUMBERALLOWED, &cursor_up},
       -        {"0",  0, NORMAL, KEY_ANY, &cursor_to_beginning}
       +        {NULL, XK_BackSpace, INSERT, KEY_ANY, KEY_ANY, &backspace},
       +        {NULL, XK_Left, INSERT|NORMAL, KEY_ANY, KEY_ANY, &cursor_left},
       +        {NULL, XK_Right, INSERT|NORMAL, KEY_ANY, KEY_ANY, &cursor_right},
       +        {NULL, XK_Up, INSERT|NORMAL, KEY_ANY, KEY_ANY, &cursor_up},
       +        {NULL, XK_Down, INSERT|NORMAL, KEY_ANY, KEY_ANY, &cursor_down},
       +        {NULL, XK_Return, INSERT, KEY_ANY, KEY_ANY, &return_key},
       +        {NULL, XK_Delete, INSERT, KEY_ANY, KEY_ANY, &delete_key},
       +        {NULL, XK_Escape, INSERT, KEY_ANY, KEY_ANY, &escape_key},
       +        {"ی",  0, NORMAL, KEY_ANY, KEY_ANY, &enter_insert},
       +        {"ح",  0, NORMAL, KEY_ANY, KEY_MOTION | KEY_NUMBERALLOWED, &cursor_left},
       +        {"ل",  0, NORMAL, KEY_ANY, KEY_MOTION | KEY_NUMBERALLOWED, &cursor_right},
       +        {"ج",  0, NORMAL, KEY_ANY, KEY_MOTION | KEY_NUMBERALLOWED, &cursor_down},
       +        {"ک",  0, NORMAL, KEY_ANY, KEY_MOTION | KEY_NUMBERALLOWED, &cursor_up},
       +        {"0",  0, NORMAL, KEY_ANY, KEY_ANY, &cursor_to_beginning}
        };
        
        static struct key keys_hi[] = {
       -        {NULL, XK_BackSpace, INSERT, KEY_ANY, &backspace},
       -        {NULL, XK_Left, INSERT|NORMAL, KEY_ANY, &cursor_left},
       -        {NULL, XK_Right, INSERT|NORMAL, KEY_ANY, &cursor_right},
       -        {NULL, XK_Up, INSERT|NORMAL, KEY_ANY, &cursor_up},
       -        {NULL, XK_Down, INSERT|NORMAL, KEY_ANY, &cursor_down},
       -        {NULL, XK_Return, INSERT, KEY_ANY, &return_key},
       -        {NULL, XK_Delete, INSERT, KEY_ANY, &delete_key},
       -        {NULL, XK_Escape, INSERT, KEY_ANY, &escape_key},
       -        {"ि",  0, NORMAL, KEY_ANY, &enter_insert},
       -        {"ह",  0, NORMAL, KEY_MOTION | KEY_NUMBERALLOWED, &cursor_left},
       -        {"ल",  0, NORMAL, KEY_MOTION | KEY_NUMBERALLOWED, &cursor_right},
       -        {"ज",  0, NORMAL, KEY_MOTION | KEY_NUMBERALLOWED, &cursor_down},
       -        {"क",  0, NORMAL, KEY_MOTION | KEY_NUMBERALLOWED, &cursor_up},
       -        {"0",  0, NORMAL, KEY_ANY, &cursor_to_beginning}
       +        {NULL, XK_BackSpace, INSERT, KEY_ANY, KEY_ANY, &backspace},
       +        {NULL, XK_Left, INSERT|NORMAL, KEY_ANY, KEY_ANY, &cursor_left},
       +        {NULL, XK_Right, INSERT|NORMAL, KEY_ANY, KEY_ANY, &cursor_right},
       +        {NULL, XK_Up, INSERT|NORMAL, KEY_ANY, KEY_ANY, &cursor_up},
       +        {NULL, XK_Down, INSERT|NORMAL, KEY_ANY, KEY_ANY, &cursor_down},
       +        {NULL, XK_Return, INSERT, KEY_ANY, KEY_ANY, &return_key},
       +        {NULL, XK_Delete, INSERT, KEY_ANY, KEY_ANY, &delete_key},
       +        {NULL, XK_Escape, INSERT, KEY_ANY, KEY_ANY, &escape_key},
       +        {"ि",  0, NORMAL, KEY_ANY, KEY_ANY, &enter_insert},
       +        {"ह",  0, NORMAL, KEY_ANY, KEY_MOTION | KEY_NUMBERALLOWED, &cursor_left},
       +        {"ल",  0, NORMAL, KEY_ANY, KEY_MOTION | KEY_NUMBERALLOWED, &cursor_right},
       +        {"ज",  0, NORMAL, KEY_ANY, KEY_MOTION | KEY_NUMBERALLOWED, &cursor_down},
       +        {"क",  0, NORMAL, KEY_ANY, KEY_MOTION | KEY_NUMBERALLOWED, &cursor_up},
       +        {"0",  0, NORMAL, KEY_ANY, KEY_ANY, &cursor_to_beginning}
        };
        
        #define LENGTH(X) (sizeof(X) / sizeof(X[0]))
       t@@ -1044,10 +1050,12 @@ key_press(XEvent event) {
                    state.xic, &event.xkey, buf, sizeof(buf), &sym, NULL
                );
                int found = 0;
       +        struct key_stack_elem *e = peek_key_stack();
                for (int i = 0; i < cur_keys->num_keys; i++) {
                        if (cur_keys->keys[i].text) {
                                if (n > 0 &&
                                    (cur_keys->keys[i].modes & state.mode) &&
       +                            (!e || (e->key & cur_keys->keys[i].prev_keys)) &&
                                     !strncmp(cur_keys->keys[i].text, buf, n)) {
                                        cur_keys->keys[i].func();
                                        found = 1;
       t@@ -1057,6 +1065,8 @@ key_press(XEvent event) {
                                cur_keys->keys[i].func();
                                found = 1;
                        }
       +                if (found)
       +                        break;
                }
                if (state.mode == INSERT && !found && n > 0) {
                        ledit_insert_text(