URI: 
       tSomewhat properly implement substitution - 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 0498ed82f507017d8a5c6d83da08c66b2520bf95
   DIR parent 3572b3d7dd300642a94067f72f30fc83c8651e39
  HTML Author: lumidify <nobody@lumidify.org>
       Date:   Thu,  9 Dec 2021 17:34:35 +0100
       
       Somewhat properly implement substitution
       
       Diffstat:
         M buffer.c                            |      29 +++++++++++++++++++++++++++++
         M buffer.h                            |      11 ++++++++++-
         M keys_basic.c                        |      61 ++++++++++++++++++-------------
         M keys_command.c                      |     242 ++++++++++++++++++++++---------
         M view.c                              |       8 ++++++--
       
       5 files changed, 251 insertions(+), 100 deletions(-)
       ---
   DIR diff --git a/buffer.c b/buffer.c
       t@@ -202,6 +202,35 @@ buffer_insert_mark(ledit_buffer *buffer, char *mark, size_t len, size_t line, si
                marklist->len++;
        }
        
       +/* FIXME: check that byte is actually at grapheme boundary
       +   (difficult because that can't currently be done by the buffer) */
       +int
       +buffer_get_mark(ledit_buffer *buffer, char *mark, size_t len, size_t *line_ret, size_t *byte_ret) {
       +        ledit_buffer_marklist *marklist = buffer->marklist;
       +        int ret = 1;
       +        for (size_t i = 0; i < marklist->len; i++) {
       +                if (!strncmp(mark, marklist->marks[i].text, len)) {
       +                        ledit_line *ll;
       +                        ledit_buffer_mark *m = &marklist->marks[i];
       +                        if (m->line >= buffer->lines_num) {
       +                                *line_ret = buffer->lines_num - 1;
       +                                ll = buffer_get_line(buffer, *line_ret);
       +                                *byte_ret = ll->len;
       +                        } else {
       +                                *line_ret = m->line;
       +                                ll = buffer_get_line(buffer, m->line);
       +                                if (m->byte >= ll->len)
       +                                        *byte_ret = ll->len;
       +                                else
       +                                        *byte_ret = m->byte;
       +                        }
       +                        ret = 0;
       +                        break;
       +                }
       +        }
       +        return ret;
       +}
       +
        static ledit_buffer_marklist *
        marklist_create(void) {
                ledit_buffer_marklist *marklist = ledit_malloc(sizeof(ledit_buffer_marklist));
   DIR diff --git a/buffer.h b/buffer.h
       t@@ -184,11 +184,20 @@ size_t line_prev_utf8(ledit_line *line, size_t index);
        size_t line_byte_to_char(ledit_line *line, size_t byte);
        
        /*
       - * Insert a mark with key 'mark' at line 'line' and byte 'byte'.
       + * Insert a mark with key 'mark' (which has byte length 'len') at line 'line' and byte 'byte'.
         */
        void buffer_insert_mark(ledit_buffer *buffer, char *mark, size_t len, size_t line, size_t byte);
        
        /*
       + * Retrieve mark 'mark' (with byte length 'len').
       + * The line and byte of the mark are written to 'line_ret' and 'byte_ret'.
       + * These returned positions are always valid positions in the buffer, even
       + * if the buffer has been modified and the mark points to an invalid location.
       + * Returns 0 if the mark was found, 1 otherwise.
       + */
       +int buffer_get_mark(ledit_buffer *buffer, char *mark, size_t len, size_t *line_ret, size_t *byte_ret);
       +
       +/*
         * Perform one undo step.
         * 'mode' should be the current mode of the calling view.
         * 'cur_line' and 'cur_byte' are filled with the new line and cursor
   DIR diff --git a/keys_basic.c b/keys_basic.c
       t@@ -121,6 +121,15 @@ static void change_cb(ledit_view *view, size_t line, size_t char_pos, enum key_t
        static void push_undo_empty_insert(ledit_view *view, size_t line, size_t index, int start_group);
        static void move_half_screen(ledit_view *view, int movement);
        
       +static struct action
       +view_locked_error(ledit_view *view) {
       +        window_show_message(view->window, view->lock_text, -1);
       +        return (struct action){ACTION_NONE, NULL};
       +}
       +
       +#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) {
       t@@ -478,6 +487,7 @@ static struct action
        delete_chars_forwards(ledit_view *view, char *text, size_t len) {
                (void)text;
                (void)len;
       +        CHECK_VIEW_LOCKED(view);
                int num = get_key_repeat();
                if (num == -1) {
                        window_show_message(view->window, "Invalid key", -1);
       t@@ -505,6 +515,7 @@ static struct action
        delete_chars_backwards(ledit_view *view, char *text, size_t len) {
                (void)text;
                (void)len;
       +        CHECK_VIEW_LOCKED(view);
                int num = get_key_repeat();
                if (num == -1) {
                        window_show_message(view->window, "Invalid key", -1);
       t@@ -543,6 +554,7 @@ push_undo_empty_insert(ledit_view *view, size_t line, size_t index, int start_gr
        
        static struct action
        append_line_above(ledit_view *view, char *text, size_t len) {
       +        CHECK_VIEW_LOCKED(view);
                size_t start, end;
                /* do this here already so the mode group is the same for the newline insertion */
                enter_insert(view, text, len);
       t@@ -558,6 +570,7 @@ append_line_above(ledit_view *view, char *text, size_t len) {
        
        static struct action
        append_line_below(ledit_view *view, char *text, size_t len) {
       +        CHECK_VIEW_LOCKED(view);
                size_t start, end;
                enter_insert(view, text, len);
                view_get_pos_softline_bounds(view, view->cur_line, view->cur_index, &start, &end);
       t@@ -572,6 +585,7 @@ append_line_below(ledit_view *view, char *text, size_t len) {
        
        static struct action
        append_after_cursor(ledit_view *view, char *text, size_t len) {
       +        CHECK_VIEW_LOCKED(view);
                enter_insert(view, text, len);
                /* make cursor jump back to original position on undo */
                push_undo_empty_insert(view, view->cur_line, view->cur_index, 1);
       t@@ -583,6 +597,7 @@ append_after_cursor(ledit_view *view, char *text, size_t len) {
        
        static struct action
        append_after_eol(ledit_view *view, char *text, size_t len) {
       +        CHECK_VIEW_LOCKED(view);
                size_t start, end;
                enter_insert(view, text, len);
                view_get_pos_softline_bounds(view, view->cur_line, view->cur_index, &start, &end);
       t@@ -833,6 +848,7 @@ static struct action
        delete_to_eol(ledit_view *view, char *text, size_t len) {
                (void)text;
                (void)len;
       +        CHECK_VIEW_LOCKED(view);
                if (!key_stack_empty())
                        return err_invalid_key(view);
                size_t start, end;
       t@@ -859,6 +875,7 @@ static struct action
        change_to_eol(ledit_view *view, char *text, size_t len) {
                (void)text;
                (void)len;
       +        CHECK_VIEW_LOCKED(view);
                if (!key_stack_empty())
                        return err_invalid_key(view);
                view_set_mode(view, INSERT);
       t@@ -885,6 +902,7 @@ static struct action
        change(ledit_view *view, char *text, size_t len) {
                (void)text;
                (void)len;
       +        CHECK_VIEW_LOCKED(view);
                motion_callback cb = NULL;
                int num = get_key_repeat_and_motion_cb(&cb);
                if (num == -1)
       t@@ -921,6 +939,7 @@ change(ledit_view *view, char *text, size_t len) {
        
        static void
        change_cb(ledit_view *view, size_t line, size_t char_pos, enum key_type type) {
       +        CHECK_VIEW_LOCKED_NORETURN(view);
                /* set mode first so the deletion is included in the undo group */
                view_set_mode(view, INSERT);
                int line_based = type == KEY_MOTION_LINE ? 1 : 0;
       t@@ -1066,6 +1085,7 @@ static struct action
        delete(ledit_view *view, char *text, size_t len) {
                (void)text;
                (void)len;
       +        CHECK_VIEW_LOCKED(view);
                motion_callback cb = NULL;
                int num = get_key_repeat_and_motion_cb(&cb);
                if (num == -1)
       t@@ -1104,6 +1124,7 @@ delete(ledit_view *view, char *text, size_t len) {
        /* FIXME: should this get number of lines to remove or actual end line? */
        static void
        delete_cb(ledit_view *view, size_t line, size_t char_pos, enum key_type type) {
       +        CHECK_VIEW_LOCKED_NORETURN(view);
                view_wipe_line_cursor_attrs(view, view->cur_line);
                int line_based = type == KEY_MOTION_LINE ? 1 : 0;
                delete_range(
       t@@ -1124,6 +1145,7 @@ static struct action
        paste_normal(ledit_view *view, char *text, size_t len) {
                (void)text;
                (void)len;
       +        CHECK_VIEW_LOCKED(view);
                if (!paste_buffer) {
                        window_show_message(view->window, "Nothing to paste", -1);
                        discard_repetition_stack();
       t@@ -1182,6 +1204,7 @@ static struct action
        paste_normal_backwards(ledit_view *view, char *text, size_t len) {
                (void)text;
                (void)len;
       +        CHECK_VIEW_LOCKED(view);
                if (!paste_buffer) {
                        window_show_message(view->window, "Nothing to paste", -1);
                        discard_repetition_stack();
       t@@ -1347,6 +1370,7 @@ static struct action
        backspace(ledit_view *view, char *text, size_t len) {
                (void)text;
                (void)len;
       +        CHECK_VIEW_LOCKED(view);
                /* FIXME: don't copy to paste buffer on del_sel here; delete entire grapheme */
                if (delete_selection(view)) {
                        /* NOP */
       t@@ -1367,6 +1391,7 @@ static struct action
        delete_key(ledit_view *view, char *text, size_t len) {
                (void)text;
                (void)len;
       +        CHECK_VIEW_LOCKED(view);
                ledit_line *cur_line = buffer_get_line(view->buffer, view->cur_line);
                if (delete_selection(view)) {
                        /* NOP */
       t@@ -1387,6 +1412,7 @@ static struct action
        move_to_eol(ledit_view *view, char *text, size_t len) {
                (void)text;
                (void)len;
       +        CHECK_VIEW_LOCKED(view);
                motion_callback cb;
                int num = get_key_repeat_and_motion_cb(&cb);
                if (num == -1)
       t@@ -1544,6 +1570,7 @@ static struct action
        return_key(ledit_view *view, char *text, size_t len) {
                (void)text;
                (void)len;
       +        CHECK_VIEW_LOCKED(view);
                int start_group = 1;
                if (delete_selection(view))
                        start_group = 0;
       t@@ -1591,6 +1618,7 @@ static struct action
        enter_insert(ledit_view *view, char *text, size_t len) {
                (void)text;
                (void)len;
       +        CHECK_VIEW_LOCKED(view);
                if (view->mode == NORMAL)
                        view_wipe_line_cursor_attrs(view, view->cur_line);
                view_set_mode(view, INSERT);
       t@@ -1663,6 +1691,7 @@ static struct action
        join_lines(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);
       t@@ -1696,6 +1725,7 @@ join_lines(ledit_view *view, char *text, size_t len) {
        
        static struct action
        insert_at_beginning(ledit_view *view, char *text, size_t len) {
       +        CHECK_VIEW_LOCKED(view);
                if (!key_stack_empty())
                        return err_invalid_key(view);
                enter_insert(view, text, len);
       t@@ -1854,39 +1884,15 @@ mark_line_cb(ledit_view *view, char *text, size_t len) {
                return (struct action){ACTION_NONE, NULL};
        }
        
       -/* FIXME: move part of this to buffer.c */
       -/* FIXME: check that byte is actually at grapheme boundary */
        static struct action
        jump_to_mark_cb(ledit_view *view, char *text, size_t len) {
                grab_char_cb = NULL;
       -        ledit_buffer_marklist *marklist = view->buffer->marklist;
                motion_callback cb;
                int num = get_key_repeat_and_motion_cb(&cb);
                if (num > 0)
                        return err_invalid_key(view);
                size_t line = 0, index = 0;
       -        int mark_found = 0;
       -        for (size_t i = 0; i < marklist->len; i++) {
       -                if (!strncmp(text, marklist->marks[i].text, len)) {
       -                        ledit_line *ll;
       -                        ledit_buffer_mark *m = &marklist->marks[i];
       -                        if (m->line >= view->lines_num) {
       -                                line = view->lines_num - 1;
       -                                ll = buffer_get_line(view->buffer, line);
       -                                index = ll->len;
       -                        } else {
       -                                line = m->line;
       -                                ll = buffer_get_line(view->buffer, m->line);
       -                                if (m->byte >= ll->len)
       -                                        index = ll->len;
       -                                else
       -                                        index = m->byte;
       -                        }
       -                        mark_found = 1;
       -                        break;
       -                }
       -        }
       -        if (!mark_found)
       +        if (buffer_get_mark(view->buffer, text, len, &line, &index))
                        return err_invalid_key(view);
                if (view->mode == VISUAL) {
                        view_set_selection(
       t@@ -1969,6 +1975,7 @@ static struct action
        undo(ledit_view *view, char *text, size_t len) {
                (void)text;
                (void)len;
       +        CHECK_VIEW_LOCKED(view);
                view_wipe_selection(view);
                view_wipe_line_cursor_attrs(view, view->cur_line);
                view_undo(view);
       t@@ -1981,6 +1988,7 @@ static struct action
        redo(ledit_view *view, char *text, size_t len) {
                (void)text;
                (void)len;
       +        CHECK_VIEW_LOCKED(view);
                view_wipe_selection(view);
                view_wipe_line_cursor_attrs(view, view->cur_line);
                view_redo(view);
       t@@ -1991,6 +1999,7 @@ redo(ledit_view *view, char *text, size_t len) {
        
        static struct action
        insert_mode_insert_text(ledit_view *view, char *text, size_t len) {
       +        CHECK_VIEW_LOCKED(view);
                /* FIXME: set cur_index when deleting selection */
                delete_selection(view);
                insert_text(view, view->cur_line, view->cur_index, text, len, 0, 0, 0, 0, 0, 0, 1);
       t@@ -2149,6 +2158,7 @@ GEN_MOVE_TO_CHAR(
        
        static struct action
        replace_cb(ledit_view *view, char *text, size_t len) {
       +        CHECK_VIEW_LOCKED(view);
                size_t start_index = view->cur_index;
                /* FIXME: replace with (key repeat) * text instead of just text */
                size_t end_index = view_next_cursor_pos(
       t@@ -2176,6 +2186,7 @@ replace(ledit_view *view, char *text, size_t len) {
                (void)view;
                (void)text;
                (void)len;
       +        CHECK_VIEW_LOCKED(view);
                int num = get_key_repeat();
                if (num != 0)
                        return err_invalid_key(view);
   DIR diff --git a/keys_command.c b/keys_command.c
       t@@ -27,9 +27,20 @@
        #include "keys_command.h"
        #include "keys_command_config.h"
        
       -static char *last_search = NULL;
       -static char *last_replacement = NULL;
       -static int last_replacement_global = 0;
       +static struct {
       +        char *search;
       +        char *replace;
       +        size_t slen;
       +        size_t rlen;
       +        size_t line;
       +        size_t byte;
       +        size_t old_line;
       +        size_t old_byte;
       +        size_t max_line;
       +        int global;
       +        int num;
       +        int start_group; /* only set for the first replacement */
       +} sub_state = {NULL, NULL, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1};
        
        static int
        view_locked_error(ledit_view *view) {
       t@@ -37,7 +48,7 @@ view_locked_error(ledit_view *view) {
                return 0;
        }
        
       -#define CHECK_VIEW_LOCKED if (view->lock_text) return view_locked_error(view)
       +#define CHECK_VIEW_LOCKED(view) if (view->lock_text) return view_locked_error(view)
        
        /* FIXME: history for search and commands */
        
       t@@ -110,13 +121,114 @@ handle_write_quit(ledit_view *view, char *cmd, size_t l1, size_t l2) {
                return 0;
        }
        
       +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);
       +        }
       +}
       +
       +/* returns 1 when match was found, 0 otherwise */
       +static int
       +next_replace_pos(
       +    ledit_view *view,
       +    size_t line, size_t byte, size_t max_line,
       +    size_t *line_ret, size_t *byte_ret) {
       +        size_t start_index = byte;
       +        for (size_t i = line; i <= max_line; i++) {
       +                ledit_line *ll = buffer_get_line(view->buffer, i);
       +                buffer_normalize_line(ll);
       +                char *pos = strstr(ll->text + start_index, sub_state.search);
       +                if (pos != NULL) {
       +                        *line_ret = i;
       +                        *byte_ret = (size_t)(pos - ll->text);
       +                        return 1;
       +                }
       +                start_index = 0;
       +        }
       +        return 0;
       +}
       +
       +/* returns whether keys should continue being captured */
       +static int
       +move_to_next_substitution(ledit_view *view) {
       +        view_wipe_line_cursor_attrs(view, view->cur_line);
       +        if (!next_replace_pos(view, sub_state.line, sub_state.byte, sub_state.max_line, &sub_state.line, &sub_state.byte)) {
       +                view->cur_line = sub_state.line;
       +                view->cur_index = sub_state.byte;
       +                view_set_line_cursor_attrs(view, view->cur_line, view->cur_index);
       +                window_show_message(view->window, "No more matches", -1);
       +                buffer_unlock_all_views(view->buffer);
       +                return 0;
       +        }
       +        view->cur_line = sub_state.line;
       +        view->cur_index = sub_state.byte;
       +        view_set_line_cursor_attrs(view, view->cur_line, view->cur_index);
       +        window_show_message(view->window, "Replace? (y/Y/n/N)", -1);
       +        view_ensure_cursor_shown(view);
       +        return 1;
       +}
       +
       +/* WARNING: sub_state must be set properly! */
       +static void
       +substitute_single(ledit_view *view) {
       +        ledit_range cur_range;
       +        cur_range.line1 = sub_state.old_line;
       +        cur_range.byte1 = sub_state.old_byte;
       +        cur_range.line2 = sub_state.line;
       +        cur_range.byte2 = sub_state.byte;
       +        buffer_delete_with_undo_base(
       +            view->buffer, cur_range,
       +            sub_state.start_group, view->mode,
       +            sub_state.line, sub_state.byte,
       +            sub_state.line, sub_state.byte + sub_state.slen, NULL
       +        );
       +        sub_state.start_group = 0;
       +        cur_range.line1 = sub_state.line;
       +        cur_range.byte1 = sub_state.byte;
       +        buffer_insert_with_undo_base(
       +            view->buffer, cur_range, 0, 0, view->mode,
       +            sub_state.line, sub_state.byte,
       +            sub_state.replace, sub_state.rlen,
       +            NULL, NULL
       +        );
       +        sub_state.num++;
       +}
       +
       +static void
       +substitute_all_remaining(ledit_view *view) {
       +        view_wipe_line_cursor_attrs(view, view->cur_line);
       +        size_t min_line = SIZE_MAX;
       +        while (next_replace_pos(view, sub_state.line, sub_state.byte, sub_state.max_line, &sub_state.line, &sub_state.byte)) {
       +                if (sub_state.line < min_line)
       +                        min_line = sub_state.line;
       +                substitute_single(view);
       +                view->cur_line = sub_state.old_line = sub_state.line;
       +                view->cur_index = sub_state.old_byte = sub_state.byte;
       +                if (!sub_state.global) {
       +                        sub_state.line++;
       +                        sub_state.byte = 0;
       +                } else {
       +                        sub_state.byte += sub_state.rlen;
       +                }
       +        }
       +        if (min_line < view->lines_num)
       +                buffer_recalc_all_views_from_line(view->buffer, min_line);
       +        /* FIXME: show number replaced */
       +        /* 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);
       +}
       +
        static int
        handle_substitute(ledit_view *view, char *cmd, size_t l1, size_t l2) {
       -        (void)view;
       -        (void)cmd;
       -        (void)l1;
       -        (void)l2;
       -        CHECK_VIEW_LOCKED;
       +        CHECK_VIEW_LOCKED(view);
                cmd++; /* remove 's' at beginning */
                size_t len = strlen(cmd);
                if (len == 0) goto error;
       t@@ -146,64 +258,27 @@ handle_substitute(ledit_view *view, char *cmd, size_t l1, size_t l2) {
                        }
                        c++;
                }
       -        free(last_search);
       -        free(last_replacement);
       -        last_search = ledit_strdup(cmd);
       -        last_replacement = ledit_strdup(next);
       -        last_replacement_global = global;
       +        free(sub_state.search);
       +        free(sub_state.replace);
       +        sub_state.search = ledit_strdup(cmd);
       +        sub_state.replace = ledit_strdup(next);
       +        sub_state.slen = strlen(sub_state.search);
       +        sub_state.rlen = strlen(sub_state.replace);
       +        sub_state.global = global;
       +        sub_state.line = l1;
       +        sub_state.byte = 0;
       +        sub_state.old_line = view->cur_line;
       +        sub_state.old_byte = view->cur_index;
       +        sub_state.max_line = l2;
       +        sub_state.num = 0;
       +        sub_state.start_group = 1;
        
                if (confirm) {
                        buffer_lock_all_views_except(view->buffer, view, "Ongoing substitution in other view.");
       -                buffer_unlock_all_views(view->buffer);
       +                view->cur_command_type = CMD_SUBSTITUTE;
       +                return move_to_next_substitution(view);
                } else {
       -                int num = 0;
       -                int start_undo_group = 1;
       -                size_t slen = strlen(last_search);
       -                size_t rlen = strlen(last_replacement);
       -                txtbuf *buf = txtbuf_new(); /* FIXME: don't allocate new every time */
       -                view_wipe_line_cursor_attrs(view, view->cur_line);
       -                size_t min_line = SIZE_MAX;
       -                for (size_t i = l1 - 1; i < l2; i++) {
       -                        ledit_line *ll = buffer_get_line(view->buffer, i);
       -                        buffer_normalize_line(ll);
       -                        char *pos = strstr(ll->text, last_search);
       -                        if (pos != NULL && i < min_line)
       -                                min_line = i;
       -                        while (pos != NULL) {
       -                                size_t index = (size_t)(pos - ll->text);
       -                                ledit_range cur_range;
       -                                cur_range.line1 = view->cur_line;
       -                                cur_range.byte1 = view->cur_line;
       -                                cur_range.line2 = i;
       -                                cur_range.byte2 = index;
       -                                buffer_delete_with_undo_base(
       -                                    view->buffer, cur_range,
       -                                    start_undo_group, view->mode,
       -                                    i, index, i, index + slen, NULL
       -                                );
       -                                view->cur_line = i;
       -                                view->cur_index = index;
       -                                start_undo_group = 0;
       -                                cur_range.line1 = i;
       -                                cur_range.byte1 = index;
       -                                buffer_insert_with_undo_base(
       -                                    view->buffer, cur_range, 0, 0, view->mode,
       -                                    i, index, last_replacement, rlen,
       -                                    NULL, NULL
       -                                );
       -                                num++;
       -                                if (!global) break;
       -                                buffer_normalize_line(ll); /* just in case */
       -                                pos = strstr(ll->text + index + rlen, last_search);
       -                        }
       -                }
       -                buffer_recalc_all_views_from_line(view->buffer, min_line);
       -                /* FIXME: show number replaced */
       -                /* 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);
       -                txtbuf_destroy(buf);
       +                substitute_all_remaining(view);
                }
                return 0;
        error:
       t@@ -312,8 +387,9 @@ parse_range(ledit_view *view, char *cmd, size_t len, char **cmd_ret, size_t *lin
                if ((*l1_valid || *l2_valid) && (l1 == 0 || l2 == 0 || l1 > view->lines_num || l2 > view->lines_num))
                        return 1; /* FIXME: better error messages */
                *cmd_ret = c;
       -        *line1_ret = l1;
       -        *line2_ret = l2;
       +        /* ranges are given 1-indexed by user */
       +        *line1_ret = l1 - 1;
       +        *line2_ret = l2 - 1;
                return 0;
        }
        
       t@@ -328,7 +404,9 @@ handle_cmd(ledit_view *view, char *cmd, size_t len) {
                if (parse_range(view, cmd, len, &c, &l1, &l2, &l1_valid, &l2_valid))
                        return 0;
                int range_given = l1_valid && l2_valid;
       -        /* FIXME: mandatory range */
       +        if (!range_given) {
       +                l1 = l2 = view->cur_line;
       +        }
                for (size_t i = 0; i < LENGTH(cmds); i++) {
                        if (!strncmp(cmds[i].cmd, c, strlen(cmds[i].cmd)) &&
                            (!range_given || cmds[i].type == CMD_OPTIONAL_RANGE)) {
       t@@ -340,33 +418,53 @@ handle_cmd(ledit_view *view, char *cmd, size_t len) {
        
        static int
        substitute_yes(ledit_view *view, char *key_text, size_t len) {
       -        (void)view;
                (void)key_text;
                (void)len;
       -        return 1;
       +        substitute_single(view);
       +        buffer_recalc_line(view->buffer, sub_state.line);
       +        if (!sub_state.global) {
       +                sub_state.line++;
       +                sub_state.byte = 0;
       +        } else {
       +                sub_state.byte += sub_state.rlen;
       +        }
       +        int ret = move_to_next_substitution(view);
       +        if (ret)
       +                show_num_substituted(view);
       +        return ret;
        }
        
        static int
        substitute_yes_all(ledit_view *view, char *key_text, size_t len) {
       -        (void)view;
                (void)key_text;
                (void)len;
       +        substitute_all_remaining(view);
       +        show_num_substituted(view);
                return 0;
        }
        
        static int
        substitute_no(ledit_view *view, char *key_text, size_t len) {
       -        (void)view;
                (void)key_text;
                (void)len;
       -        return 1;
       +        if (!sub_state.global) {
       +                sub_state.line++;
       +                sub_state.byte = 0;
       +        } else {
       +                sub_state.byte += sub_state.slen;
       +        }
       +        int ret = move_to_next_substitution(view);
       +        if (ret)
       +                show_num_substituted(view);
       +        return ret;
        }
        
        static int
        substitute_no_all(ledit_view *view, char *key_text, size_t len) {
       -        (void)view;
                (void)key_text;
                (void)len;
       +        buffer_unlock_all_views(view->buffer);
       +        show_num_substituted(view);
                return 0;
        }
        
   DIR diff --git a/view.c b/view.c
       t@@ -627,8 +627,8 @@ line_prev_word(
                PangoLayout *layout = get_pango_layout(view, line);
                const PangoLogAttr *attrs =
                    pango_layout_get_log_attrs_readonly(layout, &nattrs);
       -        if (char_index > (size_t)nattrs)
       -                return -1;
       +        if (char_index > (size_t)nattrs - 1)
       +                char_index = (size_t)nattrs - 1;
                /* this is a bit weird because size_t can't be negative */
                for (size_t i = char_index; i > 0; i--) {
                        if (attrs[i-1].is_word_start) {
       t@@ -655,6 +655,8 @@ line_prev_bigword(
                size_t next_cursorb = byte;
                size_t next_cursorc = char_index;
                int found_word = 0;
       +        if (char_index > (size_t)nattrs - 1)
       +                char_index = (size_t)nattrs - 1;
                /* this is a bit weird because size_t can't be negative */
                for (size_t i = char_index; i > 0; i--) {
                        if (!found_word && !attrs[i-1].is_white) {
       t@@ -820,6 +822,7 @@ view_next_##name(                                                               
                               cur_line < view->lines_num - 1) {                                   \
                                cur_line++;                                                        \
                                cur_byte = 0;                                                      \
       +                        cur_char = 0;                                                      \
                                wrapped_line = 1;                                                  \
                        }                                                                          \
                        if (last_ret == -1 && cur_line == view->lines_num - 1)                     \
       t@@ -854,6 +857,7 @@ view_prev_##name(                                                               
                                cur_line--;                                                        \
                                ll = buffer_get_line(view->buffer, cur_line);                      \
                                cur_byte = ll->len;                                                \
       +                        cur_char = ll->len;                                                \
                        }                                                                          \
                        if (last_ret == -1 && cur_line == 0)                                       \
                                break;                                                             \