diff --git a/CHANGELOG.md b/CHANGELOG.md index 10ea7595..658faa35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/extract.c b/extract.c index ac5f3897..b9fd3331 100644 --- a/extract.c +++ b/extract.c @@ -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; } } diff --git a/extract.h b/extract.h index 0b3693d2..dc802bc5 100644 --- a/extract.h +++ b/extract.h @@ -2,12 +2,14 @@ #include #include +#include #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); diff --git a/pgo/pgo.c b/pgo/pgo.c index c1620b04..34af7115 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -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; } diff --git a/search.c b/search.c index 911b13c0..3d1ace4b 100644 --- a/search.c +++ b/search.c @@ -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: diff --git a/selection.c b/selection.c index 5bb386ab..f684c2fa 100644 --- a/selection.c +++ b/selection.c @@ -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}; diff --git a/selection.h b/selection.h index 241ec262..295d8d1c 100644 --- a/selection.h +++ b/selection.h @@ -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); diff --git a/terminal.c b/terminal.c index 4a39b659..8264274a 100644 --- a/terminal.c +++ b/terminal.c @@ -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;