From 1719ff93a75ad886ed53be468f7faa12f530e9f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 19 Sep 2023 16:23:34 +0200 Subject: [PATCH] selection: add support for selecting the contents of a quote This patch changes the default of triple clicking, from selecting the current logical row, to first trying to select the contents of the quote under the cursor, and if failing to find a quote, selecting the current row (like before). This is implemented by adding a new key binding, 'select-quote'. It will search for surrounding quote characters, and if one is found on each side of the cursor, the quote is selected. If not, the entire row is selected instead. Subsequent selection operations will behave as if the selection is either a word selection (a quote was found), or a row selection (no quote found). Escaped quote characters are not supported: "foo \" bar" will match 'foo \', and not 'foo " bar'. Mismatched quotes are not custom handled. They will simply not match. Nested quotes ("123 'abc def' 456") are supported. Closes #1364 --- CHANGELOG.md | 8 +++ config.c | 4 +- doc/foot.ini.5.scd | 29 +++++++- foot.ini | 3 +- input.c | 5 ++ key-binding.h | 1 + selection.c | 165 +++++++++++++++++++++++++++++++++++++++++---- terminal.h | 1 + 8 files changed, 199 insertions(+), 17 deletions(-) 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 };