Skip to content

Commit cef8822

Browse files
authored
valkey-cli: Add word-jump navigation (Alt/Option/Ctrl + ←/→) (valkey-io#2583)
Interactive use of valkey-cli often involves working with long keys (e.g. MY:INCREDIBLY:LONG:keythattakesalongtimetotype). In shells like bash, zsh, or psql, users can quickly move the cursor word by word with **Alt/Option+Left/Right**, **Ctrl+Left/Right** or **Alt/Option+b/f**. This makes editing long commands much more efficient. Until now, valkey-cli (via linenoise) only supported single-character cursor moves, which is painful for frequent key editing. This patch adds such support, with simple code changes in linenoise. It now supports both the Meta (Alt/Option) style and CSI (control sequence introducer) style: | | Meta style | CSI style (Ctrl) | CSI style (Alt) | | --------------- | ---------- | ---------------- | --------- | | move word left | ESC b | ESC [1;5D | ESC [1;3D | | move word right | ESC f | ESC [1;5C | ESC [1;3C | Notice that I handle these two styles differently since people have different preference on the definition of "what is a word". Specifically, I define: - "sub-word": just letters and digits. For example "my:namespace:key" has 3 sub-words. This is handled by Meta style. - "big-word": as any character that is not space. For example "my:namespace:key" is just one single big-word. This is handled by CSI style. ## How I verified I'm using MacOS default terminal (`$TERM = xterm-256color`). I customized the terminal keyboard setting to map option + left to `\033b` , and ctrl + left to `\033[1;5D` so that I can produce both the Meta style and CSI style. This code change should also work for Linux/BSD/other terminal users. Now the valkey-cli works like the following. `|` shows where the cursor is currently at. Press Alt + left (escape sequence `ESC b` ): ``` set cache:item itemid | set cache:item itemid | set cache:item itemid | set cache:item itemid | set cache:item itemid | ``` Press Ctrl + left (escape sequence `ESC [1;5D` ): ``` set cache:item itemid | set cache:item itemid | set cache:item itemid | set cache:item itemid | ``` Press Alt + right (escape sequence `ESC f` ): ``` set cache:item itemid | set cache:item itemid | set cache:item itemid | set cache:item itemid | set cache:item itemid | ``` Press Ctrl + right (escape sequence `ESC [1;5C` ): ``` set cache:item itemid | set cache:item itemid | set cache:item itemid | set cache:item itemid | ``` --------- Signed-off-by: Zhijun <dszhijun@gmail.com>
1 parent 3b13a7c commit cef8822

File tree

2 files changed

+87
-7
lines changed

2 files changed

+87
-7
lines changed

deps/linenoise/example.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ int main(int argc, char **argv) {
6060
/* Do something with the string. */
6161
if (line[0] != '\0' && line[0] != '/') {
6262
printf("echo: '%s'\n", line);
63-
linenoiseHistoryAdd(line); /* Add to the history. */
63+
linenoiseHistoryAdd(line, 0); /* Add to the history. */
6464
linenoiseHistorySave("history.txt"); /* Save the history on disk. */
6565
} else if (!strncmp(line,"/historylen",11)) {
6666
/* The "/historylen" command will change the history len. */

deps/linenoise/linenoise.c

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,49 @@ void linenoiseEditMoveRight(struct linenoiseState *l) {
708708
}
709709
}
710710

711+
/* People have different preferences on what is considered a word
712+
* when editing commandline. For example, in command
713+
* `set business:department:product value`,
714+
* some might treat the entire long key as a word, while others
715+
* treat it as 3 subwords and prefer granular word jumping navigation
716+
* to make fixing a typo (e.g. "business:deaprtment:product") easier.
717+
* In order to accommodate this, we treat escape sequences
718+
* ESC b/f (usually Alt + ←/→ on MacOS, but you might get ESC[1;3D/C) and
719+
* ESC[1;5D/C (usually Ctrl + ←/→) differently.
720+
*
721+
* Notice that the exact behavior of key mapping depends on your
722+
* terminal/keyboard setup. To see how your environment maps keys
723+
* to escape sequences, you can run command `cat -v` and then press
724+
* arbitrary keys to see how they work.
725+
*/
726+
static int isSubWordDelimiter(const char c) {
727+
return !isalnum(c);
728+
}
729+
static int isBigWordDelimiter(const char c) {
730+
return isspace(c);
731+
}
732+
733+
typedef int (*isDelimiterFunc)(char c);
734+
735+
static void linenoiseEditMoveWordLeft(struct linenoiseState *l, const isDelimiterFunc isDelimiter) {
736+
if (l->pos == 0) return;
737+
/* Move cursor to the left over any delimiters */
738+
while (l->pos > 0 && isDelimiter(l->buf[l->pos - 1])) l->pos--;
739+
/* Then continue moving over a word */
740+
while (l->pos > 0 && !isDelimiter(l->buf[l->pos - 1])) l->pos--;
741+
refreshLine(l);
742+
}
743+
744+
static void linenoiseEditMoveWordRight(struct linenoiseState *l, const isDelimiterFunc isDelimiter) {
745+
if (l->pos == l->len) return;
746+
/* Move cursor to the right over any delimiters */
747+
while (l->pos < l->len && isDelimiter(l->buf[l->pos])) l->pos++;
748+
/* Then continue moving over a word */
749+
while (l->pos < l->len && !isDelimiter(l->buf[l->pos])) l->pos++;
750+
refreshLine(l);
751+
}
752+
753+
711754
/* Move cursor to the start of the line. */
712755
void linenoiseEditMoveHome(struct linenoiseState *l) {
713756
if (l->pos != 0) {
@@ -901,20 +944,57 @@ static int linenoiseEdit(int stdin_fd, int stdout_fd, char *buf, size_t buflen,
901944
* Use two calls to handle slow terminals returning the two
902945
* chars at different times. */
903946
if (read(l.ifd,seq,1) == -1) break;
947+
948+
/* Handle Meta-b / Meta-f directly.
949+
* Usually generated by Alt + ←/→ (but not always)
950+
*/
951+
if (seq[0] == 'b') { /* ESC b → word left */
952+
linenoiseEditMoveWordLeft(&l, isSubWordDelimiter);
953+
break;
954+
}
955+
if (seq[0] == 'f') { /* ESC f → word right */
956+
linenoiseEditMoveWordRight(&l, isSubWordDelimiter);
957+
break;
958+
}
959+
904960
if (read(l.ifd,seq+1,1) == -1) break;
905961

906962
/* ESC [ sequences. */
907963
if (seq[0] == '[') {
908964
if (seq[1] >= '0' && seq[1] <= '9') {
909-
/* Extended escape, read additional byte. */
910-
if (read(l.ifd,seq+2,1) == -1) break;
911-
if (seq[2] == '~') {
912-
switch(seq[1]) {
913-
case '3': /* Delete key. */
914-
linenoiseEditDelete(&l);
965+
/* Extended escape, read additional bytes.
966+
* Examples: ESC [1;5C ESC [3~ */
967+
const int seqBufferMaxLength = 8;
968+
char seqBuffer[seqBufferMaxLength];
969+
int i = 0;
970+
seqBuffer[i++] = seq[1];
971+
972+
/* If first param is digit or ';', read more until we see a final in @~ */
973+
char additionalChar;
974+
while (i < seqBufferMaxLength-1 && read(l.ifd, &additionalChar, 1) != -1) {
975+
seqBuffer[i++] = additionalChar;
976+
if (additionalChar >= '@' && additionalChar <= '~') { /* CSI final byte */
977+
seqBuffer[i] = '\0';
915978
break;
916979
}
917980
}
981+
982+
/* The exact key mapping behavior depends on your keyboard/terminal setup */
983+
/* Usually Alt/Ctrl + ← */
984+
if (!strcmp(seqBuffer, "1;5D") || !strcmp(seqBuffer, "1;3D")) {
985+
linenoiseEditMoveWordLeft(&l, isBigWordDelimiter);
986+
break;
987+
}
988+
/* Usually Alt/Ctrl + → */
989+
if (!strcmp(seqBuffer, "1;5C") || !strcmp(seqBuffer, "1;3C")) {
990+
linenoiseEditMoveWordRight(&l, isBigWordDelimiter);
991+
break;
992+
}
993+
/* Usually the `delete` key */
994+
if (!strcmp(seqBuffer, "3~")) {
995+
linenoiseEditDelete(&l);
996+
break;
997+
}
918998
} else {
919999
switch(seq[1]) {
9201000
case 'A': /* Up */

0 commit comments

Comments
 (0)