diff --git a/CHANGELOG.md b/CHANGELOG.md index 31a28a48..a4730e1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,8 +55,12 @@ * New mouse bindings: `scrollback-up-mouse` and `scrollback-down-mouse`, bound to `BTN_BACK` and `BTN_FORWARD` respectively. +* New key binding: `select-quote`. This key binding selects text + between quote characters, and falls back to selecting the entire + row ([#1364][1364]). [1077]: https://codeberg.org/dnkl/foot/issues/1077 +[1364]: https://codeberg.org/dnkl/foot/issues/1364 ### Changed @@ -66,6 +70,10 @@ * `foot-server.service` systemd now checks for `ConditionEnvironment=WAYLAND_DISPLAY` for consistency with the socket unit ([#1448][1448]) +* Default key binding for `select-row` is now `BTN_LEFT+4`. However, + in many cases, triple clicking will still be enough to select the + entire row; see the new key binding `select-quote` (mapped to + `BTN_LEFT+3` by default) ([#1364][1364]). [1391]: https://codeberg.org/dnkl/foot/issues/1391 [1448]: https://codeberg.org/dnkl/foot/pulls/1448 diff --git a/config.c b/config.c index aeea0b32..68dbc679 100644 --- a/config.c +++ b/config.c @@ -128,6 +128,7 @@ static const char *const binding_action_map[] = { [BIND_ACTION_SELECT_EXTEND_CHAR_WISE] = "select-extend-character-wise", [BIND_ACTION_SELECT_WORD] = "select-word", [BIND_ACTION_SELECT_WORD_WS] = "select-word-whitespace", + [BIND_ACTION_SELECT_QUOTE] = "select-quote", [BIND_ACTION_SELECT_ROW] = "select-row", }; @@ -2882,7 +2883,8 @@ add_default_mouse_bindings(struct config *conf) {BIND_ACTION_SELECT_EXTEND_CHAR_WISE, m_ctrl, {.m = {BTN_RIGHT, 1}}}, {BIND_ACTION_SELECT_WORD, m_none, {.m = {BTN_LEFT, 2}}}, {BIND_ACTION_SELECT_WORD_WS, m_ctrl, {.m = {BTN_LEFT, 2}}}, - {BIND_ACTION_SELECT_ROW, m_none, {.m = {BTN_LEFT, 3}}}, + {BIND_ACTION_SELECT_QUOTE, m_none, {.m = {BTN_LEFT, 3}}}, + {BIND_ACTION_SELECT_ROW, m_none, {.m = {BTN_LEFT, 4}}}, }; conf->bindings.mouse.count = ALEN(bindings); diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 085dd82f..6a11d38d 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -1101,10 +1101,37 @@ actions listed under *key-bindings* can be used here as well. selection_, when the button is released. Default: _Control+BTN\_LEFT-2_. +*select-quote* + Begin an interactive "quote" selection. This is similar to + *select-word*, except an entire quote is selected (that is, + everything inside the quote, excluding the quote + characters). Recognized quote characters are: *"* and *'*. + + If a complete quote cannot be found on the current logical row + (only one quote character, or none are found), the entire row is + selected. + + The selection is finalized, and copied to the _primary selection_, + when the button is released. + + After the initial selection has been made, it behaves like a + normal word, or row selection, depending on whether a quote was + found or not. This affects what happens when, for example, + extending the selection. + + Notes: + - Escaped quote characters are not supported (*"foo \\"bar"* will + match *'foo \\'*, not *'foo "bar'*). + - Foot does not try to handle mismatched quote characters; they + will simply not match. + - Nested quotes (using different quote characters) are supported. + + Default: _BTN\_LEFT-3_. + *select-row* Begin an interactive row-wise selection. The selection is finalized, and copied to the _primary selection_, when the button - is released. Default: _BTN\_LEFT-3_. + is released. Default: _BTN\_LEFT-4_. *select-extend* Interactively extend an existing selection, using the original diff --git a/foot.ini b/foot.ini index 262556e7..00505165 100644 --- a/foot.ini +++ b/foot.ini @@ -200,6 +200,7 @@ # select-extend-character-wise=Control+BTN_RIGHT # select-word=BTN_LEFT-2 # select-word-whitespace=Control+BTN_LEFT-2 -# select-row=BTN_LEFT-3 +# select-quote = BTN_LEFT-3 +# select-row=BTN_LEFT-4 # vim: ft=dosini diff --git a/input.c b/input.c index 51b75233..e3613a90 100644 --- a/input.c +++ b/input.c @@ -472,6 +472,11 @@ execute_binding(struct seat *seat, struct terminal *term, term, seat->mouse.col, seat->mouse.row, SELECTION_WORD_WISE, true); return true; + case BIND_ACTION_SELECT_QUOTE: + selection_start( + term, seat->mouse.col, seat->mouse.row, SELECTION_QUOTE_WISE, false); + break; + case BIND_ACTION_SELECT_ROW: selection_start( term, seat->mouse.col, seat->mouse.row, SELECTION_LINE_WISE, false); diff --git a/key-binding.h b/key-binding.h index 4d3ac541..ea2f3d6d 100644 --- a/key-binding.h +++ b/key-binding.h @@ -49,6 +49,7 @@ enum bind_action_normal { BIND_ACTION_SELECT_EXTEND_CHAR_WISE, BIND_ACTION_SELECT_WORD, BIND_ACTION_SELECT_WORD_WS, + BIND_ACTION_SELECT_QUOTE, BIND_ACTION_SELECT_ROW, BIND_ACTION_KEY_COUNT = BIND_ACTION_UNICODE_INPUT + 1, diff --git a/selection.c b/selection.c index d1a7ea28..f03a3d5c 100644 --- a/selection.c +++ b/selection.c @@ -298,6 +298,7 @@ foreach_selected( switch (term->selection.kind) { case SELECTION_CHAR_WISE: case SELECTION_WORD_WISE: + case SELECTION_QUOTE_WISE: case SELECTION_LINE_WISE: foreach_selected_normal(term, start, end, cb, data); return; @@ -508,9 +509,86 @@ selection_find_word_boundary_right(struct terminal *term, struct coord *pos, } } -void -selection_find_line_boundary_left(struct terminal *term, struct coord *pos, - bool spaces_only) +static bool +selection_find_quote_left(struct terminal *term, struct coord *pos, + char32_t *quote_char) +{ + const struct row *row = grid_row_in_view(term->grid, pos->row); + char32_t wc = row->cells[pos->col].wc; + + if ((*quote_char == '\0' && (wc == '"' || wc == '\'')) || + wc == *quote_char) + { + return false; + } + + int next_row = pos->row; + int next_col = pos->col; + + while (true) { + if (--next_col < 0) { + next_col = term->cols - 1; + if (--next_row < 0) + return false; + + row = grid_row_in_view(term->grid, next_row); + if (row->linebreak) + return false; + } + + wc = row->cells[next_col].wc; + + if ((*quote_char == '\0' && (wc == '"' || wc == '\'')) || + wc == *quote_char) + { + pos->row = next_row; + pos->col = next_col + 1; + xassert(pos->col < term->cols); + + *quote_char = wc; + return true; + } + } +} + +static bool +selection_find_quote_right(struct terminal *term, struct coord *pos, char32_t quote_char) +{ + if (quote_char == '\0') + return false; + + const struct row *row = grid_row_in_view(term->grid, pos->row); + char32_t wc = row->cells[pos->col].wc; + if (wc == quote_char) + return false; + + int next_row = pos->row; + int next_col = pos->col; + + while (true) { + if (++next_col >= term->cols) { + next_col = 0; + if (++next_row >= term->rows) + return false; + + if (row->linebreak) + return false; + + row = grid_row_in_view(term->grid, next_row); + } + + wc = row->cells[next_col].wc; + if (wc == quote_char) { + pos->row = next_row; + pos->col = next_col - 1; + xassert(pos->col >= 0); + return true; + } + } +} + +static void +selection_find_line_boundary_left(struct terminal *term, struct coord *pos) { int next_row = pos->row; pos->col = 0; @@ -530,9 +608,8 @@ selection_find_line_boundary_left(struct terminal *term, struct coord *pos, } } -void -selection_find_line_boundary_right(struct terminal *term, struct coord *pos, - bool spaces_only) +static void +selection_find_line_boundary_right(struct terminal *term, struct coord *pos) { int next_row = pos->row; pos->col = term->cols - 1; @@ -562,6 +639,7 @@ selection_start(struct terminal *term, int col, int row, LOG_DBG("%s selection started at %d,%d", kind == SELECTION_CHAR_WISE ? "character-wise" : kind == SELECTION_WORD_WISE ? "word-wise" : + kind == SELECTION_QUOTE_WISE ? "quote-wise" : kind == SELECTION_LINE_WISE ? "line-wise" : kind == SELECTION_BLOCK ? "block" : "", row, col); @@ -595,10 +673,61 @@ selection_start(struct terminal *term, int col, int row, break; } + case SELECTION_QUOTE_WISE: { + struct coord start = {col, row}, end = {col, row}; + + char32_t quote_char = '\0'; + bool found_left = selection_find_quote_left(term, &start, "e_char); + bool found_right = selection_find_quote_right(term, &end, quote_char); + + if (found_left && !found_right) { + xassert(quote_char != '\0'); + + /* + * Try to flip the quote character we're looking for. + * + * This lets us handle things like: + * + * "nested 'quotes are fun', right" + * + * In the example above, starting the selection at + * "right", will otherwise not match. find-left will find + * the single quote, causing find-right to fail. + * + * By flipping the quote-character, and re-trying, we + * find-left will find the starting double quote, letting + * find-right succeed as well. + */ + + if (quote_char == '\'') + quote_char = '"'; + else if (quote_char == '"') + quote_char = '\''; + + found_left = selection_find_quote_left(term, &start, "e_char); + found_right = selection_find_quote_right(term, &end, quote_char); + } + + if (found_left && found_right) { + term->selection.coords.start = (struct coord){ + start.col, term->grid->view + start.row}; + + term->selection.pivot.start = term->selection.coords.start; + term->selection.pivot.end = (struct coord){end.col, term->grid->view + end.row}; + + term->selection.kind = SELECTION_WORD_WISE; + selection_update(term, end.col, end.row); + break; + } else { + term->selection.kind = SELECTION_LINE_WISE; + /* FALLTHROUGH */ + } + } + case SELECTION_LINE_WISE: { struct coord start = {0, row}, end = {term->cols - 1, row}; - selection_find_line_boundary_left(term, &start, spaces_only); - selection_find_line_boundary_right(term, &end, spaces_only); + selection_find_line_boundary_left(term, &start); + selection_find_line_boundary_right(term, &end); term->selection.coords.start = (struct coord){ start.col, term->grid->view + start.row}; @@ -1052,20 +1181,22 @@ selection_update(struct terminal *term, int col, int row) } break; + case SELECTION_QUOTE_WISE: + BUG("quote-wise selection should always be transformed to either word-wise or line-wise"); + break; + case SELECTION_LINE_WISE: switch (term->selection.direction) { case SELECTION_LEFT: { struct coord end = {0, row}; - selection_find_line_boundary_left( - term, &end, term->selection.spaces_only); + selection_find_line_boundary_left(term, &end); new_end = (struct coord){end.col, term->grid->view + end.row}; break; } case SELECTION_RIGHT: { struct coord end = {col, row}; - selection_find_line_boundary_right( - term, &end, term->selection.spaces_only); + selection_find_line_boundary_right(term, &end); new_end = (struct coord){end.col, term->grid->view + end.row}; break; } @@ -1228,6 +1359,11 @@ selection_extend_normal(struct terminal *term, int col, int row, break; } + case SELECTION_QUOTE_WISE: { + BUG("quote-wise selection should always be transformed to either word-wise or line-wise"); + break; + } + case SELECTION_LINE_WISE: { xassert(new_kind == SELECTION_CHAR_WISE || new_kind == SELECTION_LINE_WISE); @@ -1235,8 +1371,8 @@ selection_extend_normal(struct terminal *term, int col, int row, struct coord pivot_start = {new_start.col, new_start.row - term->grid->view}; struct coord pivot_end = pivot_start; - selection_find_line_boundary_left(term, &pivot_start, spaces_only); - selection_find_line_boundary_right(term, &pivot_end, spaces_only); + selection_find_line_boundary_left(term, &pivot_start); + selection_find_line_boundary_right(term, &pivot_end); term->selection.pivot.start = (struct coord){pivot_start.col, term->grid->view + pivot_start.row}; @@ -1362,6 +1498,7 @@ selection_extend(struct seat *seat, struct terminal *term, case SELECTION_CHAR_WISE: case SELECTION_WORD_WISE: + case SELECTION_QUOTE_WISE: case SELECTION_LINE_WISE: selection_extend_normal(term, col, row, new_kind); break; diff --git a/terminal.h b/terminal.h index 0bba6945..624efd5c 100644 --- a/terminal.h +++ b/terminal.h @@ -300,6 +300,7 @@ enum selection_kind { SELECTION_NONE, SELECTION_CHAR_WISE, SELECTION_WORD_WISE, + SELECTION_QUOTE_WISE, SELECTION_LINE_WISE, SELECTION_BLOCK };