URI: 
       tImplement word movement commands - 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 335e5d61cc5d4876fb7240543fb55afb55f00cfb
   DIR parent 657c25540c4b349068c4c24f82b6b5bb1f94c459
  HTML Author: lumidify <nobody@lumidify.org>
       Date:   Fri,  5 Nov 2021 19:06:35 +0100
       
       Implement word movement commands
       
       Diffstat:
         M buffer.c                            |     284 +++++++++++++++++++++++++++++++
         M buffer.h                            |       9 +++++++++
         M keys_basic.c                        |      56 +++++++++++++++++++++++++++++++
         M keys_basic_config.h                 |      12 ++++++++++++
       
       4 files changed, 361 insertions(+), 0 deletions(-)
       ---
   DIR diff --git a/buffer.c b/buffer.c
       t@@ -866,6 +866,8 @@ ledit_buffer_copy_text_to_txtbuf(
        
        int
        ledit_line_prev_utf8(ledit_line *line, int index) {
       +        if (index <= 0)
       +                return 0;
                int i = index - 1;
                /* find valid utf8 char - this probably needs to be improved */
                /* FIXME: don't go off end or beginning */
       t@@ -876,12 +878,294 @@ ledit_line_prev_utf8(ledit_line *line, int index) {
        
        int
        ledit_line_next_utf8(ledit_line *line, int index) {
       +        if (index >= line->len)
       +                return line->len;
                int i = index + 1;
                while (i < line->len && ((LINE_CHAR(line, i) & 0xC0) == 0x80))
                        i++;
                return i;
        }
        
       +/* Warning: this is very inefficient! */
       +/* FIXME: at least attempt to be more efficient by starting from the beginning
       +   or end based on approximately where in the line the byte is */
       +static int
       +line_byte_to_char(ledit_line *line, int byte) {
       +        int c = 0;
       +        int i = 0;
       +        int b = byte > line->len ? line->len : byte; /* maybe not necessary */
       +        while (i < b) {
       +                c++;
       +                i = ledit_line_next_utf8(line, i);
       +        }
       +        return c;
       +}
       +
       +static int
       +line_next_word(ledit_line *line, int byte, int char_index, int wrapped_line, int *char_ret, int *real_byte_ret) {
       +        int c, nattrs;
       +        if (char_index >= 0)
       +                c = char_index;
       +        else
       +                c = line_byte_to_char(line, byte);
       +        int cur_byte = wrapped_line ? byte : ledit_line_next_utf8(line, byte);
       +        const PangoLogAttr *attrs =
       +            pango_layout_get_log_attrs_readonly(line->layout, &nattrs);
       +        for (int i = wrapped_line ? c : c + 1; i < nattrs; i++) {
       +                if (attrs[i].is_word_start) {
       +                        if (char_ret)
       +                                *char_ret = i;
       +                        if (real_byte_ret)
       +                                *real_byte_ret = cur_byte;
       +                        return cur_byte;
       +                }
       +                cur_byte = ledit_line_next_utf8(line, cur_byte);
       +        }
       +        return -1;
       +}
       +
       +static int
       +line_prev_word(ledit_line *line, int byte, int char_index, int *char_ret) {
       +        int c, nattrs;
       +        if (char_index >= 0)
       +                c = char_index;
       +        else
       +                c = line_byte_to_char(line, byte);
       +        int cur_byte = ledit_line_prev_utf8(line, byte);
       +        const PangoLogAttr *attrs =
       +            pango_layout_get_log_attrs_readonly(line->layout, &nattrs);
       +        if (c > nattrs)
       +                return -1;
       +        for (int i = c - 1; i >= 0; i--) {
       +                if (attrs[i].is_word_start) {
       +                        if (char_ret)
       +                                *char_ret = i;
       +                        return cur_byte;
       +                }
       +                cur_byte = ledit_line_prev_utf8(line, cur_byte);
       +        }
       +        return -1;
       +}
       +
       +static int
       +line_prev_bigword(ledit_line *line, int byte, int char_index, int *char_ret) {
       +        int c, nattrs;
       +        if (char_index >= 0)
       +                c = char_index;
       +        else
       +                c = line_byte_to_char(line, byte);
       +        int cur_byte = ledit_line_prev_utf8(line, byte);
       +        const PangoLogAttr *attrs =
       +            pango_layout_get_log_attrs_readonly(line->layout, &nattrs);
       +        int next_cursorb = byte;
       +        int next_cursorc = c;
       +        int found_word = 0;
       +        for (int i = c - 1; i >= 0; i--) {
       +                if (!found_word && !attrs[i].is_white) {
       +                        found_word = 1;
       +                } else if (found_word && attrs[i].is_white) {
       +                        if (char_ret)
       +                                *char_ret = next_cursorc;
       +                        return next_cursorb;
       +                }
       +                if (found_word && c == 0) {
       +                        if (char_ret)
       +                                *char_ret = 0;
       +                        return 0;
       +                }
       +                if (attrs[i].is_cursor_position) {
       +                        next_cursorc = i;
       +                        next_cursorb = cur_byte;
       +                }
       +                cur_byte = ledit_line_prev_utf8(line, cur_byte);
       +        }
       +        return -1;
       +}
       +
       +int
       +line_next_bigword_end(ledit_line *line, int byte, int char_index, int wrapped_line, int *char_ret, int *real_byte_ret) {
       +        int c, nattrs;
       +        if (char_index >= 0)
       +                c = char_index;
       +        else
       +                c = line_byte_to_char(line, byte);
       +        const PangoLogAttr *attrs =
       +            pango_layout_get_log_attrs_readonly(line->layout, &nattrs);
       +        int last_cursorb, last_cursorc;
       +        if (wrapped_line) {
       +                last_cursorb = byte;
       +                last_cursorc = c;
       +        } else {
       +                last_cursorb = -1;
       +                last_cursorc = -1;
       +        }
       +        int found_word = 0;
       +        int cur_byte = byte;
       +        for (int i = c; i < nattrs; i++) {
       +                if (last_cursorb != -1 && !found_word && !attrs[i].is_white) {
       +                        found_word = 1;
       +                } else if (found_word && attrs[i].is_white) {
       +                        if (char_ret)
       +                                *char_ret = last_cursorc;
       +                        if (real_byte_ret)
       +                                *real_byte_ret = cur_byte;
       +                        return last_cursorb;
       +                }
       +                if (attrs[i].is_cursor_position) {
       +                        last_cursorc = i;
       +                        last_cursorb = cur_byte;
       +                }
       +                cur_byte = ledit_line_next_utf8(line, cur_byte);
       +        }
       +        return -1;
       +}
       +
       +static int
       +line_next_word_end(ledit_line *line, int byte, int char_index, int wrapped_line, int *char_ret, int *real_byte_ret) {
       +        int c, nattrs;
       +        if (char_index >= 0)
       +                c = char_index;
       +        else
       +                c = line_byte_to_char(line, byte);
       +        int cur_byte = ledit_line_next_utf8(line, byte);
       +        const PangoLogAttr *attrs =
       +            pango_layout_get_log_attrs_readonly(line->layout, &nattrs);
       +        int last_cursorb, last_cursorc;
       +        if (wrapped_line) {
       +                last_cursorb = byte;
       +                last_cursorc = c;
       +        } else {
       +                last_cursorb = -1;
       +                last_cursorc = -1;
       +        }
       +        for (int i = c + 1; i < nattrs; i++) {
       +                if (last_cursorb != -1 && attrs[i].is_word_end) {
       +                        if (char_ret)
       +                                *char_ret = last_cursorc;
       +                        if (real_byte_ret)
       +                                *real_byte_ret = cur_byte;
       +                        return last_cursorb;
       +                }
       +                if (attrs[i].is_cursor_position) {
       +                        last_cursorc = i;
       +                        last_cursorb = cur_byte;
       +                }
       +                cur_byte = ledit_line_next_utf8(line, cur_byte);
       +        }
       +        return -1;
       +}
       +
       +static int
       +line_next_bigword(ledit_line *line, int byte, int char_index, int wrapped_line, int *char_ret, int *real_byte_ret) {
       +        int c, nattrs;
       +        if (char_index >= 0)
       +                c = char_index;
       +        else
       +                c = line_byte_to_char(line, byte);
       +        int cur_byte = byte;
       +        const PangoLogAttr *attrs =
       +            pango_layout_get_log_attrs_readonly(line->layout, &nattrs);
       +        int found_ws = wrapped_line;
       +        for (int i = c; i < nattrs; i++) {
       +                if (!found_ws && attrs[i].is_white) {
       +                        found_ws = 1;
       +                } else if (found_ws && !attrs[i].is_white) {
       +                        if (char_ret)
       +                                *char_ret = i;
       +                        if (real_byte_ret)
       +                                *real_byte_ret = cur_byte;
       +                        return cur_byte;
       +                }
       +                cur_byte = ledit_line_next_utf8(line, cur_byte);
       +        }
       +        return -1;
       +}
       +
       +/* FIXME: document that word and bigword are a bit weird because word uses unicode semantics */
       +
       +#define GEN_NEXT_WORD(name, func)                                                                               \
       +void                                                                                                            \
       +ledit_buffer_next_##name(                                                                                       \
       +    ledit_buffer *buffer,                                                                                       \
       +    int line, int byte, int num_repeat,                                                                         \
       +    int *line_ret, int *byte_ret, int *real_byte_ret) {                                                         \
       +        int cur_line = line;                                                                                    \
       +        int cur_byte = byte;                                                                                    \
       +        int cur_char = -1;                                                                                      \
       +        int real_byte = -1;                                                                                     \
       +        int wrapped_line;                                                                                       \
       +        ledit_line *ll = ledit_buffer_get_line(buffer, cur_line);                                               \
       +        for (int i = 0; i < num_repeat; i++) {                                                                  \
       +                wrapped_line = 0;                                                                               \
       +                while ((cur_byte = func(ll, cur_byte, cur_char, wrapped_line, &cur_char, &real_byte)) == -1 &&  \
       +                       cur_line < buffer->lines_num - 1) {                                                      \
       +                        cur_line++;                                                                             \
       +                        ll = ledit_buffer_get_line(buffer, cur_line);                                           \
       +                        cur_byte = 0;                                                                           \
       +                        wrapped_line = 1;                                                                       \
       +                }                                                                                               \
       +                if (cur_byte == -1 && cur_line == buffer->lines_num - 1)                                        \
       +                        break;                                                                                  \
       +        }                                                                                                       \
       +        if (cur_byte == -1) {                                                                                   \
       +                *line_ret = buffer->lines_num - 1;                                                              \
       +                *byte_ret = ledit_buffer_get_legal_normal_pos(buffer, buffer->lines_num - 1, ll->len);          \
       +                *real_byte_ret = ll->len;                                                                       \
       +        } else {                                                                                                \
       +                *line_ret = cur_line;                                                                           \
       +                *byte_ret = cur_byte;                                                                           \
       +                *real_byte_ret = real_byte;                                                                     \
       +        }                                                                                                       \
       +}
       +
       +#define GEN_PREV_WORD(name, func)                                                                         \
       +void                                                                                                      \
       +ledit_buffer_prev_##name(                                                                                 \
       +    ledit_buffer *buffer,                                                                                 \
       +    int line, int byte, int num_repeat,                                                                   \
       +    int *line_ret, int *byte_ret, int *real_byte_ret) {                                                   \
       +        int cur_line = line;                                                                              \
       +        int cur_byte = byte;                                                                              \
       +        int cur_char = -1;                                                                                \
       +        ledit_line *ll = ledit_buffer_get_line(buffer, cur_line);                                         \
       +        for (int i = 0; i < num_repeat; i++) {                                                            \
       +                while ((cur_byte = func(ll, cur_byte, cur_char, &cur_char)) == -1 && cur_line > 0) {      \
       +                        cur_line--;                                                                       \
       +                        ll = ledit_buffer_get_line(buffer, cur_line);                                     \
       +                        cur_byte = ll->len;                                                               \
       +                }                                                                                         \
       +                if (cur_byte == -1 && cur_line == 0)                                                      \
       +                        break;                                                                            \
       +        }                                                                                                 \
       +        if (cur_byte == -1) {                                                                             \
       +                *line_ret = 0;                                                                            \
       +                *byte_ret = 0;                                                                            \
       +                *real_byte_ret = 0;                                                                       \
       +        } else {                                                                                          \
       +                *line_ret = cur_line;                                                                     \
       +                *byte_ret = cur_byte;                                                                     \
       +                *real_byte_ret = cur_byte;                                                                \
       +        }                                                                                                 \
       +}
       +
       +GEN_NEXT_WORD(word, line_next_word)
       +GEN_NEXT_WORD(word_end, line_next_word_end)
       +GEN_NEXT_WORD(bigword, line_next_bigword)
       +GEN_NEXT_WORD(bigword_end, line_next_bigword_end)
       +GEN_PREV_WORD(word, line_prev_word)
       +GEN_PREV_WORD(bigword, line_prev_bigword)
       +
       +/* FIXME: implement */
       +/*
       +int
       +ledit_line_nearest_cursor_pos(ledit_line *line, int byte) {
       +}
       +
       +void
       +ledit_line_word_boundaries(ledit_line *line, int byte, int *start_ret, int *end_ret) {
       +}
       +*/
       +
        /* FIXME: no idea why this exists */
        /*
        static void
   DIR diff --git a/buffer.h b/buffer.h
       t@@ -57,6 +57,15 @@ void ledit_pos_to_x_softline(ledit_line *line, int pos, int *x_ret, int *softlin
        void ledit_x_softline_to_pos(ledit_line *line, int x, int softline, int *pos_ret);
        int ledit_line_next_utf8(ledit_line *line, int index);
        int ledit_line_prev_utf8(ledit_line *line, int index);
       +int ledit_line_byte_to_char(ledit_line *line, int byte);
       +
       +void ledit_buffer_next_word(ledit_buffer *buffer, int line, int byte, int num_repeat, int *line_ret, int *byte_ret, int *real_byte_ret);
       +void ledit_buffer_next_word_end(ledit_buffer *buffer, int line, int byte, int num_repeat, int *line_ret, int *byte_ret, int *real_byte_ret);
       +void ledit_buffer_next_bigword(ledit_buffer *buffer, int line, int byte, int num_repeat, int *line_ret, int *byte_ret, int *real_byte_ret);
       +void ledit_buffer_next_bigword_end(ledit_buffer *buffer, int line, int byte, int num_repeat, int *line_ret, int *byte_ret, int *real_byte_ret);
       +void ledit_buffer_prev_word(ledit_buffer *buffer, int line, int byte, int num_repeat, int *line_ret, int *byte_ret, int *real_byte_ret);
       +void ledit_buffer_prev_bigword(ledit_buffer *buffer, int line, int byte, int num_repeat, int *line_ret, int *byte_ret, int *real_byte_ret);
       +
        size_t ledit_buffer_textlen(ledit_buffer *buffer, int line1, int byte1, int line2, int byte2);
        void ledit_buffer_copy_text(ledit_buffer *buffer, char *dst, int line1, int byte1, int line2, int byte2);
        void ledit_buffer_copy_text_to_txtbuf(
   DIR diff --git a/keys_basic.c b/keys_basic.c
       t@@ -1040,6 +1040,62 @@ move_to_eol(ledit_buffer *buffer, char *text, int len) {
                return (struct action){ACTION_NONE, NULL};
        }
        
       +#define GEN_WORD_MOVEMENT(name, func)                                            \
       +static struct action                                                             \
       +name(ledit_buffer *buffer, char *text, int len) {                                \
       +        (void)text;                                                              \
       +        (void)len;                                                               \
       +        int num = 1;                                                             \
       +        struct key_stack_elem *e = pop_key_stack();                              \
       +        if (e != NULL) {                                                         \
       +                if (e->key & KEY_NUMBER) {                                       \
       +                        num = e->count > 0 ? e->count : 1;                       \
       +                        e = pop_key_stack();                                     \
       +                }                                                                \
       +                if (e != NULL)                                                   \
       +                        num *= (e->count > 0 ? e->count : 1);                    \
       +        }                                                                        \
       +        int new_line, new_index, new_real_index;                                 \
       +        func(                                                                    \
       +            buffer,                                                              \
       +            buffer->cur_line, buffer->cur_index, num,                            \
       +            &new_line, &new_index, &new_real_index                               \
       +        );                                                                       \
       +        if (e != NULL && e->motion_cb != NULL) {                                 \
       +                e->motion_cb(buffer, new_line, new_real_index, KEY_MOTION_CHAR); \
       +        } else {                                                                 \
       +                if (buffer->common->mode == VISUAL) {                            \
       +                        ledit_buffer_set_selection(                              \
       +                            buffer,                                              \
       +                            buffer->sel.line1, buffer->sel.byte1,                \
       +                            new_line, new_real_index                             \
       +                        );                                                       \
       +                        buffer->cur_line = new_line;                             \
       +                        buffer->cur_index = new_real_index;                      \
       +                } else {                                                         \
       +                        if (new_line != buffer->cur_line)                        \
       +                                ledit_buffer_wipe_line_cursor_attrs(             \
       +                                    buffer, buffer->cur_line                     \
       +                                );                                               \
       +                        buffer->cur_line = new_line;                             \
       +                        buffer->cur_index = new_index;                           \
       +                        ledit_buffer_set_line_cursor_attrs(                      \
       +                            buffer, buffer->cur_line, buffer->cur_index          \
       +                        );                                                       \
       +                }                                                                \
       +                discard_repetition_stack();                                      \
       +        }                                                                        \
       +        clear_key_stack();                                                       \
       +        return (struct action){ACTION_NONE, NULL};                               \
       +}
       +
       +GEN_WORD_MOVEMENT(next_word, ledit_buffer_next_word)
       +GEN_WORD_MOVEMENT(next_word_end, ledit_buffer_next_word_end)
       +GEN_WORD_MOVEMENT(next_bigword, ledit_buffer_next_bigword)
       +GEN_WORD_MOVEMENT(next_bigword_end, ledit_buffer_next_bigword_end)
       +GEN_WORD_MOVEMENT(prev_word, ledit_buffer_prev_word)
       +GEN_WORD_MOVEMENT(prev_bigword, ledit_buffer_prev_bigword)
       +
        static void
        move_cursor_left_right(ledit_buffer *buffer, int dir, int allow_illegal_index) {
                int num = 1;
   DIR diff --git a/keys_basic_config.h b/keys_basic_config.h
       t@@ -70,6 +70,12 @@ static struct action change(ledit_buffer *buffer, char *text, int len);
        static struct action move_to_eol(ledit_buffer *buffer, char *text, int len);
        static struct action mark_line(ledit_buffer *buffer, char *text, int len);
        static struct action jump_to_mark(ledit_buffer *buffer, char *text, int len);
       +static struct action next_word(ledit_buffer *buffer, char *text, int len);
       +static struct action next_word_end(ledit_buffer *buffer, char *text, int len);
       +static struct action next_bigword(ledit_buffer *buffer, char *text, int len);
       +static struct action next_bigword_end(ledit_buffer *buffer, char *text, int len);
       +static struct action prev_word(ledit_buffer *buffer, char *text, int len);
       +static struct action prev_bigword(ledit_buffer *buffer, char *text, int len);
        
        /* FIXME: maybe sort these and use binary search
           -> but that would mess with the catch-all keys */
       t@@ -128,6 +134,12 @@ static struct key keys_en[] = {
                {"d",  ControlMask, 0, NORMAL, KEY_ANY, KEY_NUMBERALLOWED, &scroll_lines_down},
                {"u",  ControlMask, 0, NORMAL, KEY_ANY, KEY_NUMBERALLOWED, &scroll_lines_up},
                {"$",  0, 0, NORMAL|VISUAL, KEY_ANY, KEY_NUMBERALLOWED, &move_to_eol},
       +        {"w",  0, 0, NORMAL|VISUAL, KEY_ANY, KEY_NUMBERALLOWED, &next_word},
       +        {"e",  0, 0, NORMAL|VISUAL, KEY_ANY, KEY_NUMBERALLOWED, &next_word_end},
       +        {"W",  0, 0, NORMAL|VISUAL, KEY_ANY, KEY_NUMBERALLOWED, &next_bigword},
       +        {"E",  0, 0, NORMAL|VISUAL, KEY_ANY, KEY_NUMBERALLOWED, &next_bigword_end},
       +        {"b",  0, 0, NORMAL|VISUAL, KEY_ANY, KEY_NUMBERALLOWED, &prev_word},
       +        {"B",  0, 0, NORMAL|VISUAL, KEY_ANY, KEY_NUMBERALLOWED, &prev_bigword},
                {"G",  0, 0, NORMAL, KEY_ANY, KEY_NUMBERALLOWED, &move_to_line},
                {"p",  0, 0, NORMAL, KEY_ANY, KEY_ANY, &paste_normal},
                {"P",  0, 0, NORMAL, KEY_ANY, KEY_ANY, &paste_normal_backwards},