Merge branch 'search-refactor-extend-word'

Closes #421
This commit is contained in:
Daniel Eklöf 2021-05-17 18:20:12 +02:00
commit 3da8dc123f
No known key found for this signature in database
GPG key ID: 5BBD4992C116573F
8 changed files with 179 additions and 106 deletions

View file

@ -75,6 +75,8 @@
* Improved performance on compositors that does not release shm
buffers immediately, e.g. KWin
(https://codeberg.org/dnkl/foot/issues/478).
* `ctrl + w` (_extend-to-word-boundary_) can now be used across lines
(https://codeberg.org/dnkl/foot/issues/421).
### Deprecated
@ -153,6 +155,7 @@
* PTY not being drained when the client application terminates.
* `auto_left_margin` not being limited to `cub1`
(https://codeberg.org/dnkl/foot/issues/441).
* Crash in scrollback search mode when searching beyond the last output.
### Contributors

View file

@ -11,6 +11,7 @@ struct extraction_context {
size_t idx;
size_t empty_count;
size_t newline_count;
bool strip_trailing_empty;
bool failed;
const struct row *last_row;
const struct cell *last_cell;
@ -18,7 +19,7 @@ struct extraction_context {
};
struct extraction_context *
extract_begin(enum selection_kind kind)
extract_begin(enum selection_kind kind, bool strip_trailing_empty)
{
struct extraction_context *ctx = malloc(sizeof(*ctx));
if (unlikely(ctx == NULL)) {
@ -28,6 +29,7 @@ extract_begin(enum selection_kind kind)
*ctx = (struct extraction_context){
.selection_kind = kind,
.strip_trailing_empty = strip_trailing_empty,
};
return ctx;
}
@ -51,10 +53,8 @@ ensure_size(struct extraction_context *ctx, size_t additional_chars)
}
bool
extract_finish(struct extraction_context *ctx, char **text, size_t *len)
extract_finish_wide(struct extraction_context *ctx, wchar_t **text, size_t *len)
{
bool ret = false;
if (text == NULL)
return false;
@ -63,12 +63,24 @@ extract_finish(struct extraction_context *ctx, char **text, size_t *len)
*len = 0;
if (ctx->failed)
goto out;
goto err;
if (!ctx->strip_trailing_empty) {
/* Insert pending newlines, and replace empty cells with spaces */
if (!ensure_size(ctx, ctx->newline_count + ctx->empty_count))
goto err;
for (size_t i = 0; i < ctx->newline_count; i++)
ctx->buf[ctx->idx++] = L'\n';
for (size_t i = 0; i < ctx->empty_count; i++)
ctx->buf[ctx->idx++] = L' ';
}
if (ctx->idx == 0) {
/* Selection of empty cells only */
if (!ensure_size(ctx, 1))
goto out;
goto err;
ctx->buf[ctx->idx++] = L'\0';
} else {
xassert(ctx->idx > 0);
@ -77,12 +89,38 @@ extract_finish(struct extraction_context *ctx, char **text, size_t *len)
ctx->buf[ctx->idx - 1] = L'\0';
else {
if (!ensure_size(ctx, 1))
goto out;
goto err;
ctx->buf[ctx->idx++] = L'\0';
}
}
size_t _len = wcstombs(NULL, ctx->buf, 0);
*text = ctx->buf;
if (len != NULL)
*len = ctx->idx - 1;
free(ctx);
return true;
err:
free(ctx->buf);
free(ctx);
return false;
}
bool
extract_finish(struct extraction_context *ctx, char **text, size_t *len)
{
if (text == NULL)
return false;
if (len != NULL)
*len = 0;
wchar_t *wtext;
if (!extract_finish_wide(ctx, &wtext, NULL))
return false;
bool ret = false;
size_t _len = wcstombs(NULL, wtext, 0);
if (_len == (size_t)-1) {
LOG_ERRNO("failed to convert selection to UTF-8");
goto out;
@ -94,7 +132,7 @@ extract_finish(struct extraction_context *ctx, char **text, size_t *len)
goto out;
}
wcstombs(*text, ctx->buf, _len + 1);
wcstombs(*text, wtext, _len + 1);
if (len != NULL)
*len = _len;
@ -102,8 +140,7 @@ extract_finish(struct extraction_context *ctx, char **text, size_t *len)
ret = true;
out:
free(ctx->buf);
free(ctx);
free(wtext);
return ret;
}
@ -130,6 +167,13 @@ extract_one(const struct terminal *term, const struct row *row,
/* Don't emit newline just yet - only if there are
* non-empty cells following it */
ctx->newline_count++;
if (!ctx->strip_trailing_empty) {
if (!ensure_size(ctx, ctx->empty_count))
goto err;
for (size_t i = 0; i < ctx->empty_count; i++)
ctx->buf[ctx->idx++] = L' ';
}
ctx->empty_count = 0;
}
} else {
@ -138,6 +182,13 @@ extract_one(const struct terminal *term, const struct row *row,
goto err;
ctx->buf[ctx->idx++] = L'\n';
if (!ctx->strip_trailing_empty) {
if (!ensure_size(ctx, ctx->empty_count))
goto err;
for (size_t i = 0; i < ctx->empty_count; i++)
ctx->buf[ctx->idx++] = L' ';
}
ctx->empty_count = 0;
}
}

View file

@ -2,12 +2,14 @@
#include <stddef.h>
#include <stdbool.h>
#include <wchar.h>
#include "terminal.h"
struct extraction_context;
struct extraction_context *extract_begin(enum selection_kind kind);
struct extraction_context *extract_begin(
enum selection_kind kind, bool strip_trailing_empty);
bool extract_one(
const struct terminal *term, const struct row *row, const struct cell *cell,
@ -15,3 +17,5 @@ bool extract_one(
bool extract_finish(
struct extraction_context *context, char **text, size_t *len);
bool extract_finish_wide(
struct extraction_context *context, wchar_t **text, size_t *len);

View file

@ -106,7 +106,7 @@ render_worker_thread(void *_ctx)
}
struct extraction_context *
extract_begin(enum selection_kind kind)
extract_begin(enum selection_kind kind, bool strip_trailing_empty)
{
return NULL;
}

164
search.c
View file

@ -11,6 +11,7 @@
#define LOG_ENABLE_DBG 0
#include "log.h"
#include "config.h"
#include "extract.h"
#include "grid.h"
#include "input.h"
#include "misc.h"
@ -77,6 +78,16 @@ search_ensure_size(struct terminal *term, size_t wanted_size)
return true;
}
static bool
has_wrapped_around(const struct terminal *term, int abs_row_no)
{
int scrollback_start = term->grid->offset + term->rows;
int rebased_row = abs_row_no - scrollback_start + term->grid->num_rows;
rebased_row &= term->grid->num_rows - 1;
return rebased_row == 0;
}
static void
search_cancel_keep_selection(struct terminal *term)
{
@ -327,13 +338,12 @@ search_find_next(struct terminal *term)
for (size_t i = 0; i < term->search.len;) {
if (end_col >= term->cols) {
if (end_row + 1 > grid_row_absolute(term->grid, term->grid->offset + term->rows - 1)) {
/* Don't continue past end of the world */
break;
}
end_row++;
end_row = (end_row + 1) & (term->grid->num_rows - 1);
end_col = 0;
if (has_wrapped_around(term, end_row))
break;
row = term->grid->rows[end_row];
}
@ -428,91 +438,85 @@ search_match_to_end_of_word(struct terminal *term, bool spaces_only)
if (term->search.match_len == 0)
return;
xassert(term->search.match.row != -1);
xassert(term->search.match.col != -1);
xassert(term->selection.end.row != -1);
int end_row = term->search.match.row;
int end_col = term->search.match.col;
size_t len = term->search.match_len;
const bool move_cursor = term->search.cursor == term->search.len;
/* Calculate end coord - note: assumed to be valid */
for (size_t i = 0; i < len; i++) {
for (size_t j = 0; j < wcwidth(term->search.buf[i]); j++) {
if (++end_col >= term->cols) {
end_row = (end_row + 1) & (term->grid->num_rows - 1);
end_col = 0;
}
}
}
const struct coord old_end = term->selection.end;
struct coord new_end = old_end;
struct row *row = NULL;
tll(wchar_t) new_chars = tll_init();
#define newline(coord) __extension__ \
({ \
bool wrapped_around = false; \
if (++(coord).col >= term->cols) { \
(coord).row = ((coord).row + 1) & (term->grid->num_rows - 1); \
(coord).col = 0; \
row = term->grid->rows[(coord).row]; \
if (has_wrapped_around(term, (coord.row))) \
wrapped_around = true; \
} \
wrapped_around; \
})
/* Always append at least one character *if* possible */
bool first = true;
for (size_t r = 0;
r < term->grid->num_rows;
end_row = (end_row + 1) & (term->grid->num_rows - 1), r++)
{
const struct row *row = term->grid->rows[end_row];
if (row == NULL)
break;
bool done = false;
for (; end_col < term->cols; end_col++) {
wchar_t wc = row->cells[end_col].wc;
if (wc >= CELL_SPACER)
continue;
const struct composed *composed = NULL;
if (wc >= CELL_COMB_CHARS_LO &&
wc < (CELL_COMB_CHARS_LO + term->composed_count))
{
composed = &term->composed[wc - CELL_COMB_CHARS_LO];
wc = composed->base;
}
if (wc == 0 || (!first && !isword(wc, spaces_only, term->conf->word_delimiters))) {
done = true;
break;
}
first = false;
tll_push_back(new_chars, wc);
if (composed != NULL) {
for (size_t i = 0; i < composed->count; i++)
tll_push_back(new_chars, composed->combining[i]);
}
}
if (done)
break;
end_col = 0;
}
if (tll_length(new_chars) == 0)
/* First character to consider is the *next* character */
if (newline(new_end))
return;
if (!search_ensure_size(term, term->search.len + tll_length(new_chars)))
xassert(term->grid->rows[new_end.row] != NULL);
/* Find next word boundary */
new_end.row -= term->grid->view;
selection_find_word_boundary_right(term, &new_end, spaces_only);
new_end.row += term->grid->view;
struct coord pos = old_end;
row = term->grid->rows[pos.row];
struct extraction_context *ctx = extract_begin(SELECTION_NONE, false);
if (ctx == NULL)
return;
/* Keep cursor at the end, but don't move it if not */
bool move_cursor = term->search.cursor == term->search.len;
do {
if (newline(pos))
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);
wchar_t *new_text;
size_t new_len;
if (!extract_finish_wide(ctx, &new_text, &new_len))
return;
if (!search_ensure_size(term, term->search.len + new_len))
return;
for (size_t i = 0; i < new_len; i++) {
if (new_text[i] == L'\n') {
/* extract() adds newlines, which we never match against */
continue;
}
term->search.buf[term->search.len++] = new_text[i];
}
/* Append newly found characters to the search buffer */
tll_foreach(new_chars, it)
term->search.buf[term->search.len++] = it->item;
term->search.buf[term->search.len] = L'\0';
free(new_text);
if (move_cursor)
term->search.cursor += tll_length(new_chars);
tll_free(new_chars);
term->search.cursor = term->search.len;
/* search_update_selection() expected end coordinate to be *exclusive* */
newline(new_end);
search_update_selection(
term, term->search.match.row, term->search.match.col, end_row, end_col);
term, term->search.match.row, term->search.match.col,
new_end.row, new_end.col);
term->search.match_len = term->search.len;
#undef newline
}
static size_t
@ -755,12 +759,14 @@ execute_binding(struct seat *seat, struct terminal *term,
case BIND_ACTION_SEARCH_EXTEND_WORD:
search_match_to_end_of_word(term, false);
*update_search_result = *redraw = true;
*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 = *redraw = true;
*update_search_result = false;
*redraw = true;
return true;
case BIND_ACTION_SEARCH_CLIPBOARD_PASTE:

View file

@ -215,7 +215,7 @@ selection_to_text(const struct terminal *term)
if (term->selection.end.row == -1)
return NULL;
struct extraction_context *ctx = extract_begin(term->selection.kind);
struct extraction_context *ctx = extract_begin(term->selection.kind, true);
if (ctx == NULL)
return NULL;
@ -227,9 +227,9 @@ selection_to_text(const struct terminal *term)
return extract_finish(ctx, &text, NULL) ? text : NULL;
}
static void
find_word_boundary_left(struct terminal *term, struct coord *pos,
bool spaces_only)
void
selection_find_word_boundary_left(struct terminal *term, struct coord *pos,
bool spaces_only)
{
const struct row *r = grid_row_in_view(term->grid, pos->row);
wchar_t c = r->cells[pos->col].wc;
@ -299,9 +299,9 @@ find_word_boundary_left(struct terminal *term, struct coord *pos,
}
}
static void
find_word_boundary_right(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 row *r = grid_row_in_view(term->grid, pos->row);
wchar_t c = r->cells[pos->col].wc;
@ -403,8 +403,8 @@ selection_start(struct terminal *term, int col, int row,
case SELECTION_WORD_WISE: {
struct coord start = {col, row}, end = {col, row};
find_word_boundary_left(term, &start, spaces_only);
find_word_boundary_right(term, &end, spaces_only);
selection_find_word_boundary_left(term, &start, spaces_only);
selection_find_word_boundary_right(term, &end, spaces_only);
term->selection.start = (struct coord){
start.col, term->grid->view + start.row};
@ -694,14 +694,16 @@ selection_update(struct terminal *term, int col, int row)
switch (term->selection.direction) {
case SELECTION_LEFT: {
struct coord end = {col, row};
find_word_boundary_left(term, &end, term->selection.spaces_only);
selection_find_word_boundary_left(
term, &end, term->selection.spaces_only);
new_end = (struct coord){end.col, term->grid->view + end.row};
break;
}
case SELECTION_RIGHT: {
struct coord end = {col, row};
find_word_boundary_right(term, &end, term->selection.spaces_only);
selection_find_word_boundary_right(
term, &end, term->selection.spaces_only);
new_end = (struct coord){end.col, term->grid->view + end.row};
break;
}
@ -840,8 +842,10 @@ 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;
find_word_boundary_left(term, &pivot_start, term->selection.spaces_only);
find_word_boundary_right(term, &pivot_end, term->selection.spaces_only);
selection_find_word_boundary_left(
term, &pivot_start, term->selection.spaces_only);
selection_find_word_boundary_right(
term, &pivot_end, term->selection.spaces_only);
term->selection.pivot.start =
(struct coord){pivot_start.col, term->grid->view + pivot_start.row};

View file

@ -74,3 +74,8 @@ void selection_start_scroll_timer(
struct terminal *term, int interval_ns,
enum selection_scroll_direction direction, int col);
void selection_stop_scroll_timer(struct terminal *term);
void selection_find_word_boundary_left(
struct terminal *term, struct coord *pos, bool spaces_only);
void selection_find_word_boundary_right(
struct terminal *term, struct coord *pos, bool spaces_only);

View file

@ -2900,7 +2900,7 @@ static bool
rows_to_text(const struct terminal *term, int start, int end,
char **text, size_t *len)
{
struct extraction_context *ctx = extract_begin(SELECTION_NONE);
struct extraction_context *ctx = extract_begin(SELECTION_NONE, true);
if (ctx == NULL)
return false;