diff --git a/README.md b/README.md
index 837a63a5..caeb8e12 100644
--- a/README.md
+++ b/README.md
@@ -118,6 +118,17 @@ available:
Search _forward_ for next match
+* ctrl+w
+
+ Extend current selection (and thus the search criteria) to the end
+ of the word, or the next word if currently at a word separating
+ character.
+
+* ctrl+shiftw
+
+ Same as ctrl+w, except that the only word
+ separating characters are whitespace characters.
+
* escape, ctrl+g
Cancel the search
diff --git a/meson.build b/meson.build
index 0603805b..5c662e9e 100644
--- a/meson.build
+++ b/meson.build
@@ -75,6 +75,7 @@ executable(
'input.c', 'input.h',
'log.c', 'log.h',
'main.c',
+ 'misc.c', 'misc.h',
'osc.c', 'osc.h',
'render.c', 'render.h',
'search.c', 'search.h',
diff --git a/misc.c b/misc.c
new file mode 100644
index 00000000..b8eb3296
--- /dev/null
+++ b/misc.c
@@ -0,0 +1,24 @@
+#include "misc.h"
+
+#include
+
+bool
+isword(wchar_t wc, bool spaces_only)
+{
+ if (spaces_only)
+ return iswgraph(wc);
+
+ switch (wc) {
+ default: return iswgraph(wc);
+
+ case L'(': case L')':
+ case L'[': case L']':
+ case L'{': case L'}':
+ case L'<': case L'>':
+ case L'│': case L'|':
+ case L',':
+ case L'`': case L'"': case L'\'':
+ case L':':
+ return false;
+ }
+}
diff --git a/misc.h b/misc.h
new file mode 100644
index 00000000..06dd3303
--- /dev/null
+++ b/misc.h
@@ -0,0 +1,6 @@
+#pragma once
+
+#include
+#include
+
+bool isword(wchar_t wc, bool spaces_only);
diff --git a/render.c b/render.c
index d17108ec..ccae7d25 100644
--- a/render.c
+++ b/render.c
@@ -795,7 +795,8 @@ render_search_box(struct terminal *term)
wl_subsurface_set_position(
term->window->search_sub_surface,
- term->width - width - margin, term->height - height - margin);
+ max(0, term->width - width - margin),
+ max(0, term->height - height - margin));
wl_surface_damage_buffer(term->window->search_surface, 0, 0, width, height);
wl_surface_attach(term->window->search_surface, buf->wl_buf, 0, 0);
diff --git a/search.c b/search.c
index b10e75ac..3609f81c 100644
--- a/search.c
+++ b/search.c
@@ -11,12 +11,32 @@
#define LOG_ENABLE_DBG 0
#include "log.h"
#include "grid.h"
+#include "misc.h"
#include "render.h"
#include "selection.h"
#include "shm.h"
#define max(x, y) ((x) > (y) ? (x) : (y))
+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;
+ wchar_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 void
search_cancel_keep_selection(struct terminal *term)
{
@@ -64,7 +84,75 @@ search_cancel(struct terminal *term)
}
static void
-search_update(struct terminal *term)
+search_update_selection(struct terminal *term,
+ int start_row, int start_col,
+ int end_row, int end_col)
+{
+ int old_view = term->grid->view;
+ int new_view = start_row;
+
+ /* Prevent scrolling in uninitialized rows */
+ bool all_initialized = false;
+ do {
+ all_initialized = true;
+
+ for (int i = 0; i < term->rows; i++) {
+ int row_no = (new_view + i) % term->grid->num_rows;
+ if (term->grid->rows[row_no] == NULL) {
+ all_initialized = false;
+ new_view--;
+ break;
+ }
+ }
+ } while (!all_initialized);
+
+ /* Don't scroll past scrollback history */
+ int end = (term->grid->offset + term->rows - 1) % term->grid->num_rows;
+ if (end >= term->grid->offset) {
+ /* Not wrapped */
+ if (new_view >= term->grid->offset && new_view <= end)
+ new_view = term->grid->offset;
+ } else {
+ if (new_view >= term->grid->offset || new_view <= end)
+ new_view = term->grid->offset;
+ }
+
+ /* Update view */
+ term->grid->view = new_view;
+ if (new_view != old_view)
+ term_damage_view(term);
+
+ /* Selection endpoint is inclusive */
+ assert(end_col > 0);
+ end_col--;
+
+ /* Begin a new selection if the start coords changed */
+ if (start_row != term->search.match.row ||
+ start_col != term->search.match.col)
+ {
+ int selection_row = start_row - term->grid->view;
+ while (selection_row < 0)
+ selection_row += term->grid->num_rows;
+
+ assert(selection_row >= 0 &&
+ selection_row < term->grid->num_rows);
+ selection_start(term, start_col, selection_row);
+ }
+
+ /* Update selection endpoint */
+ {
+ int selection_row = end_row - term->grid->view;
+ while (selection_row < 0)
+ selection_row += term->grid->num_rows;
+
+ assert(selection_row >= 0 &&
+ selection_row < term->grid->num_rows);
+ selection_update(term, end_col, selection_row);
+ }
+}
+
+static void
+search_find_next(struct terminal *term)
{
bool backward = term->search.direction == SEARCH_BACKWARD;
term->search.direction = SEARCH_BACKWARD;
@@ -106,14 +194,14 @@ search_update(struct terminal *term)
r < term->grid->num_rows;
backward ? ROW_DEC(start_row) : ROW_INC(start_row), r++)
{
- const struct row *row = term->grid->rows[start_row];
- if (row == NULL)
- continue;
-
for (;
backward ? start_col >= 0 : start_col < term->cols;
backward ? start_col-- : start_col++)
{
+ const struct row *row = term->grid->rows[start_row];
+ if (row == NULL)
+ continue;
+
if (wcsncasecmp(&row->cells[start_col].wc, term->search.buf, 1) != 0)
continue;
@@ -129,10 +217,7 @@ search_update(struct terminal *term)
size_t match_len = 0;
for (size_t i = 0; i < term->search.len; i++, match_len++) {
- if (wcsncasecmp(&row->cells[end_col].wc, &term->search.buf[i], 1) != 0)
- break;
-
- if (++end_col >= term->cols) {
+ 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;
@@ -142,6 +227,11 @@ search_update(struct terminal *term)
end_col = 0;
row = term->grid->rows[end_row];
}
+
+ if (wcsncasecmp(&row->cells[end_col].wc, &term->search.buf[i], 1) != 0)
+ break;
+
+ end_col++;
}
if (match_len != term->search.len) {
@@ -153,70 +243,7 @@ search_update(struct terminal *term)
* We matched the entire buffer. Move view to ensure the
* match is visible, create a selection and return.
*/
-
- int old_view = term->grid->view;
- int new_view = start_row;
-
- /* Prevent scrolling in uninitialized rows */
- bool all_initialized = false;
- do {
- all_initialized = true;
-
- for (int i = 0; i < term->rows; i++) {
- int row_no = (new_view + i) % term->grid->num_rows;
- if (term->grid->rows[row_no] == NULL) {
- all_initialized = false;
- new_view--;
- break;
- }
- }
- } while (!all_initialized);
-
- /* Don't scroll past scrollback history */
- int end = (term->grid->offset + term->rows - 1) % term->grid->num_rows;
- if (end >= term->grid->offset) {
- /* Not wrapped */
- if (new_view >= term->grid->offset && new_view <= end)
- new_view = term->grid->offset;
- } else {
- if (new_view >= term->grid->offset || new_view <= end)
- new_view = term->grid->offset;
- }
-
- /* Update view */
- term->grid->view = new_view;
- if (new_view != old_view)
- term_damage_view(term);
-
- /* Selection endpoint is inclusive */
- if (--end_col < 0) {
- end_col = term->cols - 1;
- start_row--;
- }
-
- /* Begin a new selection if the start coords changed */
- if (start_row != term->search.match.row ||
- start_col != term->search.match.col)
- {
- int selection_row = start_row - term->grid->view;
- while (selection_row < 0)
- selection_row += term->grid->num_rows;
-
- assert(selection_row >= 0 &&
- selection_row < term->grid->num_rows);
- selection_start(term, start_col, selection_row);
- }
-
- /* Update selection endpoint */
- {
- int selection_row = end_row - term->grid->view;
- while (selection_row < 0)
- selection_row += term->grid->num_rows;
-
- assert(selection_row >= 0 &&
- selection_row < term->grid->num_rows);
- selection_update(term, end_col, selection_row);
- }
+ search_update_selection(term, start_row, start_col, end_row, end_col);
/* Update match state */
term->search.match.row = start_row;
@@ -237,6 +264,77 @@ search_update(struct terminal *term)
#undef ROW_DEC
}
+static void
+search_match_to_end_of_word(struct terminal *term, bool spaces_only)
+{
+ if (term->search.match_len == 0)
+ return;
+
+ assert(term->search.match.row != -1);
+ assert(term->search.match.col != -1);
+
+ int end_row = term->search.match.row;
+ int end_col = term->search.match.col;
+ size_t len = term->search.match_len;
+
+ /* Calculate end coord - note: assumed to be valid */
+ for (size_t i = 0; i < len; i++) {
+ if (++end_col >= term->cols)
+ end_row = (end_row + 1) % term->grid->num_rows;
+ }
+
+ tll(wchar_t) new_chars = tll_init();
+
+ /* 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, 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 == 0 || (!first && !isword(wc, spaces_only))) {
+ done = true;
+ break;
+ }
+
+ first = false;
+ tll_push_back(new_chars, wc);
+ }
+
+ if (done)
+ break;
+ }
+
+ if (tll_length(new_chars) == 0)
+ return;
+
+ if (!search_ensure_size(term, term->search.len + tll_length(new_chars)))
+ return;
+
+ /* Keep cursor at the end, but don't move it if not */
+ bool move_cursor = term->search.cursor == term->search.len;
+
+ /* 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';
+
+ if (move_cursor)
+ term->search.cursor += tll_length(new_chars);
+
+ tll_free(new_chars);
+
+ search_update_selection(
+ term, term->search.match.row, term->search.match.col, end_row, end_col);
+}
+
static size_t
distance_next_word(const struct terminal *term)
{
@@ -257,7 +355,6 @@ distance_next_word(const struct terminal *term)
break;
}
- LOG_INFO("cursor = %zu, iswspace() = %d", cursor, iswspace(term->search.buf[cursor - 1]));
assert(cursor == term->search.len || !iswspace(term->search.buf[cursor - 1]));
if (cursor < term->search.len && !iswspace(term->search.buf[cursor]))
@@ -299,7 +396,7 @@ search_input(struct terminal *term, uint32_t key, xkb_keysym_t sym, xkb_mod_mask
const xkb_mod_mask_t ctrl = 1 << term->wl->kbd.mod_ctrl;
const xkb_mod_mask_t alt = 1 << term->wl->kbd.mod_alt;
- //const xkb_mod_mask_t shift = 1 << term->wl->kbd.mod_shift;
+ const xkb_mod_mask_t shift = 1 << term->wl->kbd.mod_shift;
//const xkb_mod_mask_t meta = 1 << term->wl->kbd.mod_meta;
enum xkb_compose_status compose_status = xkb_compose_state_get_status(
@@ -446,6 +543,12 @@ search_input(struct terminal *term, uint32_t key, xkb_keysym_t sym, xkb_mod_mask
}
}
+ else if (mods == ctrl && sym == XKB_KEY_w)
+ search_match_to_end_of_word(term, false);
+
+ else if (mods == (ctrl | shift) && sym == XKB_KEY_W)
+ search_match_to_end_of_word(term, true);
+
else {
uint8_t buf[64] = {0};
int count = 0;
@@ -454,6 +557,8 @@ search_input(struct terminal *term, uint32_t key, xkb_keysym_t sym, xkb_mod_mask
count = xkb_compose_state_get_utf8(
term->wl->kbd.xkb_compose_state, (char *)buf, sizeof(buf));
xkb_compose_state_reset(term->wl->kbd.xkb_compose_state);
+ } else if (compose_status == XKB_COMPOSE_CANCELLED) {
+ count = 0;
} else {
count = xkb_state_key_get_utf8(
term->wl->kbd.xkb_state, key, (char *)buf, sizeof(buf));
@@ -468,18 +573,8 @@ search_input(struct terminal *term, uint32_t key, xkb_keysym_t sym, xkb_mod_mask
return;
}
- while (term->search.len + wchars >= term->search.sz) {
- size_t new_sz = term->search.sz == 0 ? 64 : term->search.sz * 2;
- wchar_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;
- }
-
- term->search.buf = new_buf;
- term->search.sz = new_sz;
- }
+ if (!search_ensure_size(term, term->search.len + wchars))
+ return;
assert(term->search.len + wchars < term->search.sz);
@@ -497,7 +592,7 @@ search_input(struct terminal *term, uint32_t key, xkb_keysym_t sym, xkb_mod_mask
}
LOG_DBG("search: buffer: %S", term->search.buf);
- search_update(term);
+ search_find_next(term);
render_refresh(term);
render_search_box(term);
}
diff --git a/selection.c b/selection.c
index b77dc64a..bbe990d7 100644
--- a/selection.c
+++ b/selection.c
@@ -15,6 +15,7 @@
#include "async.h"
#include "grid.h"
+#include "misc.h"
#include "render.h"
#include "vt.h"
@@ -222,27 +223,6 @@ selection_cancel(struct terminal *term)
}
}
-static bool
-isword(wint_t c, bool spaces_only)
-{
- if (spaces_only)
- return !iswspace(c);
-
- switch (c) {
- default: return !iswspace(c);
-
- case L'{': case L'}':
- case L'[': case L']':
- case L'(': case L')':
- case L'`':
- case L'\'':
- case L'"':
- case L',': case L'.':
- case L':': case L';':
- return false;
- }
-}
-
void
selection_mark_word(struct terminal *term, int col, int row, bool spaces_only,
uint32_t serial)