From 8f3101d8208707b90e2cc93354d37a81cc2ce1ea Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 1 Sep 2026 12:49:49 +0000 Subject: [PATCH 1/2] Extend word commands to use any Unicode whitespace character, GitHub issue 5562 from Hongyi Zhao. --- grid.c | 34 +++++++++++++++---------- options-table.c | 5 ++-- tmux.1 | 19 +++++++++----- tmux.h | 3 ++- utf8.c | 68 ++++++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 105 insertions(+), 24 deletions(-) diff --git a/grid.c b/grid.c index 6ab58eb62..7539d72ef 100644 --- a/grid.c +++ b/grid.c @@ -1,4 +1,4 @@ -/* $OpenBSD: grid.c,v 1.157 2026/08/28 08:11:08 nicm Exp $ */ +/* $OpenBSD: grid.c,v 1.158 2026/09/01 12:49:49 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -1660,21 +1660,29 @@ grid_in_set(struct grid *gd, u_int px, u_int py, const char *set) { struct grid_cell gc, tmp_gc; u_int pxx; + int has_tab, has_space; + + has_tab = (strchr(set, '\t') != NULL); + has_space = (strchr(set, ' ') != NULL); grid_get_cell(gd, px, py, &gc); - if (strchr(set, '\t')) { - if (gc.flags & GRID_FLAG_PADDING) { - pxx = px; - do - grid_get_cell(gd, --pxx, py, &tmp_gc); - while (pxx > 0 && tmp_gc.flags & GRID_FLAG_PADDING); - if (tmp_gc.flags & GRID_FLAG_TAB) - return (tmp_gc.data.width - (px - pxx)); - } else if (gc.flags & GRID_FLAG_TAB) - return (gc.data.width); - } - if (gc.flags & GRID_FLAG_PADDING) + if (gc.flags & GRID_FLAG_PADDING) { + if (!has_tab && !has_space) + return (0); + pxx = px; + do + grid_get_cell(gd, --pxx, py, &tmp_gc); + while (pxx > 0 && tmp_gc.flags & GRID_FLAG_PADDING); + if (((has_tab || has_space) && + (tmp_gc.flags & GRID_FLAG_TAB)) || + (has_space && utf8_has_whitespace(&tmp_gc.data))) + return (tmp_gc.data.width - (px - pxx)); return (0); + } + if ((has_tab || has_space) && (gc.flags & GRID_FLAG_TAB)) + return (gc.data.width); + if (has_space && utf8_has_whitespace(&gc.data)) + return (gc.data.width == 0 ? 1 : gc.data.width); return (utf8_cstrhas(set, &gc.data)); } diff --git a/options-table.c b/options-table.c index ecca20153..81f043864 100644 --- a/options-table.c +++ b/options-table.c @@ -1,4 +1,4 @@ -/* $OpenBSD: options-table.c,v 1.243 2026/08/25 08:37:08 nicm Exp $ */ +/* $OpenBSD: options-table.c,v 1.244 2026/09/01 12:49:49 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -1251,7 +1251,8 @@ const struct options_table_entry options_table[] = { * underscore. */ .default_str = "!\"#$%&'()*+,-./:;<=>?@[\\]^`{|}~", - .text = "Characters considered to separate words." + .text = "Characters considered to separate words; a space matches " + "any character with the Unicode White_Space property." }, /* Window options. */ diff --git a/tmux.1 b/tmux.1 index 2b2b28f17..8ecca4fbd 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1162 2026/08/31 19:34:09 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1163 2026/09/01 12:49:49 nicm Exp $ .\" .\" Copyright (c) 2007 Nicholas Marriott .\" @@ -14,7 +14,7 @@ .\" IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING .\" OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. .\" -.Dd $Mdocdate: August 31 2026 $ +.Dd $Mdocdate: September 1 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -2206,14 +2206,14 @@ Move to the end of the next word. .Xc Same as .Ic next\-word -but use a space alone as the word separator. +but treat all non-whitespace characters as part of a word. .It Xo .Ic next\-space\-end (vi: E) .Xc Same as .Ic next\-word\-end -but use a space alone as the word separator. +but treat all non-whitespace characters as part of a word. .It Xo .Ic other\-end (vi: o) @@ -2287,7 +2287,7 @@ Move to the previous word. .Xc Same as .Ic previous\-word -but use a space alone as the word separator. +but treat all non-whitespace characters as part of a word. .It Xo .Ic rectangle\-on .Xc @@ -2572,8 +2572,8 @@ Word separators can be customized with the session option. Next word moves to the start of the next word, next word end to the end of the next word and previous word to the start of the previous word. -The three next and previous space keys work similarly but use a space alone as -the word separator. +The three next and previous space keys work similarly but treat all +non-whitespace characters as part of a word. Setting .Em word\-separators to the empty string makes next/previous word equivalent to next/previous space. @@ -5647,6 +5647,11 @@ If set to both, a bell and a message are produced. Sets the session's conception of what characters are considered word separators, for the purposes of the next and previous word commands in copy mode. +A space in +.Ar string +matches any character with the Unicode +.Em White_Space +property. .El .Pp Available window options are: diff --git a/tmux.h b/tmux.h index 02059ccc7..c920f8408 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1432 2026/08/31 19:34:09 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1433 2026/09/01 12:49:49 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -4057,6 +4057,7 @@ void session_update_history(struct session *); /* utf8.c */ enum utf8_state utf8_towc (const struct utf8_data *, wchar_t *); enum utf8_state utf8_fromwc(wchar_t wc, struct utf8_data *); +int utf8_has_whitespace(const struct utf8_data *); void utf8_update_width_cache(void); utf8_char utf8_build_one(u_char); enum utf8_state utf8_from_data(const struct utf8_data *, utf8_char *); diff --git a/utf8.c b/utf8.c index e634b43f1..076b1ff56 100644 --- a/utf8.c +++ b/utf8.c @@ -1,4 +1,4 @@ -/* $OpenBSD: utf8.c,v 1.71 2026/05/12 09:37:25 nicm Exp $ */ +/* $OpenBSD: utf8.c,v 1.72 2026/09/01 12:49:49 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -594,6 +594,72 @@ utf8_towc(const struct utf8_data *ud, wchar_t *wc) return (UTF8_DONE); } +/* Check for a Unicode whitespace character. */ +int +utf8_has_whitespace(const struct utf8_data *ud) +{ + struct utf8_data tmp; + wchar_t wc; + u_int offset = 0, size; + u_char ch; + + while (offset < ud->size) { + ch = ud->data[offset]; + if (ch < 0x80) { + wc = ch; + size = 1; + } else { + if (ch >= 0xc2 && ch <= 0xdf) + size = 2; + else if (ch >= 0xe0 && ch <= 0xef) + size = 3; + else if (ch >= 0xf0 && ch <= 0xf4) + size = 4; + else + return (0); + if (size > ud->size - offset) + return (0); + + memset(&tmp, 0, sizeof tmp); + memcpy(tmp.data, ud->data + offset, size); + tmp.size = tmp.have = size; + if (utf8_towc(&tmp, &wc) != UTF8_DONE) + return (0); + } + offset += size; + + switch (wc) { + case 0x0009: + case 0x000A: + case 0x000B: + case 0x000C: + case 0x000D: + case 0x0020: + case 0x0085: + case 0x00A0: + case 0x1680: + case 0x2000: + case 0x2001: + case 0x2002: + case 0x2003: + case 0x2004: + case 0x2005: + case 0x2006: + case 0x2007: + case 0x2008: + case 0x2009: + case 0x200A: + case 0x2028: + case 0x2029: + case 0x202F: + case 0x205F: + case 0x3000: + return (1); + } + } + return (0); +} + /* Convert wide character to UTF-8 character. */ enum utf8_state utf8_fromwc(wchar_t wc, struct utf8_data *ud) From bd8218e85d9305cda3abbe9a54a4288e538632e9 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 1 Sep 2026 13:04:29 +0000 Subject: [PATCH 2/2] When changing selection-mode to line, set up the selection start and end correctly, GitHub issue 5545. --- window-copy.c | 65 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/window-copy.c b/window-copy.c index 572a199fb..c5529a3ff 100644 --- a/window-copy.c +++ b/window-copy.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-copy.c,v 1.428 2026/08/31 07:42:56 nicm Exp $ */ +/* $OpenBSD: window-copy.c,v 1.429 2026/09/01 13:04:29 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -2207,15 +2207,76 @@ window_copy_cmd_selection_mode(struct window_copy_cmd_state *cs) struct window_mode_entry *wme = cs->wme; struct options *so = cs->s->options; struct window_copy_mode_data *data = wme->data; + struct grid_reader gr; const char *s = args_string(cs->wargs, 0); + u_int sx, sy, ex, ey, fx, fy, x, y; if (s == NULL || strcasecmp(s, "char") == 0 || strcasecmp(s, "c") == 0) data->selflag = SEL_CHAR; else if (strcasecmp(s, "word") == 0 || strcasecmp(s, "w") == 0) { data->separators = options_get_string(so, "word-separators"); data->selflag = SEL_WORD; - } else if (strcasecmp(s, "line") == 0 || strcasecmp(s, "l") == 0) + } else if (strcasecmp(s, "line") == 0 || strcasecmp(s, "l") == 0) { data->selflag = SEL_LINE; + if (data->screen.sel == NULL) + return (WINDOW_COPY_CMD_MOVE); + + /* + * Line selection normally starts with select-line, which sets + * up the reset positions used when the cursor changes + * direction. Do the same when changing an existing selection + * to line mode. + */ + if (data->cursordrag == CURSORDRAG_SEL) { + fx = data->endselx; + fy = data->endsely; + } else { + fx = data->selx; + fy = data->sely; + } + + sx = data->selx; + sy = data->sely; + ex = data->endselx; + ey = data->endsely; + if (ey < sy || (ey == sy && ex < sx)) { + x = sx; sx = ex; ex = x; + y = sy; sy = ey; ey = y; + } + grid_reader_start(&gr, data->backing->grid, sx, sy); + grid_reader_cursor_start_of_line(&gr, 1); + grid_reader_get_cursor(&gr, &sx, &sy); + grid_reader_start(&gr, data->backing->grid, ex, ey); + grid_reader_cursor_end_of_line(&gr, 1, 0); + grid_reader_get_cursor(&gr, &ex, &ey); + + data->rectflag = 0; + data->selrx = data->selx = sx; + data->selry = data->sely = sy; + data->endselrx = data->endselx = ex; + data->endselry = data->endsely = ey; + + x = data->cx; + y = screen_hsize(data->backing) + data->cy - data->oy; + data->dx = fx; + data->dy = fy; + if (data->cursordrag != CURSORDRAG_NONE && + (y < fy || (y == fy && x < fx))) { + data->lineflag = LINE_SEL_RIGHT_LEFT; + data->cursordrag = CURSORDRAG_SEL; + window_copy_scroll_to(wme, sx, sy, 1); + } else { + data->lineflag = LINE_SEL_LEFT_RIGHT; + if (data->cursordrag != CURSORDRAG_NONE) { + data->cursordrag = CURSORDRAG_ENDSEL; + x = window_copy_cursor_limit(wme, ey, 0); + window_copy_scroll_to(wme, x, ey, 1); + } + } + if (data->cursordrag == CURSORDRAG_NONE) + window_copy_set_selection(wme, 0, 0); + return (WINDOW_COPY_CMD_REDRAW); + } return (WINDOW_COPY_CMD_MOVE); }