More accurate vi(1) word navigation in copy mode and on the status line. This

changes the meaning of the word-separators option - setting it to the empty
string is equivalent to the previous behavior. From Will Noble in GitHub issue
2693.
This commit is contained in:
Nicholas Marriott 2021-05-13 08:49:58 +01:00
parent f03b27c72b
commit 022d0210c5
11 changed files with 564 additions and 176 deletions

View File

@ -1,6 +1,8 @@
CHANGES FROM 3.2 TO 3.3
XXX
* More accurate vi(1) word navigation in copy mode and on the status line. This
changes the meaning of the word-separators option - setting it to the empty
string is equivalent to the previous behavior.
CHANGES FROM 3.1c TO 3.2

View File

@ -153,6 +153,29 @@ grid_reader_cursor_end_of_line(struct grid_reader *gr, int wrap, int all)
gr->cx = grid_reader_line_length(gr);
}
/* Handle line wrapping while moving the cursor. */
static int
grid_reader_handle_wrap(struct grid_reader *gr, u_int *xx, u_int *yy)
{
/*
* Make sure the cursor lies within the grid reader's bounding area,
* wrapping to the next line as necessary. Return zero if the cursor
* would wrap past the bottom of the grid.
*/
while (gr->cx > *xx) {
if (gr->cy == *yy)
return (0);
grid_reader_cursor_start_of_line(gr, 0);
grid_reader_cursor_down(gr);
if (grid_get_line(gr->gd, gr->cy)->flags & GRID_LINE_WRAPPED)
*xx = gr->gd->sx - 1;
else
*xx = grid_reader_line_length(gr);
}
return (1);
}
/* Check if character under cursor is in set. */
int
grid_reader_in_set(struct grid_reader *gr, const char *set)
@ -170,7 +193,6 @@ void
grid_reader_cursor_next_word(struct grid_reader *gr, const char *separators)
{
u_int xx, yy;
int expected = 0;
/* Do not break up wrapped words. */
if (grid_get_line(gr->gd, gr->cy)->flags & GRID_LINE_WRAPPED)
@ -180,33 +202,35 @@ grid_reader_cursor_next_word(struct grid_reader *gr, const char *separators)
yy = gr->gd->hsize + gr->gd->sy - 1;
/*
* If we started inside a word, skip over word characters. Then skip
* over separators till the next word.
* When navigating via spaces (for example with next-space) separators
* should be empty.
*
* expected is initially set to 0 for the former and then 1 for the
* latter. It is finally set to 0 when the beginning of the next word is
* found.
* If we started on a separator that is not whitespace, skip over
* subsequent separators that are not whitespace. Otherwise, if we
* started on a non-whitespace character, skip over subsequent
* characters that are neither whitespace nor separators. Then, skip
* over whitespace (if any) until the next non-whitespace character.
*/
do {
while (gr->cx > xx ||
grid_reader_in_set(gr, separators) == expected) {
/* Move down if we are past the end of the line. */
if (gr->cx > xx) {
if (gr->cy == yy)
return;
grid_reader_cursor_start_of_line(gr, 0);
grid_reader_cursor_down(gr);
if (grid_get_line(gr->gd, gr->cy)->flags &
GRID_LINE_WRAPPED)
xx = gr->gd->sx - 1;
else
xx = grid_reader_line_length(gr);
} else
if (!grid_reader_handle_wrap(gr, &xx, &yy))
return;
if (!grid_reader_in_set(gr, WHITESPACE)) {
if (grid_reader_in_set(gr, separators)) {
do
gr->cx++;
while (grid_reader_handle_wrap(gr, &xx, &yy) &&
grid_reader_in_set(gr, separators) &&
!grid_reader_in_set(gr, WHITESPACE));
} else {
do
gr->cx++;
while (grid_reader_handle_wrap(gr, &xx, &yy) &&
!(grid_reader_in_set(gr, separators) ||
grid_reader_in_set(gr, WHITESPACE)));
}
expected = !expected;
} while (expected == 1);
}
while (grid_reader_handle_wrap(gr, &xx, &yy) &&
grid_reader_in_set(gr, WHITESPACE))
gr->cx++;
}
/* Move cursor to the end of the next word. */
@ -214,7 +238,6 @@ void
grid_reader_cursor_next_word_end(struct grid_reader *gr, const char *separators)
{
u_int xx, yy;
int expected = 1;
/* Do not break up wrapped words. */
if (grid_get_line(gr->gd, gr->cy)->flags & GRID_LINE_WRAPPED)
@ -224,49 +247,54 @@ grid_reader_cursor_next_word_end(struct grid_reader *gr, const char *separators)
yy = gr->gd->hsize + gr->gd->sy - 1;
/*
* If we started on a separator, skip over separators. Then skip over
* word characters till the next separator.
* When navigating via spaces (for example with next-space), separators
* should be empty in both modes.
*
* expected is initially set to 1 for the former and then 1 for the
* latter. It is finally set to 1 when the end of the next word is
* found.
* If we started on a whitespace, move until reaching the first
* non-whitespace character. If that character is a separator, treat
* subsequent separators as a word, and continue moving until the first
* non-separator. Otherwise, continue moving until the first separator
* or whitespace.
*/
do {
while (gr->cx > xx ||
grid_reader_in_set(gr, separators) == expected) {
/* Move down if we are past the end of the line. */
if (gr->cx > xx) {
if (gr->cy == yy)
return;
grid_reader_cursor_start_of_line(gr, 0);
grid_reader_cursor_down(gr);
if (grid_get_line(gr->gd, gr->cy)->flags &
GRID_LINE_WRAPPED)
xx = gr->gd->sx - 1;
else
xx = grid_reader_line_length(gr);
} else
while (grid_reader_handle_wrap(gr, &xx, &yy)) {
if (grid_reader_in_set(gr, WHITESPACE))
gr->cx++;
else if (grid_reader_in_set(gr, separators)) {
do
gr->cx++;
while (grid_reader_handle_wrap(gr, &xx, &yy) &&
grid_reader_in_set(gr, separators) &&
!grid_reader_in_set(gr, WHITESPACE));
return;
} else {
do
gr->cx++;
while (grid_reader_handle_wrap(gr, &xx, &yy) &&
!(grid_reader_in_set(gr, WHITESPACE) ||
grid_reader_in_set(gr, separators)));
return;
}
expected = !expected;
} while (expected == 0);
}
}
/* Move to the previous place where a word begins. */
void
grid_reader_cursor_previous_word(struct grid_reader *gr, const char *separators,
int already)
int already, int stop_at_eol)
{
int oldx, oldy, r;
int oldx, oldy, at_eol, word_is_letters;
/* Move back to the previous word character. */
if (already || grid_reader_in_set(gr, separators)) {
if (already || grid_reader_in_set(gr, WHITESPACE)) {
for (;;) {
if (gr->cx > 0) {
gr->cx--;
if (!grid_reader_in_set(gr, separators))
if (!grid_reader_in_set(gr, WHITESPACE)) {
word_is_letters =
!grid_reader_in_set(gr, separators);
break;
}
} else {
if (gr->cy == 0)
return;
@ -274,17 +302,21 @@ grid_reader_cursor_previous_word(struct grid_reader *gr, const char *separators,
grid_reader_cursor_end_of_line(gr, 0, 0);
/* Stop if separator at EOL. */
if (gr->cx > 0) {
if (stop_at_eol && gr->cx > 0) {
oldx = gr->cx;
gr->cx--;
r = grid_reader_in_set(gr, separators);
at_eol = grid_reader_in_set(gr,
WHITESPACE);
gr->cx = oldx;
if (r)
if (at_eol) {
word_is_letters = 0;
break;
}
}
}
}
}
} else
word_is_letters = !grid_reader_in_set(gr, separators);
/* Move back to the beginning of this word. */
do {
@ -292,15 +324,16 @@ grid_reader_cursor_previous_word(struct grid_reader *gr, const char *separators,
oldy = gr->cy;
if (gr->cx == 0) {
if (gr->cy == 0 ||
~grid_get_line(gr->gd, gr->cy - 1)->flags &
GRID_LINE_WRAPPED)
(~grid_get_line(gr->gd, gr->cy - 1)->flags &
GRID_LINE_WRAPPED))
break;
grid_reader_cursor_up(gr);
grid_reader_cursor_end_of_line(gr, 0, 1);
}
if (gr->cx > 0)
gr->cx--;
} while (!grid_reader_in_set(gr, separators));
} while (!grid_reader_in_set(gr, WHITESPACE) &&
word_is_letters != grid_reader_in_set(gr, separators));
gr->cx = oldx;
gr->cy = oldy;
}
@ -324,17 +357,17 @@ grid_reader_cursor_jump(struct grid_reader *gr, const struct utf8_data *jc)
memcmp(gc.data.data, jc->data, gc.data.size) == 0) {
gr->cx = px;
gr->cy = py;
return 1;
return (1);
}
px++;
}
if (py == yy ||
!(grid_get_line(gr->gd, py)->flags & GRID_LINE_WRAPPED))
return 0;
return (0);
px = 0;
}
return 0;
return (0);
}
/* Jump back to character. */
@ -354,16 +387,16 @@ grid_reader_cursor_jump_back(struct grid_reader *gr, const struct utf8_data *jc)
memcmp(gc.data.data, jc->data, gc.data.size) == 0) {
gr->cx = px - 1;
gr->cy = py - 1;
return 1;
return (1);
}
}
if (py == 1 ||
!(grid_get_line(gr->gd, py - 2)->flags & GRID_LINE_WRAPPED))
return 0;
return (0);
xx = grid_line_length(gr->gd, py - 2);
}
return 0;
return (0);
}
/* Jump back to the first non-blank character of the line. */

