mirror of
https://codeberg.org/dnkl/foot.git
synced 2026-02-05 04:06:08 -05:00
As this means the last call to sarch_matches_next() found a match at the bottom of the view, and then set the iter’s *next* start position to a row outside the view. This is fine, but we need to handle it, by immediately stopping the iter.
1107 lines
32 KiB
C
1107 lines
32 KiB
C
#include "search.h"
|
||
|
||
#include <string.h>
|
||
|
||
#include <wayland-client.h>
|
||
#include <xkbcommon/xkbcommon-compose.h>
|
||
|
||
#define LOG_MODULE "search"
|
||
#define LOG_ENABLE_DBG 0
|
||
#include "log.h"
|
||
#include "char32.h"
|
||
#include "config.h"
|
||
#include "extract.h"
|
||
#include "grid.h"
|
||
#include "input.h"
|
||
#include "key-binding.h"
|
||
#include "misc.h"
|
||
#include "render.h"
|
||
#include "selection.h"
|
||
#include "shm.h"
|
||
#include "util.h"
|
||
#include "xmalloc.h"
|
||
|
||
/*
|
||
* Ensures a "new" viewport doesn't contain any unallocated rows.
|
||
*
|
||
* This is done by first checking if the *first* row is NULL. If so,
|
||
* we move the viewport *forward*, until the first row is non-NULL. At
|
||
* this point, the entire viewport should be allocated rows only.
|
||
*
|
||
* If the first row already was non-NULL, we instead check the *last*
|
||
* row, and if it is NULL, we move the viewport *backward* until the
|
||
* last row is non-NULL.
|
||
*/
|
||
static int
|
||
ensure_view_is_allocated(struct terminal *term, int new_view)
|
||
{
|
||
struct grid *grid = term->grid;
|
||
int view_end = (new_view + term->rows - 1) & (grid->num_rows - 1);
|
||
|
||
if (grid->rows[new_view] == NULL) {
|
||
while (grid->rows[new_view] == NULL)
|
||
new_view = (new_view + 1) & (grid->num_rows - 1);
|
||
}
|
||
|
||
else if (grid->rows[view_end] == NULL) {
|
||
while (grid->rows[view_end] == NULL) {
|
||
new_view--;
|
||
if (new_view < 0)
|
||
new_view += grid->num_rows;
|
||
view_end = (new_view + term->rows - 1) & (grid->num_rows - 1);
|
||
}
|
||
}
|
||
|
||
#if defined(_DEBUG)
|
||
for (size_t r = 0; r < term->rows; r++)
|
||
xassert(grid->rows[(new_view + r) & (grid->num_rows - 1)] != NULL);
|
||
#endif
|
||
|
||
return new_view;
|
||
}
|
||
|
||
static bool
|
||
search_ensure_size(struct terminal *term, size_t wanted_size)
|
||
{
|
||
while (wanted_size >= term->search.sz) {
|
||
size_t new_sz = term->search.sz == 0 ? 64 : term->search.sz * 2;
|
||
char32_t *new_buf = realloc(term->search.buf, new_sz * sizeof(term->search.buf[0]));
|
||
|
||
if (new_buf == NULL) {
|
||
LOG_ERRNO("failed to resize search buffer");
|
||
return false;
|
||
}
|
||
|
||
term->search.buf = new_buf;
|
||
term->search.sz = new_sz;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
static bool
|
||
has_wrapped_around(const struct terminal *term, int abs_row_no)
|
||
{
|
||
const struct grid *grid = term->grid;
|
||
int scrollback_start = grid->offset + term->rows;
|
||
int rebased_row = abs_row_no - scrollback_start + grid->num_rows;
|
||
rebased_row &= grid->num_rows - 1;
|
||
|
||
return rebased_row == 0;
|
||
}
|
||
|
||
static void
|
||
search_cancel_keep_selection(struct terminal *term)
|
||
{
|
||
struct wl_window *win = term->window;
|
||
wayl_win_subsurface_destroy(&win->search);
|
||
|
||
if (term->search.len > 0) {
|
||
free(term->search.last.buf);
|
||
term->search.last.buf = term->search.buf;
|
||
term->search.last.len = term->search.len;
|
||
} else
|
||
free(term->search.buf);
|
||
|
||
term->search.buf = NULL;
|
||
term->search.len = term->search.sz = 0;
|
||
|
||
term->search.cursor = 0;
|
||
term->search.match = (struct coord){-1, -1};
|
||
term->search.match_len = 0;
|
||
term->is_searching = false;
|
||
term->render.search_glyph_offset = 0;
|
||
|
||
/* Reset IME state */
|
||
if (term_ime_is_enabled(term)) {
|
||
term_ime_disable(term);
|
||
term_ime_enable(term);
|
||
}
|
||
|
||
term_xcursor_update(term);
|
||
render_refresh(term);
|
||
}
|
||
|
||
void
|
||
search_begin(struct terminal *term)
|
||
{
|
||
LOG_DBG("search: begin");
|
||
|
||
search_cancel_keep_selection(term);
|
||
selection_cancel(term);
|
||
|
||
/* Reset IME state */
|
||
if (term_ime_is_enabled(term)) {
|
||
term_ime_disable(term);
|
||
term_ime_enable(term);
|
||
}
|
||
|
||
/* On-demand instantiate wayland surface */
|
||
bool ret = wayl_win_subsurface_new(
|
||
term->window, &term->window->search, false);
|
||
xassert(ret);
|
||
|
||
const struct grid *grid = term->grid;
|
||
term->search.original_view = grid->view;
|
||
term->search.view_followed_offset = grid->view == grid->offset;
|
||
term->is_searching = true;
|
||
|
||
term->search.len = 0;
|
||
term->search.sz = 64;
|
||
term->search.buf = xmalloc(term->search.sz * sizeof(term->search.buf[0]));
|
||
term->search.buf[0] = U'\0';
|
||
|
||
term_xcursor_update(term);
|
||
render_refresh_search(term);
|
||
}
|
||
|
||
void
|
||
search_cancel(struct terminal *term)
|
||
{
|
||
if (!term->is_searching)
|
||
return;
|
||
|
||
search_cancel_keep_selection(term);
|
||
selection_cancel(term);
|
||
}
|
||
|
||
void
|
||
search_selection_cancelled(struct terminal *term)
|
||
{
|
||
term->search.match = (struct coord){-1, -1};
|
||
term->search.match_len = 0;
|
||
render_refresh_search(term);
|
||
}
|
||
|
||
static void
|
||
search_update_selection(struct terminal *term, const struct range *match)
|
||
{
|
||
struct grid *grid = term->grid;
|
||
int start_row = match->start.row;
|
||
int start_col = match->start.col;
|
||
int end_row = match->end.row;
|
||
int end_col = match->end.col;
|
||
|
||
bool move_viewport = true;
|
||
|
||
int view_end = (grid->view + term->rows - 1) & (grid->num_rows - 1);
|
||
if (view_end >= grid->view) {
|
||
/* Viewport does *not* wrap around */
|
||
if (start_row >= grid->view && end_row <= view_end)
|
||
move_viewport = false;
|
||
} else {
|
||
/* Viewport wraps */
|
||
if (start_row >= grid->view || end_row <= view_end)
|
||
move_viewport = false;
|
||
}
|
||
|
||
if (move_viewport) {
|
||
int old_view = grid->view;
|
||
int new_view = start_row - term->rows / 2;
|
||
|
||
while (new_view < 0)
|
||
new_view += grid->num_rows;
|
||
|
||
new_view = ensure_view_is_allocated(term, new_view);
|
||
|
||
/* Don't scroll past scrollback history */
|
||
int end = (grid->offset + term->rows - 1) & (grid->num_rows - 1);
|
||
if (end >= grid->offset) {
|
||
/* Not wrapped */
|
||
if (new_view >= grid->offset && new_view <= end)
|
||
new_view = grid->offset;
|
||
} else {
|
||
if (new_view >= grid->offset || new_view <= end)
|
||
new_view = grid->offset;
|
||
}
|
||
|
||
#if defined(_DEBUG)
|
||
/* Verify all to-be-visible rows have been allocated */
|
||
for (int r = 0; r < term->rows; r++)
|
||
xassert(grid->rows[(new_view + r) & (grid->num_rows - 1)] != NULL);
|
||
#endif
|
||
|
||
/* Update view */
|
||
grid->view = new_view;
|
||
if (new_view != old_view)
|
||
term_damage_view(term);
|
||
}
|
||
|
||
#if 0
|
||
/* Selection endpoint is inclusive */
|
||
if (--end_col < 0) {
|
||
end_col = term->cols - 1;
|
||
end_row--;
|
||
}
|
||
#endif
|
||
|
||
/*
|
||
* Begin a new selection if the start coords changed
|
||
*
|
||
* Note: check column against selection.coords, since our “old”
|
||
* start column isn’t reliable - we modify it when searching
|
||
* “next” or “prev”.
|
||
*/
|
||
if (start_row != term->search.match.row ||
|
||
start_col != term->selection.coords.start.col)
|
||
{
|
||
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);
|
||
}
|
||
|
||
/* Update selection endpoint */
|
||
{
|
||
int selection_row = end_row - grid->view + grid->num_rows;
|
||
selection_row &= grid->num_rows - 1;
|
||
selection_update(term, end_col, selection_row);
|
||
}
|
||
}
|
||
|
||
static ssize_t
|
||
matches_cell(const struct terminal *term, const struct cell *cell, size_t search_ofs)
|
||
{
|
||
assert(search_ofs < term->search.len);
|
||
|
||
char32_t base = cell->wc;
|
||
const struct composed *composed = NULL;
|
||
|
||
if (base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI)
|
||
{
|
||
composed = composed_lookup(term->composed, base - CELL_COMB_CHARS_LO);
|
||
base = composed->chars[0];
|
||
}
|
||
|
||
if (composed == NULL && base == 0 && term->search.buf[search_ofs] == U' ')
|
||
return 1;
|
||
|
||
if (c32ncasecmp(&base, &term->search.buf[search_ofs], 1) != 0)
|
||
return -1;
|
||
|
||
if (composed != NULL) {
|
||
if (search_ofs + 1 + 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])
|
||
return -1;
|
||
}
|
||
}
|
||
|
||
return composed != NULL ? 1 + composed->count : 1;
|
||
}
|
||
|
||
static bool
|
||
find_next(struct terminal *term, enum search_direction direction,
|
||
struct coord abs_start, struct coord abs_end, struct range *match)
|
||
{
|
||
#define ROW_DEC(_r) ((_r) = ((_r) - 1 + grid->num_rows) & (grid->num_rows - 1))
|
||
#define ROW_INC(_r) ((_r) = ((_r) + 1) & (grid->num_rows - 1))
|
||
|
||
struct grid *grid = term->grid;
|
||
const bool backward = direction != SEARCH_FORWARD;
|
||
|
||
LOG_DBG("%s: start: %dx%d, end: %dx%d", backward ? "backward" : "forward",
|
||
abs_start.row, abs_start.col, abs_end.row, abs_end.col);
|
||
|
||
xassert(abs_start.row >= 0);
|
||
xassert(abs_start.row < grid->num_rows);
|
||
xassert(abs_start.col >= 0);
|
||
xassert(abs_start.col < term->cols);
|
||
|
||
xassert(abs_end.row >= 0);
|
||
xassert(abs_end.row < grid->num_rows);
|
||
xassert(abs_end.col >= 0);
|
||
xassert(abs_end.col < term->cols);
|
||
|
||
for (int match_start_row = abs_start.row, match_start_col = abs_start.col;
|
||
;
|
||
backward ? ROW_DEC(match_start_row) : ROW_INC(match_start_row)) {
|
||
|
||
const struct row *row = grid->rows[match_start_row];
|
||
if (row == NULL) {
|
||
if (match_start_row == abs_end.row)
|
||
break;
|
||
continue;
|
||
}
|
||
|
||
for (;
|
||
backward ? match_start_col >= 0 : match_start_col < term->cols;
|
||
backward ? match_start_col-- : match_start_col++)
|
||
{
|
||
if (matches_cell(term, &row->cells[match_start_col], 0) < 0) {
|
||
if (match_start_row == abs_end.row &&
|
||
match_start_col == abs_end.col)
|
||
{
|
||
break;
|
||
}
|
||
continue;
|
||
}
|
||
|
||
/*
|
||
* Got a match on the first letter. Now we'll see if the
|
||
* rest of the search buffer matches.
|
||
*/
|
||
|
||
LOG_DBG("search: initial match at row=%d, col=%d",
|
||
match_start_row, match_start_col);
|
||
|
||
int match_end_row = match_start_row;
|
||
int match_end_col = match_start_col;
|
||
const struct row *match_row = row;
|
||
size_t match_len = 0;
|
||
|
||
for (size_t i = 0; i < term->search.len;) {
|
||
if (match_end_col >= term->cols) {
|
||
ROW_INC(match_end_row);
|
||
match_end_col = 0;
|
||
|
||
match_row = grid->rows[match_end_row];
|
||
if (match_row == NULL)
|
||
break;
|
||
}
|
||
|
||
if (match_row->cells[match_end_col].wc >= CELL_SPACER) {
|
||
match_end_col++;
|
||
continue;
|
||
}
|
||
|
||
ssize_t additional_chars = matches_cell(
|
||
term, &match_row->cells[match_end_col], i);
|
||
if (additional_chars < 0)
|
||
break;
|
||
|
||
i += additional_chars;
|
||
match_len += additional_chars;
|
||
match_end_col++;
|
||
}
|
||
|
||
if (match_len != term->search.len) {
|
||
/* Didn't match (completely) */
|
||
continue;
|
||
}
|
||
|
||
*match = (struct range){
|
||
.start = {match_start_col, match_start_row},
|
||
.end = {match_end_col - 1, match_end_row},
|
||
};
|
||
|
||
return true;
|
||
}
|
||
|
||
if (match_start_row == abs_end.row && match_start_col == abs_end.col)
|
||
break;
|
||
|
||
match_start_col = backward ? term->cols - 1 : 0;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
static void
|
||
search_find_next(struct terminal *term, enum search_direction direction)
|
||
{
|
||
struct grid *grid = term->grid;
|
||
|
||
if (term->search.len == 0) {
|
||
term->search.match = (struct coord){-1, -1};
|
||
term->search.match_len = 0;
|
||
selection_cancel(term);
|
||
return;
|
||
}
|
||
|
||
struct coord start = term->search.match;
|
||
size_t len = term->search.match_len;
|
||
|
||
xassert((len == 0 && start.row == -1 && start.col == -1) ||
|
||
(len > 0 && start.row >= 0 && start.col >= 0));
|
||
|
||
if (len == 0) {
|
||
/* No previous match, start from the top, or bottom, of the scrollback */
|
||
switch (direction) {
|
||
case SEARCH_FORWARD:
|
||
start.row = grid_row_absolute_in_view(grid, 0);
|
||
start.col = 0;
|
||
break;
|
||
|
||
case SEARCH_BACKWARD:
|
||
case SEARCH_BACKWARD_SAME_POSITION:
|
||
start.row = grid_row_absolute_in_view(grid, term->rows - 1);
|
||
start.col = term->cols - 1;
|
||
break;
|
||
}
|
||
} else {
|
||
/* Continue from last match */
|
||
xassert(start.row >= 0);
|
||
xassert(start.col >= 0);
|
||
|
||
switch (direction) {
|
||
case SEARCH_BACKWARD_SAME_POSITION:
|
||
break;
|
||
|
||
case SEARCH_BACKWARD:
|
||
if (--start.col < 0) {
|
||
start.col = term->cols - 1;
|
||
start.row += grid->num_rows - 1;
|
||
start.row &= grid->num_rows - 1;
|
||
}
|
||
break;
|
||
|
||
case SEARCH_FORWARD:
|
||
if (++start.col >= term->cols) {
|
||
start.col = 0;
|
||
start.row++;
|
||
start.row &= grid->num_rows - 1;
|
||
}
|
||
break;
|
||
}
|
||
|
||
xassert(start.row >= 0);
|
||
xassert(start.row < grid->num_rows);
|
||
xassert(start.col >= 0);
|
||
xassert(start.col < term->cols);
|
||
}
|
||
|
||
LOG_DBG(
|
||
"update: %s: starting at row=%d col=%d "
|
||
"(offset = %d, view = %d)",
|
||
direction != SEARCH_FORWARD ? "backward" : "forward",
|
||
start.row, start.col,
|
||
grid->offset, grid->view);
|
||
|
||
struct coord end = start;
|
||
switch (direction) {
|
||
case SEARCH_FORWARD:
|
||
/* Search forward, until we reach the cell *before* current start */
|
||
if (--end.col < 0) {
|
||
end.col = term->cols - 1;
|
||
end.row += grid->num_rows - 1;
|
||
end.row &= grid->num_rows - 1;
|
||
}
|
||
break;
|
||
|
||
case SEARCH_BACKWARD:
|
||
case SEARCH_BACKWARD_SAME_POSITION:
|
||
/* Search backwards, until we reach the cell *after* current start */
|
||
if (++end.col >= term->cols) {
|
||
end.col = 0;
|
||
end.row++;
|
||
end.row &= grid->num_rows - 1;
|
||
}
|
||
break;
|
||
}
|
||
|
||
struct range match;
|
||
bool found = find_next(term, direction, start, end, &match);
|
||
|
||
if (found) {
|
||
LOG_DBG("primary match found at %dx%d",
|
||
match.start.row, match.start.col);
|
||
search_update_selection(term, &match);
|
||
term->search.match = match.start;
|
||
term->search.match_len = term->search.len;
|
||
} else {
|
||
LOG_DBG("no match");
|
||
term->search.match = (struct coord){-1, -1};
|
||
term->search.match_len = 0;
|
||
selection_cancel(term);
|
||
}
|
||
#undef ROW_DEC
|
||
}
|
||
|
||
struct search_match_iterator
|
||
search_matches_new_iter(struct terminal *term)
|
||
{
|
||
return (struct search_match_iterator){
|
||
.term = term,
|
||
.start = {-2, -2},
|
||
};
|
||
}
|
||
|
||
struct range
|
||
search_matches_next(struct search_match_iterator *iter)
|
||
{
|
||
struct terminal *term = iter->term;
|
||
struct grid *grid = term->grid;
|
||
|
||
if (term->search.match_len == 0)
|
||
goto no_match;
|
||
|
||
struct range match;
|
||
bool found;
|
||
|
||
const bool return_primary_match =
|
||
iter->start.row == -2 && term->selection.coords.end.row >= 0;
|
||
|
||
if (return_primary_match) {
|
||
/* First, return the primary match */
|
||
match = term->selection.coords;
|
||
found = true;
|
||
}
|
||
|
||
else if (iter->start.row >= term->rows) {
|
||
goto no_match;
|
||
}
|
||
|
||
else {
|
||
xassert(iter->start.row >= 0);
|
||
xassert(iter->start.row < term->rows);
|
||
|
||
struct coord abs_start = iter->start;
|
||
abs_start.row = grid_row_absolute_in_view(grid, abs_start.row);
|
||
|
||
struct coord abs_end = {
|
||
term->cols - 1,
|
||
grid_row_absolute_in_view(grid, term->rows - 1)};
|
||
|
||
found = find_next(term, SEARCH_FORWARD, abs_start, abs_end, &match);
|
||
}
|
||
|
||
if (found) {
|
||
LOG_DBG("match at %dx%d-%dx%d",
|
||
match.start.row, match.start.col,
|
||
match.end.row, match.end.col);
|
||
|
||
/* Convert absolute row numbers back to view relative */
|
||
match.start.row = match.start.row - grid->view + grid->num_rows;
|
||
match.start.row &= grid->num_rows - 1;
|
||
match.end.row = match.end.row - grid->view + grid->num_rows;
|
||
match.end.row &= grid->num_rows - 1;
|
||
|
||
xassert(match.start.row >= 0);
|
||
xassert(match.start.row < term->rows);
|
||
xassert(match.end.row >= 0);
|
||
xassert(match.end.row < term->rows);
|
||
|
||
xassert(match.end.row > match.start.row ||
|
||
(match.end.row == match.start.row &&
|
||
match.end.col >= match.start.col));
|
||
|
||
if (return_primary_match) {
|
||
iter->start.row = 0;
|
||
iter->start.col = 0;
|
||
} else {
|
||
/* Continue at next column, next time */
|
||
iter->start.row = match.start.row;
|
||
iter->start.col = match.start.col + 1;
|
||
|
||
if (iter->start.col >= term->cols) {
|
||
iter->start.col = 0;
|
||
iter->start.row++; /* Overflow is caught in next iteration */
|
||
}
|
||
|
||
xassert(iter->start.row >= 0);
|
||
xassert(iter->start.row <= term->rows);
|
||
xassert(iter->start.col >= 0);
|
||
xassert(iter->start.col < term->cols);
|
||
|
||
if (match.start.row == term->search.match.row &&
|
||
match.start.col == term->search.match.col)
|
||
{
|
||
/* Primary match is handled explicitly */
|
||
LOG_DBG("primary match: skipping");
|
||
return search_matches_next(iter);
|
||
}
|
||
}
|
||
return match;
|
||
}
|
||
|
||
no_match:
|
||
iter->start.row = -1;
|
||
iter->start.col = -1;
|
||
return (struct range){{-1, -1}, {-1, -1}};
|
||
}
|
||
|
||
static void
|
||
add_wchars(struct terminal *term, char32_t *src, size_t count)
|
||
{
|
||
/* Strip non-printable characters */
|
||
for (size_t i = 0, j = 0, orig_count = count; i < orig_count; i++) {
|
||
if (isc32print(src[i]))
|
||
src[j++] = src[i];
|
||
else
|
||
count--;
|
||
}
|
||
|
||
if (!search_ensure_size(term, term->search.len + count))
|
||
return;
|
||
|
||
xassert(term->search.len + count < term->search.sz);
|
||
|
||
memmove(&term->search.buf[term->search.cursor + count],
|
||
&term->search.buf[term->search.cursor],
|
||
(term->search.len - term->search.cursor) * sizeof(char32_t));
|
||
|
||
memcpy(&term->search.buf[term->search.cursor], src, count * sizeof(char32_t));
|
||
|
||
term->search.len += count;
|
||
term->search.cursor += count;
|
||
term->search.buf[term->search.len] = U'\0';
|
||
}
|
||
|
||
void
|
||
search_add_chars(struct terminal *term, const char *src, size_t count)
|
||
{
|
||
size_t chars = mbsntoc32(NULL, src, count, 0);
|
||
if (chars == (size_t)-1) {
|
||
LOG_ERRNO("failed to convert %.*s to Unicode", (int)count, src);
|
||
return;
|
||
}
|
||
|
||
char32_t c32s[chars + 1];
|
||
mbsntoc32(c32s, src, count, chars);
|
||
add_wchars(term, c32s, chars);
|
||
}
|
||
|
||
static void
|
||
search_match_to_end_of_word(struct terminal *term, bool spaces_only)
|
||
{
|
||
if (term->search.match_len == 0)
|
||
return;
|
||
|
||
xassert(term->selection.coords.end.row != -1);
|
||
|
||
struct grid *grid = term->grid;
|
||
const bool move_cursor = term->search.cursor == term->search.len;
|
||
|
||
const struct coord old_end = term->selection.coords.end;
|
||
struct coord new_end = old_end;
|
||
struct row *row = NULL;
|
||
|
||
/* 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))
|
||
return;
|
||
|
||
xassert(grid->rows[new_end.row] != NULL);
|
||
|
||
/* Find next word boundary */
|
||
new_end.row -= grid->view;
|
||
selection_find_word_boundary_right(term, &new_end, spaces_only);
|
||
new_end.row += grid->view;
|
||
|
||
struct coord pos = old_end;
|
||
row = grid->rows[pos.row];
|
||
|
||
struct extraction_context *ctx = extract_begin(SELECTION_NONE, false);
|
||
if (ctx == NULL)
|
||
return;
|
||
|
||
do {
|
||
if (!advance_pos(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);
|
||
|
||
char32_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] == U'\n') {
|
||
/* extract() adds newlines, which we never match against */
|
||
continue;
|
||
}
|
||
|
||
term->search.buf[term->search.len++] = new_text[i];
|
||
}
|
||
|
||
term->search.buf[term->search.len] = U'\0';
|
||
free(new_text);
|
||
|
||
if (move_cursor)
|
||
term->search.cursor = term->search.len;
|
||
|
||
struct range match = {.start = term->search.match, .end = new_end};
|
||
search_update_selection(term, &match);
|
||
|
||
term->search.match_len = term->search.len;
|
||
|
||
#undef advance_pos
|
||
}
|
||
|
||
static size_t
|
||
distance_next_word(const struct terminal *term)
|
||
{
|
||
size_t cursor = term->search.cursor;
|
||
|
||
/* First eat non-whitespace. This is the word we're skipping past */
|
||
while (cursor < term->search.len) {
|
||
if (isc32space(term->search.buf[cursor++]))
|
||
break;
|
||
}
|
||
|
||
xassert(cursor == term->search.len || isc32space(term->search.buf[cursor - 1]));
|
||
|
||
/* Now skip past whitespace, so that we end up at the beginning of
|
||
* the next word */
|
||
while (cursor < term->search.len) {
|
||
if (!isc32space(term->search.buf[cursor++]))
|
||
break;
|
||
}
|
||
|
||
xassert(cursor == term->search.len || !isc32space(term->search.buf[cursor - 1]));
|
||
|
||
if (cursor < term->search.len && !isc32space(term->search.buf[cursor]))
|
||
cursor--;
|
||
|
||
return cursor - term->search.cursor;
|
||
}
|
||
|
||
static size_t
|
||
distance_prev_word(const struct terminal *term)
|
||
{
|
||
int cursor = term->search.cursor;
|
||
|
||
/* First, eat whitespace prefix */
|
||
while (cursor > 0) {
|
||
if (!isc32space(term->search.buf[--cursor]))
|
||
break;
|
||
}
|
||
|
||
xassert(cursor == 0 || !isc32space(term->search.buf[cursor]));
|
||
|
||
/* Now eat non-whitespace. This is the word we're skipping past */
|
||
while (cursor > 0) {
|
||
if (isc32space(term->search.buf[--cursor]))
|
||
break;
|
||
}
|
||
|
||
xassert(cursor == 0 || isc32space(term->search.buf[cursor]));
|
||
if (cursor > 0 && isc32space(term->search.buf[cursor]))
|
||
cursor++;
|
||
|
||
return term->search.cursor - cursor;
|
||
}
|
||
|
||
static void
|
||
from_clipboard_cb(char *text, size_t size, void *user)
|
||
{
|
||
struct terminal *term = user;
|
||
search_add_chars(term, text, size);
|
||
}
|
||
|
||
static void
|
||
from_clipboard_done(void *user)
|
||
{
|
||
struct terminal *term = user;
|
||
|
||
LOG_DBG("search: buffer: %ls", (const wchar_t *)term->search.buf);
|
||
search_find_next(term, SEARCH_BACKWARD_SAME_POSITION);
|
||
render_refresh_search(term);
|
||
}
|
||
|
||
static bool
|
||
execute_binding(struct seat *seat, struct terminal *term,
|
||
const struct key_binding *binding, uint32_t serial,
|
||
bool *update_search_result, enum search_direction *direction,
|
||
bool *redraw)
|
||
{
|
||
*update_search_result = *redraw = false;
|
||
const enum bind_action_search action = binding->action;
|
||
|
||
struct grid *grid = term->grid;
|
||
|
||
switch (action) {
|
||
case BIND_ACTION_SEARCH_NONE:
|
||
return false;
|
||
|
||
case BIND_ACTION_SEARCH_CANCEL:
|
||
if (term->search.view_followed_offset)
|
||
grid->view = grid->offset;
|
||
else {
|
||
grid->view = ensure_view_is_allocated(
|
||
term, term->search.original_view);
|
||
}
|
||
term_damage_view(term);
|
||
search_cancel(term);
|
||
return true;
|
||
|
||
case BIND_ACTION_SEARCH_COMMIT:
|
||
selection_finalize(seat, term, serial);
|
||
search_cancel_keep_selection(term);
|
||
return true;
|
||
|
||
case BIND_ACTION_SEARCH_FIND_PREV:
|
||
if (term->search.last.buf != NULL && term->search.len == 0) {
|
||
add_wchars(term, term->search.last.buf, term->search.last.len);
|
||
|
||
free(term->search.last.buf);
|
||
term->search.last.buf = NULL;
|
||
term->search.last.len = 0;
|
||
}
|
||
|
||
*direction = SEARCH_BACKWARD;
|
||
*update_search_result = *redraw = true;
|
||
return true;
|
||
|
||
case BIND_ACTION_SEARCH_FIND_NEXT:
|
||
if (term->search.last.buf != NULL && term->search.len == 0) {
|
||
add_wchars(term, term->search.last.buf, term->search.last.len);
|
||
|
||
free(term->search.last.buf);
|
||
term->search.last.buf = NULL;
|
||
term->search.last.len = 0;
|
||
}
|
||
|
||
*direction = SEARCH_FORWARD;
|
||
*update_search_result = *redraw = true;
|
||
return true;
|
||
|
||
case BIND_ACTION_SEARCH_EDIT_LEFT:
|
||
if (term->search.cursor > 0) {
|
||
term->search.cursor--;
|
||
*redraw = true;
|
||
}
|
||
return true;
|
||
|
||
case BIND_ACTION_SEARCH_EDIT_LEFT_WORD: {
|
||
size_t diff = distance_prev_word(term);
|
||
term->search.cursor -= diff;
|
||
xassert(term->search.cursor <= term->search.len);
|
||
|
||
if (diff > 0)
|
||
*redraw = true;
|
||
return true;
|
||
}
|
||
|
||
case BIND_ACTION_SEARCH_EDIT_RIGHT:
|
||
if (term->search.cursor < term->search.len) {
|
||
term->search.cursor++;
|
||
*redraw = true;
|
||
}
|
||
return true;
|
||
|
||
case BIND_ACTION_SEARCH_EDIT_RIGHT_WORD: {
|
||
size_t diff = distance_next_word(term);
|
||
term->search.cursor += diff;
|
||
xassert(term->search.cursor <= term->search.len);
|
||
|
||
if (diff > 0)
|
||
*redraw = true;
|
||
return true;
|
||
}
|
||
|
||
case BIND_ACTION_SEARCH_EDIT_HOME:
|
||
if (term->search.cursor != 0) {
|
||
term->search.cursor = 0;
|
||
*redraw = true;
|
||
}
|
||
return true;
|
||
|
||
case BIND_ACTION_SEARCH_EDIT_END:
|
||
if (term->search.cursor != term->search.len) {
|
||
term->search.cursor = term->search.len;
|
||
*redraw = true;
|
||
}
|
||
return true;
|
||
|
||
case BIND_ACTION_SEARCH_DELETE_PREV:
|
||
if (term->search.cursor > 0) {
|
||
memmove(
|
||
&term->search.buf[term->search.cursor - 1],
|
||
&term->search.buf[term->search.cursor],
|
||
(term->search.len - term->search.cursor) * sizeof(char32_t));
|
||
term->search.cursor--;
|
||
term->search.buf[--term->search.len] = U'\0';
|
||
*update_search_result = *redraw = true;
|
||
}
|
||
return true;
|
||
|
||
case BIND_ACTION_SEARCH_DELETE_PREV_WORD: {
|
||
size_t diff = distance_prev_word(term);
|
||
size_t old_cursor = term->search.cursor;
|
||
size_t new_cursor = old_cursor - diff;
|
||
|
||
if (diff > 0) {
|
||
memmove(&term->search.buf[new_cursor],
|
||
&term->search.buf[old_cursor],
|
||
(term->search.len - old_cursor) * sizeof(char32_t));
|
||
|
||
term->search.len -= diff;
|
||
term->search.cursor = new_cursor;
|
||
*update_search_result = *redraw = true;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
case BIND_ACTION_SEARCH_DELETE_NEXT:
|
||
if (term->search.cursor < term->search.len) {
|
||
memmove(
|
||
&term->search.buf[term->search.cursor],
|
||
&term->search.buf[term->search.cursor + 1],
|
||
(term->search.len - term->search.cursor - 1) * sizeof(char32_t));
|
||
term->search.buf[--term->search.len] = U'\0';
|
||
*update_search_result = *redraw = true;
|
||
}
|
||
return true;
|
||
|
||
case BIND_ACTION_SEARCH_DELETE_NEXT_WORD: {
|
||
size_t diff = distance_next_word(term);
|
||
size_t cursor = term->search.cursor;
|
||
|
||
if (diff > 0) {
|
||
memmove(&term->search.buf[cursor],
|
||
&term->search.buf[cursor + diff],
|
||
(term->search.len - (cursor + diff)) * sizeof(char32_t));
|
||
|
||
term->search.len -= diff;
|
||
*update_search_result = *redraw = true;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
case BIND_ACTION_SEARCH_EXTEND_WORD:
|
||
search_match_to_end_of_word(term, false);
|
||
*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;
|
||
return true;
|
||
|
||
case BIND_ACTION_SEARCH_CLIPBOARD_PASTE:
|
||
text_from_clipboard(
|
||
seat, term, &from_clipboard_cb, &from_clipboard_done, term);
|
||
*update_search_result = *redraw = true;
|
||
return true;
|
||
|
||
case BIND_ACTION_SEARCH_PRIMARY_PASTE:
|
||
text_from_primary(
|
||
seat, term, &from_clipboard_cb, &from_clipboard_done, term);
|
||
*update_search_result = *redraw = true;
|
||
return true;
|
||
|
||
case BIND_ACTION_SEARCH_COUNT:
|
||
BUG("Invalid action type");
|
||
return true;
|
||
}
|
||
|
||
BUG("Unhandled action type");
|
||
return false;
|
||
}
|
||
|
||
void
|
||
search_input(struct seat *seat, struct terminal *term,
|
||
const struct key_binding_set *bindings, uint32_t key,
|
||
xkb_keysym_t sym, xkb_mod_mask_t mods, xkb_mod_mask_t consumed,
|
||
xkb_mod_mask_t locked,
|
||
const xkb_keysym_t *raw_syms, size_t raw_count,
|
||
uint32_t serial)
|
||
{
|
||
LOG_DBG("search: input: sym=%d/0x%x, mods=0x%08x, consumed=0x%08x",
|
||
sym, sym, mods, consumed);
|
||
|
||
const xkb_mod_mask_t bind_mods =
|
||
mods & seat->kbd.bind_significant & ~locked;
|
||
const xkb_mod_mask_t bind_consumed =
|
||
consumed & seat->kbd.bind_significant & ~locked;
|
||
enum xkb_compose_status compose_status = seat->kbd.xkb_compose_state != NULL
|
||
? xkb_compose_state_get_status(seat->kbd.xkb_compose_state)
|
||
: XKB_COMPOSE_NOTHING;
|
||
|
||
enum search_direction search_direction = SEARCH_BACKWARD_SAME_POSITION;
|
||
bool update_search_result = false;
|
||
bool redraw = false;
|
||
|
||
/* Key bindings */
|
||
tll_foreach(bindings->search, it) {
|
||
const struct key_binding *bind = &it->item;
|
||
|
||
/* Match translated symbol */
|
||
if (bind->k.sym == sym &&
|
||
bind->mods == (bind_mods & ~bind_consumed)) {
|
||
|
||
if (execute_binding(seat, term, bind, serial,
|
||
&update_search_result, &search_direction,
|
||
&redraw))
|
||
{
|
||
goto update_search;
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (bind->mods != bind_mods || bind_mods != (mods & ~locked))
|
||
continue;
|
||
|
||
/* Match untranslated symbols */
|
||
for (size_t i = 0; i < raw_count; i++) {
|
||
if (bind->k.sym == raw_syms[i]) {
|
||
if (execute_binding(seat, term, bind, serial,
|
||
&update_search_result, &search_direction,
|
||
&redraw))
|
||
{
|
||
goto update_search;
|
||
}
|
||
return;
|
||
}
|
||
}
|
||
|
||
/* Match raw key code */
|
||
tll_foreach(bind->k.key_codes, code) {
|
||
if (code->item == key) {
|
||
if (execute_binding(seat, term, bind, serial,
|
||
&update_search_result, &search_direction,
|
||
&redraw))
|
||
{
|
||
goto update_search;
|
||
}
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
uint8_t buf[64] = {0};
|
||
int count = 0;
|
||
|
||
if (compose_status == XKB_COMPOSE_COMPOSED) {
|
||
count = xkb_compose_state_get_utf8(
|
||
seat->kbd.xkb_compose_state, (char *)buf, sizeof(buf));
|
||
xkb_compose_state_reset(seat->kbd.xkb_compose_state);
|
||
} else if (compose_status == XKB_COMPOSE_CANCELLED) {
|
||
count = 0;
|
||
} else {
|
||
count = xkb_state_key_get_utf8(
|
||
seat->kbd.xkb_state, key, (char *)buf, sizeof(buf));
|
||
}
|
||
|
||
update_search_result = redraw = count > 0;
|
||
search_direction = SEARCH_BACKWARD_SAME_POSITION;
|
||
|
||
if (count == 0)
|
||
return;
|
||
|
||
search_add_chars(term, (const char *)buf, count);
|
||
|
||
update_search:
|
||
LOG_DBG("search: buffer: %ls", (const wchar_t *)term->search.buf);
|
||
if (update_search_result)
|
||
search_find_next(term, search_direction);
|
||
if (redraw)
|
||
render_refresh_search(term);
|
||
}
|