diff --git a/CHANGELOG.md b/CHANGELOG.md index f55792d8..d8a22a71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,9 +60,24 @@ row ([#1364][1364]). * Support for DECSET/DECRST/DECRQM 2027 (_Grapheme cluster processing_). +* New search mode key bindings (along with their defaults) + ([#419][419]): + - `extend-char` (shift+right) + - `extend-line-down` (shift+down) + - `extend-backward-char` (shift+left) + - `extend-backward-to-word-boundary` (ctrl+shift+left) + - `extend-backward-to-next-whitespace` (none) + - `extend-line-up` (shift+up) + - `scrollback-up-page` (shift+page-up) + - `scrollback-up-half-page` (none) + - `scrollback-up-line` (none) + - `scrollback-down-page` (shift+page-down) + - `scrollback-down-half-page` (none) + - `scrollback-down-line` (none) [1077]: https://codeberg.org/dnkl/foot/issues/1077 [1364]: https://codeberg.org/dnkl/foot/issues/1364 +[419]: https://codeberg.org/dnkl/foot/issues/419 ### Changed @@ -111,6 +126,8 @@ turned off and then back on again) ([#1498][1498]). * Transparency in margins (padding) not being disabled in fullscreen mode ([#1503][1503]). +* Crash when a scrollback search match is in the last column. +* Scrollback search: grapheme clusters not matching correctly. [1436]: https://codeberg.org/dnkl/foot/issues/1436 [1464]: https://codeberg.org/dnkl/foot/issues/1464 diff --git a/config.c b/config.c index 6ba371e8..e5ebb558 100644 --- a/config.c +++ b/config.c @@ -134,6 +134,14 @@ static const char *const binding_action_map[] = { static const char *const search_binding_action_map[] = { [BIND_ACTION_SEARCH_NONE] = NULL, + [BIND_ACTION_SEARCH_SCROLLBACK_UP_PAGE] = "scrollback-up-page", + [BIND_ACTION_SEARCH_SCROLLBACK_UP_HALF_PAGE] = "scrollback-up-half-page", + [BIND_ACTION_SEARCH_SCROLLBACK_UP_LINE] = "scrollback-up-line", + [BIND_ACTION_SEARCH_SCROLLBACK_DOWN_PAGE] = "scrollback-down-page", + [BIND_ACTION_SEARCH_SCROLLBACK_DOWN_HALF_PAGE] = "scrollback-down-half-page", + [BIND_ACTION_SEARCH_SCROLLBACK_DOWN_LINE] = "scrollback-down-line", + [BIND_ACTION_SEARCH_SCROLLBACK_HOME] = "scrollback-home", + [BIND_ACTION_SEARCH_SCROLLBACK_END] = "scrollback-end", [BIND_ACTION_SEARCH_CANCEL] = "cancel", [BIND_ACTION_SEARCH_COMMIT] = "commit", [BIND_ACTION_SEARCH_FIND_PREV] = "find-prev", @@ -148,8 +156,14 @@ static const char *const search_binding_action_map[] = { [BIND_ACTION_SEARCH_DELETE_PREV_WORD] = "delete-prev-word", [BIND_ACTION_SEARCH_DELETE_NEXT] = "delete-next", [BIND_ACTION_SEARCH_DELETE_NEXT_WORD] = "delete-next-word", + [BIND_ACTION_SEARCH_EXTEND_CHAR] = "extend-char", [BIND_ACTION_SEARCH_EXTEND_WORD] = "extend-to-word-boundary", [BIND_ACTION_SEARCH_EXTEND_WORD_WS] = "extend-to-next-whitespace", + [BIND_ACTION_SEARCH_EXTEND_LINE_DOWN] = "extend-line-down", + [BIND_ACTION_SEARCH_EXTEND_BACKWARD_CHAR] = "extend-backward-char", + [BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD] = "extend-backward-to-word-boundary", + [BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD_WS] = "extend-backward-to-next-whitespace", + [BIND_ACTION_SEARCH_EXTEND_LINE_UP] = "extend-line-up", [BIND_ACTION_SEARCH_CLIPBOARD_PASTE] = "clipboard-paste", [BIND_ACTION_SEARCH_PRIMARY_PASTE] = "primary-paste", [BIND_ACTION_SEARCH_UNICODE_INPUT] = "unicode-input", @@ -2774,11 +2788,12 @@ get_server_socket_path(void) return xasprintf("%s/foot-%s.sock", xdg_runtime, wayland_display); } -#define m_none {0} -#define m_alt {.alt = true} -#define m_ctrl {.ctrl = true} -#define m_shift {.shift = true} -#define m_ctrl_shift {.ctrl = true, .shift = true} +#define m_none {0} +#define m_alt {.alt = true} +#define m_ctrl {.ctrl = true} +#define m_shift {.shift = true} +#define m_ctrl_shift {.ctrl = true, .shift = true} +#define m_ctrl_shift_alt {.ctrl = true, .shift = true, .alt = true} static void add_default_key_bindings(struct config *conf) @@ -2816,6 +2831,8 @@ static void add_default_search_bindings(struct config *conf) { static const struct config_key_binding bindings[] = { + {BIND_ACTION_SEARCH_SCROLLBACK_UP_PAGE, m_shift, {{XKB_KEY_Prior}}}, + {BIND_ACTION_SEARCH_SCROLLBACK_DOWN_PAGE, m_shift, {{XKB_KEY_Next}}}, {BIND_ACTION_SEARCH_CANCEL, m_ctrl, {{XKB_KEY_c}}}, {BIND_ACTION_SEARCH_CANCEL, m_ctrl, {{XKB_KEY_g}}}, {BIND_ACTION_SEARCH_CANCEL, m_none, {{XKB_KEY_Escape}}}, @@ -2840,8 +2857,14 @@ add_default_search_bindings(struct config *conf) {BIND_ACTION_SEARCH_DELETE_NEXT, m_none, {{XKB_KEY_Delete}}}, {BIND_ACTION_SEARCH_DELETE_NEXT_WORD, m_ctrl, {{XKB_KEY_Delete}}}, {BIND_ACTION_SEARCH_DELETE_NEXT_WORD, m_alt, {{XKB_KEY_d}}}, + {BIND_ACTION_SEARCH_EXTEND_CHAR, m_shift, {{XKB_KEY_Right}}}, {BIND_ACTION_SEARCH_EXTEND_WORD, m_ctrl, {{XKB_KEY_w}}}, + {BIND_ACTION_SEARCH_EXTEND_WORD, m_ctrl_shift, {{XKB_KEY_Right}}}, {BIND_ACTION_SEARCH_EXTEND_WORD_WS, m_ctrl_shift, {{XKB_KEY_w}}}, + {BIND_ACTION_SEARCH_EXTEND_LINE_DOWN, m_shift, {{XKB_KEY_Down}}}, + {BIND_ACTION_SEARCH_EXTEND_BACKWARD_CHAR, m_shift, {{XKB_KEY_Left}}}, + {BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD, m_ctrl_shift, {{XKB_KEY_Left}}}, + {BIND_ACTION_SEARCH_EXTEND_LINE_UP, m_shift, {{XKB_KEY_Up}}}, {BIND_ACTION_SEARCH_CLIPBOARD_PASTE, m_ctrl, {{XKB_KEY_v}}}, {BIND_ACTION_SEARCH_CLIPBOARD_PASTE, m_ctrl_shift, {{XKB_KEY_v}}}, {BIND_ACTION_SEARCH_CLIPBOARD_PASTE, m_ctrl, {{XKB_KEY_y}}}, diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 24c42a5e..5ef4dcb4 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -945,13 +945,35 @@ scrollback search mode. The syntax is exactly the same as the regular Deletes the **word after** the cursor. Default: _Mod1+d Control+Delete_. +*extend-char* + Extend current selection to the right, by one character. Default: + _Shift+Right_. + *extend-to-word-boundary* - Extend current selection to the next word boundary. Default: - _Control+w_. + Extend current selection to the right, to the next word + boundary. Default: _Control+w Control+Shift+Right_. *extend-to-next-whitespace* - Extend the current selection to the next whitespace. Default: - _Control+Shift+w_. + Extend the current selection to the right, to the next + whitespace. Default: _Control+Shift+w_. + +*extend-line-down* + Extend current selection down one line. Default: _Shift+Down_. + +*extend-backward-char* + Extend current selection to the left, by one character. Default: + _Shift+Left_. + +*extend-backward-to-word-boundary* + Extend current selection to the left, to the next word + boundary. Default: _Control+Shift+Left_. + +*extend-backward-to-next-whitespace* + Extend the current selection to the left, to the next + whitespace. Default: _none_. + +*extend-line-up* + Extend current selection up one line. Default: _Shift+Up_. *clipboard-paste* Paste from the _clipboard_ into the search buffer. Default: @@ -965,6 +987,31 @@ scrollback search mode. The syntax is exactly the same as the regular Unicode input mode. See _key-bindings.unicode-input_ for details. Default: _none_. +*scrollback-up-page* + Scrolls up/back one page in history. Default: _Shift+Page\_Up_. + +*scrollback-up-half-page* + Scrolls up/back half of a page in history. Default: _none_. + +*scrollback-up-line* + Scrolls up/back a single line in history. Default: _none_. + +*scrollback-down-page* + Scroll down/forward one page in history. Default: + _Shift+Page\_Down_. + +*scrollback-down-half-page* + Scroll down/forward half of a page in history. Default: _none_. + +*scrollback-down-line* + Scroll down/forward a single line in history. Default: _none_. + +*scrollback-home* + Scroll to the beginning of the scrollback. Default: _none_. + +*scrollback-end* + Scroll to the end (bottom) of the scrollback. Default: _none_. + # SECTION: url-bindings This section lets you override the default key bindings used in URL diff --git a/foot.ini b/foot.ini index 00505165..cdbd8259 100644 --- a/foot.ini +++ b/foot.ini @@ -139,6 +139,8 @@ # scrollback-down-page=Shift+Page_Down # scrollback-down-half-page=none # scrollback-down-line=none +# scrollback-home=none +# scrollback-end=none # clipboard-copy=Control+Shift+c XF86Copy # clipboard-paste=Control+Shift+v XF86Paste # primary-paste=Shift+Insert @@ -176,11 +178,25 @@ # delete-prev-word=Mod1+BackSpace Control+BackSpace # delete-next=Delete # delete-next-word=Mod1+d Control+Delete -# extend-to-word-boundary=Control+w +# extend-char=Shift+Right +# extend-to-word-boundary=Control+w Control+Shift+Right # extend-to-next-whitespace=Control+Shift+w +# extend-line-down=Shift+Down +# extend-backward-char=Shift+Left +# extend-backward-to-word-boundary=Control+Shift+Left +# extend-backward-to-next-whitespace=none +# extend-line-up=Shift+Up # clipboard-paste=Control+v Control+Shift+v Control+y XF86Paste # primary-paste=Shift+Insert # unicode-input=none +# scrollback-up-page=Shift+Page_Up +# scrollback-up-half-page=none +# scrollback-up-line=none +# scrollback-down-page=Shift+Page_Down +# scrollback-down-half-page=none +# scrollback-down-line=none +# scrollback-home=none +# scrollback-end=none [url-bindings] # cancel=Control+g Control+c Control+d Escape diff --git a/key-binding.h b/key-binding.h index ea2f3d6d..050c80a6 100644 --- a/key-binding.h +++ b/key-binding.h @@ -58,6 +58,14 @@ enum bind_action_normal { enum bind_action_search { BIND_ACTION_SEARCH_NONE, + BIND_ACTION_SEARCH_SCROLLBACK_UP_PAGE, + BIND_ACTION_SEARCH_SCROLLBACK_UP_HALF_PAGE, + BIND_ACTION_SEARCH_SCROLLBACK_UP_LINE, + BIND_ACTION_SEARCH_SCROLLBACK_DOWN_PAGE, + BIND_ACTION_SEARCH_SCROLLBACK_DOWN_HALF_PAGE, + BIND_ACTION_SEARCH_SCROLLBACK_DOWN_LINE, + BIND_ACTION_SEARCH_SCROLLBACK_HOME, + BIND_ACTION_SEARCH_SCROLLBACK_END, BIND_ACTION_SEARCH_CANCEL, BIND_ACTION_SEARCH_COMMIT, BIND_ACTION_SEARCH_FIND_PREV, @@ -72,8 +80,14 @@ enum bind_action_search { BIND_ACTION_SEARCH_DELETE_PREV_WORD, BIND_ACTION_SEARCH_DELETE_NEXT, BIND_ACTION_SEARCH_DELETE_NEXT_WORD, + BIND_ACTION_SEARCH_EXTEND_CHAR, BIND_ACTION_SEARCH_EXTEND_WORD, BIND_ACTION_SEARCH_EXTEND_WORD_WS, + BIND_ACTION_SEARCH_EXTEND_LINE_DOWN, + BIND_ACTION_SEARCH_EXTEND_BACKWARD_CHAR, + BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD, + BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD_WS, + BIND_ACTION_SEARCH_EXTEND_LINE_UP, BIND_ACTION_SEARCH_CLIPBOARD_PASTE, BIND_ACTION_SEARCH_PRIMARY_PASTE, BIND_ACTION_SEARCH_UNICODE_INPUT, diff --git a/search.c b/search.c index 6c2a2a7e..55388577 100644 --- a/search.c +++ b/search.c @@ -9,6 +9,7 @@ #define LOG_ENABLE_DBG 0 #include "log.h" #include "char32.h" +#include "commands.h" #include "config.h" #include "extract.h" #include "grid.h" @@ -82,7 +83,14 @@ search_ensure_size(struct terminal *term, size_t wanted_size) } static bool -has_wrapped_around(const struct terminal *term, int abs_row_no) +has_wrapped_around_left(const struct terminal *term, int abs_row_no) +{ + int rebased_row = grid_row_abs_to_sb(term->grid, term->rows, abs_row_no); + return rebased_row == term->grid->num_rows - 1 || term->grid->rows[abs_row_no] == NULL; +} + +static bool +has_wrapped_around_right(const struct terminal *term, int abs_row_no) { int rebased_row = grid_row_abs_to_sb(term->grid, term->rows, abs_row_no); return rebased_row == 0; @@ -235,13 +243,19 @@ search_update_selection(struct terminal *term, const struct range *match) } if (start_row != term->search.match.row || - start_col != term->search.match.col) + start_col != term->search.match.col || + + /* Pointer leave events trigger selection_finalize() :/ */ + !term->selection.ongoing) { int selection_row = start_row - grid->view + grid->num_rows; selection_row &= grid->num_rows - 1; selection_start( term, start_col, selection_row, SELECTION_CHAR_WISE, false); + + term->search.match.row = start_row; + term->search.match.col = start_col; } /* Update selection endpoint */ @@ -273,16 +287,16 @@ matches_cell(const struct terminal *term, const struct cell *cell, size_t search return -1; if (composed != NULL) { - if (search_ofs + 1 + composed->count > term->search.len) + if (search_ofs + composed->count > term->search.len) return -1; for (size_t j = 1; j < composed->count; j++) { - if (composed->chars[j] != term->search.buf[search_ofs + 1 + j]) + if (composed->chars[j] != term->search.buf[search_ofs + j]) return -1; } } - return composed != NULL ? 1 + composed->count : 1; + return composed != NULL ? composed->count : 1; } static bool @@ -369,8 +383,11 @@ find_next(struct terminal *term, enum search_direction direction, match_len += additional_chars; match_end_col++; - while (match_row->cells[match_end_col].wc > CELL_SPACER) + while (match_end_col < term->cols && + match_row->cells[match_end_col].wc > CELL_SPACER) + { match_end_col++; + } } if (match_len != term->search.len) { @@ -546,6 +563,7 @@ search_matches_next(struct search_match_iterator *iter) term->cols - 1, grid_row_absolute_in_view(grid, term->rows - 1)}; + /* BUG: matches *starting* outside the view, but ending *inside*, aren't matched */ struct range match; bool found = find_next(term, SEARCH_FORWARD, abs_start, abs_end, &match); if (!found) @@ -565,11 +583,6 @@ search_matches_next(struct search_match_iterator *iter) match.start.row, match.start.col, match.end.row, match.end.col, grid->view); - xassert(match.start.row >= 0); - xassert(match.start.row < term->rows); - xassert(match.end.row >= 0); - xassert(match.end.row < term->rows); - /* Assert match end comes *after* the match start */ xassert(match.end.row > match.start.row || (match.end.row == match.start.row && @@ -642,67 +655,299 @@ search_add_chars(struct terminal *term, const char *src, size_t count) add_wchars(term, c32s, chars); } +enum extend_direction {SEARCH_EXTEND_LEFT, SEARCH_EXTEND_RIGHT}; + +static bool +coord_advance_left(const struct terminal *term, struct coord *pos, + const struct row **row) +{ + const struct grid *grid = term->grid; + struct coord new_pos = *pos; + + if (--new_pos.col < 0) { + new_pos.row = (new_pos.row - 1 + grid->num_rows) & (grid->num_rows - 1); + new_pos.col = term->cols - 1; + + if (has_wrapped_around_left(term, new_pos.row)) + return false; + + if (row != NULL) + *row = grid->rows[new_pos.row]; + } + + *pos = new_pos; + return true; +} + +static bool +coord_advance_right(const struct terminal *term, struct coord *pos, + const struct row **row) +{ + const struct grid *grid = term->grid; + struct coord new_pos = *pos; + + if (++new_pos.col >= term->cols) { + new_pos.row = (new_pos.row + 1) & (grid->num_rows - 1); + new_pos.col = 0; + + if (has_wrapped_around_right(term, new_pos.row)) + return false; + + if (row != NULL) + *row = grid->rows[new_pos.row]; + } + + *pos = new_pos; + return true; +} + +static bool +search_extend_find_char(const struct terminal *term, struct coord *target, + enum extend_direction direction) +{ + if (term->search.match_len == 0) + return false; + + struct coord pos = direction == SEARCH_EXTEND_LEFT + ? selection_get_start(term) : selection_get_end(term); + xassert(pos.row >= 0); + xassert(pos.row < term->grid->num_rows); + + *target = pos; + + const struct row *row = term->grid->rows[pos.row]; + + while (true) { + switch (direction) { + case SEARCH_EXTEND_LEFT: + if (!coord_advance_left(term, &pos, &row)) + return false; + break; + + case SEARCH_EXTEND_RIGHT: + if (!coord_advance_right(term, &pos, &row)) + return false; + break; + } + + const char32_t wc = row->cells[pos.col].wc; + + if (wc >= CELL_SPACER || wc == U'\0') + continue; + + *target = pos; + return true; + } +} + +static bool +search_extend_find_char_left(const struct terminal *term, struct coord *target) +{ + return search_extend_find_char(term, target, SEARCH_EXTEND_LEFT); +} + +static bool +search_extend_find_char_right(const struct terminal *term, struct coord *target) +{ + return search_extend_find_char(term, target, SEARCH_EXTEND_RIGHT); +} + +static bool +search_extend_find_word(const struct terminal *term, bool spaces_only, + struct coord *target, enum extend_direction direction) +{ + if (term->search.match_len == 0) + return false; + + struct grid *grid = term->grid; + struct coord pos = direction == SEARCH_EXTEND_LEFT + ? selection_get_start(term) + : selection_get_end(term); + + xassert(pos.row >= 0); + xassert(pos.row < grid->num_rows); + + *target = pos; + + /* First character to consider is the *next* character */ + switch (direction) { + case SEARCH_EXTEND_LEFT: + if (!coord_advance_left(term, &pos, NULL)) + return false; + break; + + case SEARCH_EXTEND_RIGHT: + if (!coord_advance_right(term, &pos, NULL)) + return false; + break; + } + + xassert(pos.row >= 0); + xassert(pos.row < grid->num_rows); + xassert(grid->rows[pos.row] != NULL); + + /* Find next word boundary */ + switch (direction) { + case SEARCH_EXTEND_LEFT: + selection_find_word_boundary_left(term, &pos, spaces_only); + break; + + case SEARCH_EXTEND_RIGHT: + selection_find_word_boundary_right(term, &pos, spaces_only, false); + break; + } + + *target = pos; + return true; +} + +static bool +search_extend_find_word_left(const struct terminal *term, bool spaces_only, + struct coord *target) +{ + return search_extend_find_word(term, spaces_only, target, SEARCH_EXTEND_LEFT); +} + +static bool +search_extend_find_word_right(const struct terminal *term, bool spaces_only, + struct coord *target) +{ + return search_extend_find_word(term, spaces_only, target, SEARCH_EXTEND_RIGHT); +} + +static bool +search_extend_find_line(const struct terminal *term, struct coord *target, + enum extend_direction direction) +{ + if (term->search.match_len == 0) + return false; + + struct coord pos = direction == SEARCH_EXTEND_LEFT + ? selection_get_start(term) : selection_get_end(term); + + xassert(pos.row >= 0); + xassert(pos.row < term->grid->num_rows); + + *target = pos; + + const struct grid *grid = term->grid; + + switch (direction) { + case SEARCH_EXTEND_LEFT: + pos.row = (pos.row - 1 + grid->num_rows) & (grid->num_rows - 1); + if (has_wrapped_around_left(term, pos.row)) + return false; + break; + + case SEARCH_EXTEND_RIGHT: + pos.row = (pos.row + 1) & (grid->num_rows - 1); + if (has_wrapped_around_right(term, pos.row)) + return false; + break; + } + + *target = pos; + return true; +} + +static bool +search_extend_find_line_up(const struct terminal *term, struct coord *target) +{ + return search_extend_find_line(term, target, SEARCH_EXTEND_LEFT); +} + +static bool +search_extend_find_line_down(const struct terminal *term, struct coord *target) +{ + return search_extend_find_line(term, target, SEARCH_EXTEND_RIGHT); +} + static void -search_match_to_end_of_word(struct terminal *term, bool spaces_only) +search_extend_left(struct terminal *term, const struct coord *target) { if (term->search.match_len == 0) return; - xassert(term->selection.coords.end.row >= 0); + const struct coord last_coord = selection_get_start(term); + struct coord pos = *target; + const struct row *row = term->grid->rows[pos.row]; - struct grid *grid = term->grid; - const bool move_cursor = term->search.cursor == term->search.len; + const bool move_cursor = term->search.cursor != 0; - struct coord old_end = selection_get_end(term); - struct coord new_end = old_end; - struct row *row = NULL; - - xassert(new_end.row >= 0); - xassert(new_end.row < grid->num_rows); - - /* Advances a coordinate by one column, to the right. Returns - * false if we’ve reached the scrollback wrap-around */ -#define advance_pos(coord) __extension__ \ - ({ \ - bool wrapped_around = false; \ - if (++(coord).col >= term->cols) { \ - (coord).row = ((coord).row + 1) & (grid->num_rows - 1); \ - (coord).col = 0; \ - row = grid->rows[(coord).row]; \ - if (has_wrapped_around(term, (coord.row))) \ - wrapped_around = true; \ - } \ - !wrapped_around; \ - }) - - /* First character to consider is the *next* character */ - if (!advance_pos(new_end)) + struct extraction_context *ctx = extract_begin(SELECTION_NONE, false); + if (ctx == NULL) return; - xassert(new_end.row >= 0); - xassert(new_end.row < grid->num_rows); - xassert(grid->rows[new_end.row] != NULL); + while (pos.col != last_coord.col || pos.row != last_coord.row) { + if (!extract_one(term, row, &row->cells[pos.col], pos.col, ctx)) + break; + if (!coord_advance_right(term, &pos, &row)) + break; + } - /* Find next word boundary */ - new_end.row -= grid->view + grid->num_rows; - new_end.row &= grid->num_rows - 1; - selection_find_word_boundary_right(term, &new_end, spaces_only, false); - new_end.row += grid->view; - new_end.row &= grid->num_rows - 1; + char32_t *new_text; + size_t new_len; - struct coord pos = old_end; - row = grid->rows[pos.row]; + if (!extract_finish_wide(ctx, &new_text, &new_len)) + return; + + if (!search_ensure_size(term, term->search.len + new_len)) + return; + + memmove(&term->search.buf[new_len], &term->search.buf[0], + term->search.len * sizeof(term->search.buf[0])); + + size_t actually_copied = 0; + for (size_t i = 0; i < new_len; i++) { + if (new_text[i] == U'\n') { + /* extract() adds newlines, which we never match against */ + continue; + } + + term->search.buf[actually_copied++] = new_text[i]; + term->search.len++; + } + + xassert(actually_copied <= new_len); + if (actually_copied < new_len) { + memmove( + &term->search.buf[actually_copied], &term->search.buf[new_len], + (term->search.len - actually_copied) * sizeof(term->search.buf[0])); + } + + term->search.buf[term->search.len] = U'\0'; + free(new_text); + + if (move_cursor) + term->search.cursor += actually_copied; + + struct range match = {.start = *target, .end = selection_get_end(term)}; + search_update_selection(term, &match); + + term->search.match_len = term->search.len; +} + +static void +search_extend_right(struct terminal *term, const struct coord *target) +{ + if (term->search.match_len == 0) + return; + + struct coord pos = selection_get_end(term); + const struct row *row = term->grid->rows[pos.row]; + + const bool move_cursor = term->search.cursor == term->search.len; struct extraction_context *ctx = extract_begin(SELECTION_NONE, false); if (ctx == NULL) return; do { - if (!advance_pos(pos)) + if (!coord_advance_right(term, &pos, &row)) break; if (!extract_one(term, row, &row->cells[pos.col], pos.col, ctx)) break; - } while (pos.col != new_end.col || pos.row != new_end.row); + } while (pos.col != target->col || pos.row != target->row); char32_t *new_text; size_t new_len; @@ -728,12 +973,9 @@ search_match_to_end_of_word(struct terminal *term, bool spaces_only) if (move_cursor) term->search.cursor = term->search.len; - struct range match = {.start = term->search.match, .end = new_end}; + struct range match = {.start = term->search.match, .end = *target}; search_update_selection(term, &match); - term->search.match_len = term->search.len; - -#undef advance_pos } static size_t @@ -822,6 +1064,62 @@ execute_binding(struct seat *seat, struct terminal *term, case BIND_ACTION_SEARCH_NONE: return false; + case BIND_ACTION_SEARCH_SCROLLBACK_UP_PAGE: + if (term->grid == &term->normal) { + cmd_scrollback_up(term, term->rows); + return true; + } + return false; + + case BIND_ACTION_SEARCH_SCROLLBACK_UP_HALF_PAGE: + if (term->grid == &term->normal) { + cmd_scrollback_up(term, max(term->rows / 2, 1)); + return true; + } + break; + + case BIND_ACTION_SEARCH_SCROLLBACK_UP_LINE: + if (term->grid == &term->normal) { + cmd_scrollback_up(term, 1); + return true; + } + break; + + case BIND_ACTION_SEARCH_SCROLLBACK_DOWN_PAGE: + if (term->grid == &term->normal) { + cmd_scrollback_down(term, term->rows); + return true; + } + return false; + + case BIND_ACTION_SEARCH_SCROLLBACK_DOWN_HALF_PAGE: + if (term->grid == &term->normal) { + cmd_scrollback_down(term, max(term->rows / 2, 1)); + return true; + } + break; + + case BIND_ACTION_SEARCH_SCROLLBACK_DOWN_LINE: + if (term->grid == &term->normal) { + cmd_scrollback_down(term, 1); + return true; + } + break; + + case BIND_ACTION_SEARCH_SCROLLBACK_HOME: + if (term->grid == &term->normal) { + cmd_scrollback_up(term, term->grid->num_rows); + return true; + } + break; + + case BIND_ACTION_SEARCH_SCROLLBACK_END: + if (term->grid == &term->normal) { + cmd_scrollback_down(term, term->grid->num_rows); + return true; + } + break; + case BIND_ACTION_SEARCH_CANCEL: if (term->search.view_followed_offset) grid->view = grid->offset; @@ -967,17 +1265,85 @@ execute_binding(struct seat *seat, struct terminal *term, return true; } - case BIND_ACTION_SEARCH_EXTEND_WORD: - search_match_to_end_of_word(term, false); - *update_search_result = false; - *redraw = true; + case BIND_ACTION_SEARCH_EXTEND_CHAR: { + struct coord target; + if (search_extend_find_char_right(term, &target)) { + search_extend_right(term, &target); + *update_search_result = false; + *redraw = true; + } return true; + } - case BIND_ACTION_SEARCH_EXTEND_WORD_WS: - search_match_to_end_of_word(term, true); - *update_search_result = false; - *redraw = true; + case BIND_ACTION_SEARCH_EXTEND_WORD: { + struct coord target; + if (search_extend_find_word_right(term, false, &target)) { + search_extend_right(term, &target); + *update_search_result = false; + *redraw = true; + } return true; + } + + case BIND_ACTION_SEARCH_EXTEND_WORD_WS: { + struct coord target; + if (search_extend_find_word_right(term, true, &target)) { + search_extend_right(term, &target); + *update_search_result = false; + *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_EXTEND_LINE_DOWN: { + struct coord target; + if (search_extend_find_line_down(term, &target)) { + search_extend_right(term, &target); + *update_search_result = false; + *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_EXTEND_BACKWARD_CHAR: { + struct coord target; + if (search_extend_find_char_left(term, &target)) { + search_extend_left(term, &target); + *update_search_result = false; + *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD: { + struct coord target; + if (search_extend_find_word_left(term, false, &target)) { + search_extend_left(term, &target); + *update_search_result = false; + *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD_WS: { + struct coord target; + if (search_extend_find_word_left(term, true, &target)) { + search_extend_left(term, &target); + *update_search_result = false; + *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_EXTEND_LINE_UP: { + struct coord target; + if (search_extend_find_line_up(term, &target)) { + search_extend_left(term, &target); + *update_search_result = false; + *redraw = true; + } + return true; + } case BIND_ACTION_SEARCH_CLIPBOARD_PASTE: text_from_clipboard( diff --git a/selection.c b/selection.c index f03a3d5c..50e41637 100644 --- a/selection.c +++ b/selection.c @@ -340,16 +340,19 @@ selection_to_text(const struct terminal *term) return extract_finish(ctx, &text, NULL) ? text : NULL; } +/* Coordinates are in *absolute* row numbers (NOT view local) */ void -selection_find_word_boundary_left(struct terminal *term, struct coord *pos, +selection_find_word_boundary_left(const struct terminal *term, struct coord *pos, bool spaces_only) { - xassert(pos->row >= 0); - xassert(pos->row < term->rows); + const struct grid *grid = term->grid; + xassert(pos->col >= 0); xassert(pos->col < term->cols); + xassert(pos->row >= 0); + pos->row &= grid->num_rows - 1; - const struct row *r = grid_row_in_view(term->grid, pos->row); + const struct row *r = grid->rows[pos->row]; char32_t c = r->cells[pos->col].wc; while (c >= CELL_SPACER) { @@ -373,15 +376,22 @@ selection_find_word_boundary_left(struct terminal *term, struct coord *pos, int next_col = pos->col - 1; int next_row = pos->row; - const struct row *row = grid_row_in_view(term->grid, next_row); + const struct row *row = grid->rows[next_row]; /* Linewrap */ if (next_col < 0) { next_col = term->cols - 1; - if (--next_row < 0) - break; - row = grid_row_in_view(term->grid, next_row); + next_row = (next_row - 1 + grid->num_rows) & (grid->num_rows - 1); + + if (grid_row_abs_to_sb(grid, term->rows, next_row) == term->grid->num_rows - 1 || + grid->rows[next_row] == NULL) + { + /* Scrollback wrap-around */ + break; + } + + row = grid->rows[next_row]; if (row->linebreak) { /* Hard linebreak, treat as space. I.e. break selection */ @@ -418,17 +428,20 @@ selection_find_word_boundary_left(struct terminal *term, struct coord *pos, } } +/* Coordinates are in *absolute* row numbers (NOT view local) */ void -selection_find_word_boundary_right(struct terminal *term, struct coord *pos, +selection_find_word_boundary_right(const struct terminal *term, struct coord *pos, bool spaces_only, bool stop_on_space_to_word_boundary) { - xassert(pos->row >= 0); - xassert(pos->row < term->rows); + const struct grid *grid = term->grid; + xassert(pos->col >= 0); xassert(pos->col < term->cols); + xassert(pos->row >= 0); + pos->row &= grid->num_rows - 1; - const struct row *r = grid_row_in_view(term->grid, pos->row); + const struct row *r = grid->rows[pos->row]; char32_t c = r->cells[pos->col].wc; while (c >= CELL_SPACER) { @@ -453,7 +466,7 @@ selection_find_word_boundary_right(struct terminal *term, struct coord *pos, int next_col = pos->col + 1; int next_row = pos->row; - const struct row *row = grid_row_in_view(term->grid, next_row); + const struct row *row = term->grid->rows[next_row]; /* Linewrap */ if (next_col >= term->cols) { @@ -463,10 +476,14 @@ selection_find_word_boundary_right(struct terminal *term, struct coord *pos, } next_col = 0; - if (++next_row >= term->rows) - break; + next_row = (next_row + 1) & (grid->num_rows - 1); - row = grid_row_in_view(term->grid, next_row); + if (grid_row_abs_to_sb(grid, term->rows, next_row) == 0) { + /* Scrollback wrap-around */ + break; + } + + row = grid->rows[next_row]; } c = row->cells[next_col].wc; @@ -659,17 +676,26 @@ selection_start(struct terminal *term, int col, int row, break; case SELECTION_WORD_WISE: { - struct coord start = {col, row}, end = {col, row}; + struct coord start = {col, term->grid->view + row}; + struct coord end = {col, term->grid->view + row}; selection_find_word_boundary_left(term, &start, spaces_only); selection_find_word_boundary_right(term, &end, spaces_only, true); - term->selection.coords.start = (struct coord){ - start.col, term->grid->view + start.row}; + term->selection.coords.start = start; term->selection.pivot.start = term->selection.coords.start; - term->selection.pivot.end = (struct coord){end.col, term->grid->view + end.row}; + term->selection.pivot.end = end; - selection_update(term, end.col, end.row); + /* + * FIXME: go through selection.c and make sure all public + * functions use the *same* coordinate system... + * + * selection_find_word_boundary*() uses absolute row numbers, + * while selection_update(), and pretty much all others, use + * view-local. + */ + + selection_update(term, end.col, end.row - term->grid->view); break; } @@ -1091,16 +1117,16 @@ selection_update(struct terminal *term, int col, int row) if (!term->selection.ongoing) return; - LOG_DBG("selection updated: start = %d,%d, end = %d,%d -> %d, %d", - term->selection.coords.start.row, term->selection.coords.start.col, - term->selection.coords.end.row, term->selection.coords.end.col, - row, col); - xassert(term->grid->view + row != -1); struct coord new_start = term->selection.coords.start; struct coord new_end = {col, term->grid->view + row}; + LOG_DBG("selection updated: start = %d,%d, end = %d,%d -> %d, %d", + term->selection.coords.start.row, term->selection.coords.start.col, + term->selection.coords.end.row, term->selection.coords.end.col, + new_end.row, new_end.col); + /* Adjust start point if the selection has changed 'direction' */ if (!(new_end.row == new_start.row && new_end.col == new_start.col)) { enum selection_direction new_direction = term->selection.direction; @@ -1160,21 +1186,17 @@ selection_update(struct terminal *term, int col, int row) case SELECTION_WORD_WISE: switch (term->selection.direction) { - case SELECTION_LEFT: { - struct coord end = {col, row}; + case SELECTION_LEFT: + new_end = (struct coord){col, term->grid->view + row}; selection_find_word_boundary_left( - term, &end, term->selection.spaces_only); - new_end = (struct coord){end.col, term->grid->view + end.row}; + term, &new_end, term->selection.spaces_only); break; - } - case SELECTION_RIGHT: { - struct coord end = {col, row}; + case SELECTION_RIGHT: + new_end = (struct coord){col, term->grid->view + row}; selection_find_word_boundary_right( - term, &end, term->selection.spaces_only, true); - new_end = (struct coord){end.col, term->grid->view + end.row}; + term, &new_end, term->selection.spaces_only, true); break; - } case SELECTION_UNDIR: break; @@ -1346,16 +1368,14 @@ selection_extend_normal(struct terminal *term, int col, int row, xassert(new_kind == SELECTION_CHAR_WISE || new_kind == SELECTION_WORD_WISE); - struct coord pivot_start = {new_start.col, new_start.row - term->grid->view}; + struct coord pivot_start = {new_start.col, new_start.row}; struct coord pivot_end = pivot_start; selection_find_word_boundary_left(term, &pivot_start, spaces_only); selection_find_word_boundary_right(term, &pivot_end, spaces_only, true); - term->selection.pivot.start = - (struct coord){pivot_start.col, term->grid->view + pivot_start.row}; - term->selection.pivot.end = - (struct coord){pivot_end.col, term->grid->view + pivot_end.row}; + term->selection.pivot.start = pivot_start; + term->selection.pivot.end = pivot_end; break; } diff --git a/selection.h b/selection.h index c6d7f968..26298457 100644 --- a/selection.h +++ b/selection.h @@ -78,9 +78,9 @@ void selection_start_scroll_timer( void selection_stop_scroll_timer(struct terminal *term); void selection_find_word_boundary_left( - struct terminal *term, struct coord *pos, bool spaces_only); + const struct terminal *term, struct coord *pos, bool spaces_only); void selection_find_word_boundary_right( - struct terminal *term, struct coord *pos, bool spaces_only, + const struct terminal *term, struct coord *pos, bool spaces_only, bool stop_on_space_to_word_boundary); struct coord selection_get_start(const struct terminal *term);