View File

@ -754,7 +754,11 @@ const struct options_table_entry options_table[] = {
{ .name = "word-separators",
.type = OPTIONS_TABLE_STRING,
.scope = OPTIONS_TABLE_SESSION,
.default_str = " ",
/*
* The set of non-alphanumeric printable ASCII characters minus the
* underscore.
*/
.default_str = "!\"#$%&'()*+,-./:;<=>?@[\\]^`{|}~",
.text = "Characters considered to separate words."
},

View File

@ -0,0 +1,116 @@
#!/bin/sh
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -f/dev/null -Ltest"
$TMUX kill-server 2>/dev/null
$TMUX new -d -x40 -y10 \
"cat copy-mode-test.txt; printf '\e[9;15H'; cat" || exit 1
$TMUX set -g window-size manual || exit 1
# Enter copy mode and go to the first column of the first row.
$TMUX set-window-option -g mode-keys emacs
$TMUX set-window-option -g word-separators ""
$TMUX copy-mode
$TMUX send-keys -X history-top
$TMUX send-keys -X start-of-line
# Test that `previous-word` and `previous-space`
# do not go past the start of text.
$TMUX send-keys -X begin-selection
$TMUX send-keys -X previous-word
$TMUX send-keys -X previous-space
$TMUX send-keys -X previous-word
$TMUX send-keys -X copy-selection
[ "$($TMUX show-buffer 2>/dev/null)" = "" ] || exit 1
# Test that `next-word-end` does not skip single-letter words.
$TMUX send-keys -X next-word-end
$TMUX send-keys -X begin-selection
$TMUX send-keys -X previous-word
$TMUX send-keys -X copy-selection
[ "$($TMUX show-buffer)" = "A" ] || exit 1
# Test that `next-word-end` wraps around indented line breaks.
$TMUX send-keys -X next-word
$TMUX send-keys -X next-word
$TMUX send-keys -X next-word
$TMUX send-keys -X begin-selection
$TMUX send-keys -X next-word-end
$TMUX send-keys -X next-word-end
$TMUX send-keys -X copy-selection
[ "$($TMUX show-buffer)" = "$(echo -e "words\n Indented")" ] || exit 1
# Test that `next-word` wraps around un-indented line breaks.
$TMUX send-keys -X next-word
$TMUX send-keys -X begin-selection
$TMUX send-keys -X next-word
$TMUX send-keys -X copy-selection
[ "$($TMUX show-buffer)" = "$(echo -e "line\n")" ] || exit 1
# Test that `next-word-end` treats periods as letters.
$TMUX send-keys -X next-word
$TMUX send-keys -X begin-selection
$TMUX send-keys -X next-word-end
$TMUX send-keys -X copy-selection
[ "$($TMUX show-buffer)" = "line..." ] || exit 1
# Test that `previous-word` and `next-word` treat periods as letters.
$TMUX send-keys -X previous-word
$TMUX send-keys -X begin-selection
$TMUX send-keys -X next-word
$TMUX send-keys -X copy-selection
[ "$($TMUX show-buffer)" = "$(echo -e "line...\n")" ] || exit 1
# Test that `previous-space` and `next-space` treat periods as letters.
$TMUX send-keys -X previous-space
$TMUX send-keys -X begin-selection
$TMUX send-keys -X next-space
$TMUX send-keys -X copy-selection
[ "$($TMUX show-buffer)" = "$(echo -e "line...\n")" ] || exit 1
# Test that `next-word` and `next-word-end` treat other symbols as letters.
$TMUX send-keys -X begin-selection
$TMUX send-keys -X next-word
$TMUX send-keys -X next-word
$TMUX send-keys -X next-word-end
$TMUX send-keys -X next-word-end
$TMUX send-keys -X copy-selection
[ "$($TMUX show-buffer)" = "... @nd then \$ym_bols[]{}" ] || exit 1
# Test that `previous-word` treats other symbols as letters
# and `next-word` wraps around for indented symbols
$TMUX send-keys -X previous-word
$TMUX send-keys -X begin-selection
$TMUX send-keys -X next-word
$TMUX send-keys -X copy-selection
[ "$($TMUX show-buffer)" = "$(echo -e "\$ym_bols[]{}\n ")" ] || exit 1
# Test that `next-word-end` treats digits as letters
$TMUX send-keys -X next-word-end
$TMUX send-keys -X begin-selection
$TMUX send-keys -X next-word-end
$TMUX send-keys -X copy-selection
[ "$($TMUX show-buffer)" = " 500xyz" ] || exit 1
# Test that `previous-word` treats digits as letters
$TMUX send-keys -X begin-selection
$TMUX send-keys -X previous-word
$TMUX send-keys -X copy-selection
[ "$($TMUX show-buffer)" = "500xyz" ] || exit 1
# Test that `next-word` and `next-word-end` stop at the end of text.
$TMUX send-keys -X begin-selection
$TMUX send-keys -X next-word
$TMUX send-keys -X next-word-end
$TMUX send-keys -X next-word
$TMUX send-keys -X next-space
$TMUX send-keys -X next-space-end
$TMUX send-keys -X copy-selection
[ "$($TMUX show-buffer)" = "500xyz" ] || exit 1
$TMUX kill-server 2>/dev/null
exit 0

View File

@ -0,0 +1,115 @@
#!/bin/sh
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -f/dev/null -Ltest"
$TMUX kill-server 2>/dev/null
$TMUX new -d -x40 -y10 \
"cat copy-mode-test.txt; printf '\e[9;15H'; cat" || exit 1
$TMUX set -g window-size manual || exit 1
# Enter copy mode and go to the first column of the first row.
$TMUX set-window-option -g mode-keys vi
$TMUX copy-mode
$TMUX send-keys -X history-top
$TMUX send-keys -X start-of-line
# Test that `previous-word` and `previous-space`
# do not go past the start of text.
$TMUX send-keys -X begin-selection
$TMUX send-keys -X previous-word
$TMUX send-keys -X previous-space
$TMUX send-keys -X previous-word
$TMUX send-keys -X copy-selection
[ "$($TMUX show-buffer)" = "A" ] || exit 1
# Test that `next-word-end` skips single-letter words
# and `previous-word` does not skip multi-letter words.
$TMUX send-keys -X next-word-end
$TMUX send-keys -X begin-selection
$TMUX send-keys -X previous-word
$TMUX send-keys -X copy-selection
[ "$($TMUX show-buffer)" = "line" ] || exit 1
# Test that `next-word-end` wraps around indented line breaks.
$TMUX send-keys -X next-word
$TMUX send-keys -X next-word
$TMUX send-keys -X begin-selection
$TMUX send-keys -X next-word-end
$TMUX send-keys -X next-word-end
$TMUX send-keys -X copy-selection
[ "$($TMUX show-buffer)" = "$(echo -e "words\n Indented")" ] || exit 1
# Test that `next-word` wraps around un-indented line breaks.
$TMUX send-keys -X next-word
$TMUX send-keys -X begin-selection
$TMUX send-keys -X next-word
$TMUX send-keys -X copy-selection
[ "$($TMUX show-buffer)" = "$(echo -e "line\nA")" ] || exit 1
# Test that `next-word-end` does not treat periods as letters.
$TMUX send-keys -X next-word
$TMUX send-keys -X begin-selection
$TMUX send-keys -X next-word-end
$TMUX send-keys -X copy-selection
[ "$($TMUX show-buffer)" = "line" ] || exit 1
# Test that `next-space-end` treats periods as letters.
$TMUX send-keys -X previous-word
$TMUX send-keys -X begin-selection
$TMUX send-keys -X next-space-end
$TMUX send-keys -X copy-selection
[ "$($TMUX show-buffer)" = "line..." ] || exit 1
# Test that `previous-space` and `next-space` treat periods as letters.
$TMUX send-keys -X previous-space
$TMUX send-keys -X begin-selection
$TMUX send-keys -X next-space
$TMUX send-keys -X copy-selection
[ "$($TMUX show-buffer)" = "$(echo -e "line...\n.")" ] || exit 1
# Test that `next-word` and `next-word-end` do not treat other symbols as letters.
$TMUX send-keys -X begin-selection
$TMUX send-keys -X next-word
$TMUX send-keys -X next-word
$TMUX send-keys -X next-word-end
$TMUX send-keys -X next-word-end
$TMUX send-keys -X copy-selection
[ "$($TMUX show-buffer)" = "... @nd then" ] || exit 1
# Test that `next-space` wraps around for indented symbols
$TMUX send-keys -X next-space
$TMUX send-keys -X begin-selection
$TMUX send-keys -X next-space
$TMUX send-keys -X copy-selection
[ "$($TMUX show-buffer)" = "$(echo -e "\$ym_bols[]{}\n ?")" ] || exit 1
# Test that `next-word-end` treats digits as letters
$TMUX send-keys -X next-word-end
$TMUX send-keys -X begin-selection
$TMUX send-keys -X next-word-end
$TMUX send-keys -X copy-selection
[ "$($TMUX show-buffer)" = "? 500xyz" ] || exit 1
# Test that `previous-word` treats digits as letters
$TMUX send-keys -X begin-selection
$TMUX send-keys -X previous-word
$TMUX send-keys -X copy-selection
[ "$($TMUX show-buffer)" = "500xyz" ] || exit 1
# Test that `next-word`, `next-word-end`,
# `next-space`, and `next-space-end` stop at the end of text.
$TMUX send-keys -X begin-selection
$TMUX send-keys -X next-word
$TMUX send-keys -X next-word-end
$TMUX send-keys -X next-word
$TMUX send-keys -X next-space
$TMUX send-keys -X next-space-end
$TMUX send-keys -X copy-selection
[ "$($TMUX show-buffer)" = "500xyz" ] || exit 1
$TMUX kill-server 2>/dev/null
exit 0

View File

@ -0,0 +1,5 @@
A line of words
Indented line
Another line...
... @nd then $ym_bols[]{}
?? 500xyz

207
status.c
View File

@ -876,17 +876,25 @@ status_prompt_translate_key(struct client *c, key_code key, key_code *new_key)
*new_key = KEYC_BSPACE;
return (1);
case 'b':
case 'B':
*new_key = 'b'|KEYC_META;
return (1);
case 'B':
*new_key = 'B'|KEYC_VI;
return (1);
case 'd':
*new_key = '\025';
return (1);
case 'e':
*new_key = 'e'|KEYC_VI;
return (1);
case 'E':
*new_key = 'E'|KEYC_VI;
return (1);
case 'w':
*new_key = 'w'|KEYC_VI;
return (1);
case 'W':
*new_key = 'f'|KEYC_META;
*new_key = 'W'|KEYC_VI;
return (1);
case 'p':
*new_key = '\031'; /* C-y */
@ -1061,16 +1069,125 @@ status_prompt_replace_complete(struct client *c, const char *s)
return (1);
}
/* Prompt forward to the next beginning of a word. */
static void
status_prompt_forward_word(struct client *c, size_t size, int vi,
const char *separators)
{
size_t idx = c->prompt_index;
int word_is_separators;
/* In emacs mode, skip until the first non-whitespace character. */
if (!vi)
while (idx != size &&
status_prompt_space(&c->prompt_buffer[idx]))
idx++;
/* Can't move forward if we're already at the end. */
if (idx == size) {
c->prompt_index = idx;
return;
}
/* Determine the current character class (separators or not). */
word_is_separators = status_prompt_in_list(separators,
&c->prompt_buffer[idx]) &&
!status_prompt_space(&c->prompt_buffer[idx]);
/* Skip ahead until the first space or opposite character class. */
do {
idx++;
if (status_prompt_space(&c->prompt_buffer[idx])) {
/* In vi mode, go to the start of the next word. */
if (vi)
while (idx != size &&
status_prompt_space(&c->prompt_buffer[idx]))
idx++;
break;
}
} while (idx != size && word_is_separators == status_prompt_in_list(
separators, &c->prompt_buffer[idx]));
c->prompt_index = idx;
}
/* Prompt forward to the next end of a word. */
static void
status_prompt_end_word(struct client *c, size_t size, const char *separators)
{
size_t idx = c->prompt_index;
int word_is_separators;
/* Can't move forward if we're already at the end. */
if (idx == size)
return;
/* Find the next word. */
do {
idx++;
if (idx == size) {
c->prompt_index = idx;
return;
}
} while (status_prompt_space(&c->prompt_buffer[idx]));
/* Determine the character class (separators or not). */
word_is_separators = status_prompt_in_list(separators,
&c->prompt_buffer[idx]);
/* Skip ahead until the next space or opposite character class. */
do {
idx++;
if (idx == size)
break;
} while (!status_prompt_space(&c->prompt_buffer[idx]) &&
word_is_separators == status_prompt_in_list(separators,
&c->prompt_buffer[idx]));
/* Back up to the previous character to stop at the end of the word. */
c->prompt_index = idx - 1;
}
/* Prompt backward to the previous beginning of a word. */
static void
status_prompt_backward_word(struct client *c, const char *separators)
{
size_t idx = c->prompt_index;
int word_is_separators;
/* Find non-whitespace. */
while (idx != 0) {
--idx;
if (!status_prompt_space(&c->prompt_buffer[idx]))
break;
}
word_is_separators = status_prompt_in_list(separators,
&c->prompt_buffer[idx]);
/* Find the character before the beginning of the word. */
while (idx != 0) {
--idx;
if (status_prompt_space(&c->prompt_buffer[idx]) ||
word_is_separators != status_prompt_in_list(separators,
&c->prompt_buffer[idx])) {
/* Go back to the word. */
idx++;
break;
}
}
c->prompt_index = idx;
}
/* Handle keys in prompt. */
int
status_prompt_key(struct client *c, key_code key)
{
struct options *oo = c->session->options;
char *s, *cp, prefix = '=';
const char *histstr, *ws = NULL, *keystring;
const char *histstr, *separators = NULL, *keystring;
size_t size, idx;
struct utf8_data tmp;
int keys;
int keys, word_is_separators;
if (c->prompt_flags & PROMPT_KEY) {
keystring = key_string_lookup_key(key, 0);
@ -1173,20 +1290,24 @@ process_key:
}
break;
case '\027': /* C-w */
ws = options_get_string(oo, "word-separators");
separators = options_get_string(oo, "word-separators");
idx = c->prompt_index;
/* Find a non-separator. */
/* Find non-whitespace. */
while (idx != 0) {
idx--;
if (!status_prompt_in_list(ws, &c->prompt_buffer[idx]))
if (!status_prompt_space(&c->prompt_buffer[idx]))
break;
}
word_is_separators = status_prompt_in_list(separators,
&c->prompt_buffer[idx]);
/* Find the separator at the beginning of the word. */
/* Find the character before the beginning of the word. */
while (idx != 0) {
idx--;
if (status_prompt_in_list(ws, &c->prompt_buffer[idx])) {
if (status_prompt_space(&c->prompt_buffer[idx]) ||
word_is_separators != status_prompt_in_list(
separators, &c->prompt_buffer[idx])) {
/* Go back to the word. */
idx++;
break;
@ -1208,50 +1329,32 @@ process_key:
c->prompt_index = idx;
goto changed;
case 'f'|KEYC_META:
case KEYC_RIGHT|KEYC_CTRL:
ws = options_get_string(oo, "word-separators");
/* Find a word. */
while (c->prompt_index != size) {
idx = ++c->prompt_index;
if (!status_prompt_in_list(ws, &c->prompt_buffer[idx]))
break;
}
/* Find the separator at the end of the word. */
while (c->prompt_index != size) {
idx = ++c->prompt_index;
if (status_prompt_in_list(ws, &c->prompt_buffer[idx]))
break;
}
/* Back up to the end-of-word like vi. */
if (options_get_number(oo, "status-keys") == MODEKEY_VI &&
c->prompt_index != 0)
c->prompt_index--;
case 'f'|KEYC_META:
separators = options_get_string(oo, "word-separators");
status_prompt_forward_word(c, size, 0, separators);
goto changed;
case 'E'|KEYC_VI:
status_prompt_end_word(c, size, "");
goto changed;
case 'e'|KEYC_VI:
separators = options_get_string(oo, "word-separators");
status_prompt_end_word(c, size, separators);
goto changed;
case 'W'|KEYC_VI:
status_prompt_forward_word(c, size, 1, "");
goto changed;
case 'w'|KEYC_VI:
separators = options_get_string(oo, "word-separators");
status_prompt_forward_word(c, size, 1, separators);
goto changed;
case 'B'|KEYC_VI:
status_prompt_backward_word(c, "");
goto changed;
case 'b'|KEYC_META:
case KEYC_LEFT|KEYC_CTRL:
ws = options_get_string(oo, "word-separators");
/* Find a non-separator. */
while (c->prompt_index != 0) {
idx = --c->prompt_index;
if (!status_prompt_in_list(ws, &c->prompt_buffer[idx]))
break;
}
/* Find the separator at the beginning of the word. */
while (c->prompt_index != 0) {
idx = --c->prompt_index;
if (status_prompt_in_list(ws, &c->prompt_buffer[idx])) {
/* Go back to the word. */
c->prompt_index++;
break;
}
}
case 'b'|KEYC_META:
separators = options_get_string(oo, "word-separators");
status_prompt_backward_word(c, separators);
goto changed;
case KEYC_UP:
case '\020': /* C-p */
@ -1339,8 +1442,10 @@ append_key:
return (0);
if (key <= 0x7f)
utf8_set(&tmp, key);
else
else if (KEYC_IS_UNICODE(key))
utf8_to_data(key, &tmp);
else
return (0);
c->prompt_buffer = xreallocarray(c->prompt_buffer, size + 2,
sizeof *c->prompt_buffer);

16
tmux.1
View File

@ -1782,19 +1782,18 @@ commands) or when the cursor reaches the bottom (for scrolling commands).
.Ql -no-clear
variants do not clear the selection.
.Pp
The next and previous word keys use space and the
.Ql - ,
.Ql _
and
.Ql @
characters as word delimiters by default, but this can be adjusted by
setting the
The next and previous word keys skip over whitespace and treat consecutive
runs of either word separators or other letters as words.
Word separators can be customized with the
.Em word-separators
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.
Setting
.Em word-separators
to the empty string makes next/previous word equivalent to next/previous space.
.Pp
The jump commands enable quick movement within a line.
For instance, typing
@ -3974,9 +3973,6 @@ 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.
The default is
.Ql \ -_@ .
.El
.Pp
Available window options are:
.Pp

6
tmux.h
View File

@ -131,6 +131,7 @@ struct winlink;
#define KEYC_CURSOR 0x04000000000000ULL
#define KEYC_IMPLIED_META 0x08000000000000ULL
#define KEYC_BUILD_MODIFIERS 0x10000000000000ULL
#define KEYC_VI 0x20000000000000ULL
/* Masks for key bits. */
#define KEYC_MASK_MODIFIERS 0x00f00000000000ULL
@ -590,6 +591,9 @@ struct msg_write_close {
int stream;
};
/* Character classes. */
#define WHITESPACE " "
/* Mode keys. */
#define MODEKEY_EMACS 0
#define MODEKEY_VI 1
@ -2640,7 +2644,7 @@ void grid_reader_cursor_end_of_line(struct grid_reader *, int, int);
void grid_reader_cursor_next_word(struct grid_reader *, const char *);
void grid_reader_cursor_next_word_end(struct grid_reader *, const char *);
void grid_reader_cursor_previous_word(struct grid_reader *, const char *,
int);
int, int);
int grid_reader_cursor_jump(struct grid_reader *,
const struct utf8_data *);
int grid_reader_cursor_jump_back(struct grid_reader *,

2
utf8.c
View File

@ -64,7 +64,7 @@ static struct utf8_index_tree utf8_index_tree = RB_INITIALIZER(utf8_index_tree);
static u_int utf8_next_index;
#define UTF8_GET_SIZE(uc) (((uc) >> 24) & 0x1f)
#define UTF8_GET_WIDTH(flags) (((uc) >> 29) - 1)
#define UTF8_GET_WIDTH(uc) (((uc) >> 29) - 1)
#define UTF8_SET_SIZE(size) (((utf8_char)(size)) << 24)
#define UTF8_SET_WIDTH(width) ((((utf8_char)(width)) + 1) << 29)

View File

@ -128,7 +128,7 @@ static void window_copy_cursor_next_word_end_pos(struct window_mode_entry *,
static void window_copy_cursor_next_word_end(struct window_mode_entry *,
const char *, int);
static void window_copy_cursor_previous_word_pos(struct window_mode_entry *,
const char *, int, u_int *, u_int *);
const char *, u_int *, u_int *);
static void window_copy_cursor_previous_word(struct window_mode_entry *,
const char *, int);
static void window_copy_scroll_up(struct window_mode_entry *, u_int);
@ -255,7 +255,7 @@ struct window_copy_mode_data {
SEL_LINE, /* select one line at a time */
} selflag;
const char *ws; /* word separators */
const char *separators; /* word separators */
u_int dx; /* drag start position */
u_int dy;
@ -1321,7 +1321,7 @@ window_copy_cmd_previous_matching_bracket(struct window_copy_cmd_state *cs)
tried = 1;
goto retry;
}
window_copy_cursor_previous_word(wme, "}]) ", 1);
window_copy_cursor_previous_word(wme, close, 1);
}
continue;
}
@ -1433,8 +1433,7 @@ window_copy_cmd_next_matching_bracket(struct window_copy_cmd_state *cs)
tried = 1;
goto retry;
}
window_copy_cursor_next_word_end(wme, "{[( ",
0);
window_copy_cursor_next_word_end(wme, open, 0);
continue;
}
/* For vi, continue searching for bracket until EOL. */
@ -1506,7 +1505,7 @@ window_copy_cmd_next_space(struct window_copy_cmd_state *cs)
u_int np = wme->prefix;
for (; np != 0; np--)
window_copy_cursor_next_word(wme, " ");
window_copy_cursor_next_word(wme, "");
return (WINDOW_COPY_CMD_NOTHING);
}
@ -1517,7 +1516,7 @@ window_copy_cmd_next_space_end(struct window_copy_cmd_state *cs)
u_int np = wme->prefix;
for (; np != 0; np--)
window_copy_cursor_next_word_end(wme, " ", 0);
window_copy_cursor_next_word_end(wme, "", 0);
return (WINDOW_COPY_CMD_NOTHING);
}
@ -1525,13 +1524,13 @@ static enum window_copy_cmd_action
window_copy_cmd_next_word(struct window_copy_cmd_state *cs)
{
struct window_mode_entry *wme = cs->wme;
struct session *s = cs->s;
u_int np = wme->prefix;
const char *ws;
const char *separators;
separators = options_get_string(cs->s->options, "word-separators");
ws = options_get_string(s->options, "word-separators");
for (; np != 0; np--)
window_copy_cursor_next_word(wme, ws);
window_copy_cursor_next_word(wme, separators);
return (WINDOW_COPY_CMD_NOTHING);
}
@ -1539,13 +1538,13 @@ static enum window_copy_cmd_action
window_copy_cmd_next_word_end(struct window_copy_cmd_state *cs)
{
struct window_mode_entry *wme = cs->wme;
struct session *s = cs->s;
u_int np = wme->prefix;
const char *ws;
const char *separators;
separators = options_get_string(cs->s->options, "word-separators");
ws = options_get_string(s->options, "word-separators");
for (; np != 0; np--)
window_copy_cursor_next_word_end(wme, ws, 0);
window_copy_cursor_next_word_end(wme, separators, 0);
return (WINDOW_COPY_CMD_NOTHING);
}
@ -1618,7 +1617,7 @@ window_copy_cmd_previous_space(struct window_copy_cmd_state *cs)
u_int np = wme->prefix;
for (; np != 0; np--)
window_copy_cursor_previous_word(wme, " ", 1);
window_copy_cursor_previous_word(wme, "", 1);
return (WINDOW_COPY_CMD_NOTHING);
}
@ -1626,13 +1625,13 @@ static enum window_copy_cmd_action
window_copy_cmd_previous_word(struct window_copy_cmd_state *cs)
{
struct window_mode_entry *wme = cs->wme;
struct session *s = cs->s;
u_int np = wme->prefix;
const char *ws;
const char *separators;
separators = options_get_string(cs->s->options, "word-separators");
ws = options_get_string(s->options, "word-separators");
for (; np != 0; np--)
window_copy_cursor_previous_word(wme, ws, 1);
window_copy_cursor_previous_word(wme, separators, 1);
return (WINDOW_COPY_CMD_NOTHING);
}
@ -1778,18 +1777,20 @@ static enum window_copy_cmd_action
window_copy_cmd_select_word(struct window_copy_cmd_state *cs)
{
struct window_mode_entry *wme = cs->wme;
struct session *s = cs->s;
struct options *session_options = cs->s->options;
struct window_copy_mode_data *data = wme->data;
u_int px, py, nextx, nexty;
data->lineflag = LINE_SEL_LEFT_RIGHT;
data->rectflag = 0;
data->selflag = SEL_WORD;
data->dx = data->cx;
data->dy = screen_hsize(data->backing) + data->cy - data->oy;
data->ws = options_get_string(s->options, "word-separators");
window_copy_cursor_previous_word(wme, data->ws, 0);
data->separators = options_get_string(session_options,
"word-separators");
window_copy_cursor_previous_word(wme, data->separators, 0);
px = data->cx;
py = screen_hsize(data->backing) + data->cy - data->oy;
data->selrx = px;
@ -1805,8 +1806,8 @@ window_copy_cmd_select_word(struct window_copy_cmd_state *cs)
nexty++;
}
if (px >= window_copy_find_length(wme, py) ||
!window_copy_in_set(wme, nextx, nexty, data->ws))
window_copy_cursor_next_word_end(wme, data->ws, 1);
!window_copy_in_set(wme, nextx, nexty, WHITESPACE))
window_copy_cursor_next_word_end(wme, data->separators, 1);
else {
window_copy_update_cursor(wme, px, data->cy);
if (window_copy_update_selection(wme, 1, 1))
@ -3730,8 +3731,8 @@ window_copy_synchronize_cursor_end(struct window_mode_entry *wme, int begin,
begin = 0;
if (data->dy > yy || (data->dy == yy && data->dx > xx)) {
/* Right to left selection. */
window_copy_cursor_previous_word_pos(wme, data->ws, 0,
&xx, &yy);
window_copy_cursor_previous_word_pos(wme,
data->separators, &xx, &yy);
begin = 1;
/* Reset the end. */
@ -3740,9 +3741,10 @@ window_copy_synchronize_cursor_end(struct window_mode_entry *wme, int begin,
} else {
/* Left to right selection. */
if (xx >= window_copy_find_length(wme, yy) ||
!window_copy_in_set(wme, xx + 1, yy, data->ws))
!window_copy_in_set(wme, xx + 1, yy, WHITESPACE)) {
window_copy_cursor_next_word_end_pos(wme,
data->ws, &xx, &yy);
data->separators, &xx, &yy);
}
/* Reset the start. */
data->selx = data->selrx;
@ -4666,19 +4668,19 @@ window_copy_cursor_next_word_end_pos(struct window_mode_entry *wme,
struct screen *back_s = data->backing;
struct grid_reader gr;
u_int px, py, hsize;
int keys;
px = data->cx;
hsize = screen_hsize(back_s);
py = hsize + data->cy - data->oy;
grid_reader_start(&gr, back_s->grid, px, py);
keys = options_get_number(oo, "mode-keys");
if (keys == MODEKEY_VI && !grid_reader_in_set(&gr, separators))
grid_reader_cursor_right(&gr, 0, 0);
grid_reader_cursor_next_word_end(&gr, separators);
if (keys == MODEKEY_VI)
if (options_get_number(oo, "mode-keys") == MODEKEY_VI) {
if (!grid_reader_in_set(&gr, WHITESPACE))
grid_reader_cursor_right(&gr, 0, 0);
grid_reader_cursor_next_word_end(&gr, separators);
grid_reader_cursor_left(&gr, 1);
} else
grid_reader_cursor_next_word_end(&gr, separators);
grid_reader_get_cursor(&gr, &px, &py);
*ppx = px;
*ppy = py;
@ -4695,7 +4697,6 @@ window_copy_cursor_next_word_end(struct window_mode_entry *wme,
struct screen *back_s = data->backing;
struct grid_reader gr;
u_int px, py, oldy, hsize;
int keys;
px = data->cx;
hsize = screen_hsize(back_s);
@ -4703,12 +4704,13 @@ window_copy_cursor_next_word_end(struct window_mode_entry *wme,
oldy = data->cy;
grid_reader_start(&gr, back_s->grid, px, py);
keys = options_get_number(oo, "mode-keys");
if (keys == MODEKEY_VI && !grid_reader_in_set(&gr, separators))
grid_reader_cursor_right(&gr, 0, 0);
grid_reader_cursor_next_word_end(&gr, separators);
if (keys == MODEKEY_VI)
if (options_get_number(oo, "mode-keys") == MODEKEY_VI) {
if (!grid_reader_in_set(&gr, WHITESPACE))
grid_reader_cursor_right(&gr, 0, 0);
grid_reader_cursor_next_word_end(&gr, separators);
grid_reader_cursor_left(&gr, 1);
} else
grid_reader_cursor_next_word_end(&gr, separators);
grid_reader_get_cursor(&gr, &px, &py);
window_copy_acquire_cursor_down(wme, hsize, screen_size_y(back_s),
data->oy, oldy, px, py, no_reset);
@ -4717,7 +4719,7 @@ window_copy_cursor_next_word_end(struct window_mode_entry *wme,
/* Compute the previous place where a word begins. */
static void
window_copy_cursor_previous_word_pos(struct window_mode_entry *wme,
const char *separators, int already, u_int *ppx, u_int *ppy)
const char *separators, u_int *ppx, u_int *ppy)
{
struct window_copy_mode_data *data = wme->data;
struct screen *back_s = data->backing;
@ -4729,7 +4731,8 @@ window_copy_cursor_previous_word_pos(struct window_mode_entry *wme,
py = hsize + data->cy - data->oy;
grid_reader_start(&gr, back_s->grid, px, py);
grid_reader_cursor_previous_word(&gr, separators, already);
grid_reader_cursor_previous_word(&gr, separators, /* already= */ 0,
/* stop_at_eol= */ 1);
grid_reader_get_cursor(&gr, &px, &py);
*ppx = px;
*ppy = py;
@ -4744,6 +4747,11 @@ window_copy_cursor_previous_word(struct window_mode_entry *wme,
struct screen *back_s = data->backing;
struct grid_reader gr;
u_int px, py, oldy, hsize;
int stop_at_eol;
stop_at_eol =
options_get_number(wme->wp->window->options, "mode-keys")
== MODEKEY_EMACS;
px = data->cx;
hsize = screen_hsize(back_s);
@ -4751,7 +4759,7 @@ window_copy_cursor_previous_word(struct window_mode_entry *wme,
oldy = data->cy;
grid_reader_start(&gr, back_s->grid, px, py);
grid_reader_cursor_previous_word(&gr, separators, already);
grid_reader_cursor_previous_word(&gr, separators, already, stop_at_eol);
grid_reader_get_cursor(&gr, &px, &py);
window_copy_acquire_cursor_up(wme, hsize, data->oy, oldy, px, py);
}
@ -4893,10 +4901,10 @@ window_copy_start_drag(struct client *c, struct mouse_event *m)
data->selflag = SEL_CHAR;
switch (data->selflag) {
case SEL_WORD:
if (data->ws != NULL) {
if (data->separators != NULL) {
window_copy_update_cursor(wme, x, y);
window_copy_cursor_previous_word_pos(wme, data->ws, 0,
&x, &y);
window_copy_cursor_previous_word_pos(wme,
data->separators, &x, &y);
y -= screen_hsize(data->backing) - data->oy;
}
window_copy_update_cursor(wme, x, y);