Skip to content

Commit 853aaa0

Browse files
Jongydpgeorge
authored andcommitted
lib/mp-readline: Add word-based move/delete EMACS key sequences.
This commit adds backward-word, backward-kill-word, forward-word, forward-kill-word sequences for the REPL, with bindings to Alt+F, Alt+B, Alt+D and Alt+Backspace respectively. It is disabled by default and can be enabled via MICROPY_REPL_EMACS_WORDS_MOVE. Further enabling MICROPY_REPL_EMACS_EXTRA_WORDS_MOVE adds extra bindings for these new sequences: Ctrl+Right, Ctrl+Left and Ctrl+W. The features are enabled on unix micropython-coverage and micropython-dev.
1 parent dce590c commit 853aaa0

File tree

10 files changed

+202
-1
lines changed

10 files changed

+202
-1
lines changed

lib/mp-readline/readline.c

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,35 @@ typedef struct _readline_t {
9999

100100
STATIC readline_t rl;
101101

102+
#if MICROPY_REPL_EMACS_WORDS_MOVE
103+
STATIC size_t cursor_count_word(int forward) {
104+
const char *line_buf = vstr_str(rl.line);
105+
size_t pos = rl.cursor_pos;
106+
bool in_word = false;
107+
108+
for (;;) {
109+
// if moving backwards and we've reached 0... break
110+
if (!forward && pos == 0) {
111+
break;
112+
}
113+
// or if moving forwards and we've reached to the end of line... break
114+
else if (forward && pos == vstr_len(rl.line)) {
115+
break;
116+
}
117+
118+
if (unichar_isalnum(line_buf[pos + (forward - 1)])) {
119+
in_word = true;
120+
} else if (in_word) {
121+
break;
122+
}
123+
124+
pos += forward ? forward : -1;
125+
}
126+
127+
return forward ? pos - rl.cursor_pos : rl.cursor_pos - pos;
128+
}
129+
#endif
130+
102131
int readline_process_char(int c) {
103132
size_t last_line_len = rl.line->len;
104133
int redraw_step_back = 0;
@@ -149,6 +178,10 @@ int readline_process_char(int c) {
149178
redraw_step_back = rl.cursor_pos - rl.orig_line_len;
150179
redraw_from_cursor = true;
151180
#endif
181+
#if MICROPY_REPL_EMACS_EXTRA_WORDS_MOVE
182+
} else if (c == CHAR_CTRL_W) {
183+
goto backward_kill_word;
184+
#endif
152185
} else if (c == '\r') {
153186
// newline
154187
mp_hal_stdout_tx_str("\r\n");
@@ -222,9 +255,40 @@ int readline_process_char(int c) {
222255
case 'O':
223256
rl.escape_seq = ESEQ_ESC_O;
224257
break;
258+
#if MICROPY_REPL_EMACS_WORDS_MOVE
259+
case 'b':
260+
#if MICROPY_REPL_EMACS_EXTRA_WORDS_MOVE
261+
backward_word:
262+
#endif
263+
redraw_step_back = cursor_count_word(0);
264+
rl.escape_seq = ESEQ_NONE;
265+
break;
266+
case 'f':
267+
#if MICROPY_REPL_EMACS_EXTRA_WORDS_MOVE
268+
forward_word:
269+
#endif
270+
redraw_step_forward = cursor_count_word(1);
271+
rl.escape_seq = ESEQ_NONE;
272+
break;
273+
case 'd':
274+
vstr_cut_out_bytes(rl.line, rl.cursor_pos, cursor_count_word(1));
275+
redraw_from_cursor = true;
276+
rl.escape_seq = ESEQ_NONE;
277+
break;
278+
case 127:
279+
#if MICROPY_REPL_EMACS_EXTRA_WORDS_MOVE
280+
backward_kill_word:
281+
#endif
282+
redraw_step_back = cursor_count_word(0);
283+
vstr_cut_out_bytes(rl.line, rl.cursor_pos - redraw_step_back, redraw_step_back);
284+
redraw_from_cursor = true;
285+
rl.escape_seq = ESEQ_NONE;
286+
break;
287+
#endif
225288
default:
226289
DEBUG_printf("(ESC %d)", c);
227290
rl.escape_seq = ESEQ_NONE;
291+
break;
228292
}
229293
} else if (rl.escape_seq == ESEQ_ESC_BRACKET) {
230294
if ('0' <= c && c <= '9') {
@@ -312,6 +376,24 @@ int readline_process_char(int c) {
312376
} else {
313377
DEBUG_printf("(ESC [ %c %d)", rl.escape_seq_buf[0], c);
314378
}
379+
#if MICROPY_REPL_EMACS_EXTRA_WORDS_MOVE
380+
} else if (c == ';' && rl.escape_seq_buf[0] == '1') {
381+
// ';' is used to separate parameters. so first parameter was '1',
382+
// that's used for sequences like ctrl+left, which we will try to parse.
383+
// escape_seq state is reset back to ESEQ_ESC_BRACKET, as if we've just received
384+
// the opening bracket, because more parameters are to come.
385+
// we don't track the parameters themselves to keep low on logic and code size. that
386+
// might be required in the future if more complex sequences are added.
387+
rl.escape_seq = ESEQ_ESC_BRACKET;
388+
// goto away from the state-machine, as rl.escape_seq will be overridden.
389+
goto redraw;
390+
} else if (rl.escape_seq_buf[0] == '5' && c == 'C') {
391+
// ctrl+right
392+
goto forward_word;
393+
} else if (rl.escape_seq_buf[0] == '5' && c == 'D') {
394+
// ctrl+left
395+
goto backward_word;
396+
#endif
315397
} else {
316398
DEBUG_printf("(ESC [ %c %d)", rl.escape_seq_buf[0], c);
317399
}
@@ -330,6 +412,10 @@ int readline_process_char(int c) {
330412
rl.escape_seq = ESEQ_NONE;
331413
}
332414

415+
#if MICROPY_REPL_EMACS_EXTRA_WORDS_MOVE
416+
redraw:
417+
#endif
418+
333419
// redraw command prompt, efficiently
334420
if (redraw_step_back > 0) {
335421
mp_hal_move_cursor_back(redraw_step_back);

lib/mp-readline/readline.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
#define CHAR_CTRL_N (14)
3737
#define CHAR_CTRL_P (16)
3838
#define CHAR_CTRL_U (21)
39+
#define CHAR_CTRL_W (23)
3940

4041
void readline_init0(void);
4142
int readline(vstr_t *line, const char *prompt);

ports/unix/variants/coverage/mpconfigvariant.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
#define MICROPY_FLOAT_HIGH_QUALITY_HASH (1)
3535
#define MICROPY_ENABLE_SCHEDULER (1)
3636
#define MICROPY_READER_VFS (1)
37+
#define MICROPY_REPL_EMACS_WORDS_MOVE (1)
38+
#define MICROPY_REPL_EMACS_EXTRA_WORDS_MOVE (1)
3739
#define MICROPY_WARNINGS_CATEGORY (1)
3840
#define MICROPY_MODULE_GETATTR (1)
3941
#define MICROPY_PY_DELATTR_SETATTR (1)

ports/unix/variants/dev/mpconfigvariant.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,7 @@
2424
* THE SOFTWARE.
2525
*/
2626

27+
#define MICROPY_REPL_EMACS_WORDS_MOVE (1)
28+
#define MICROPY_REPL_EMACS_EXTRA_WORDS_MOVE (1)
29+
2730
#define MICROPY_PY_SYS_SETTRACE (1)

py/mpconfig.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,21 @@
570570
#define MICROPY_REPL_EMACS_KEYS (0)
571571
#endif
572572

573+
// Whether to include emacs-style word movement/kill readline behavior in REPL.
574+
// This adds Alt+F, Alt+B, Alt+D and Alt+Backspace for forward-word, backward-word, forward-kill-word
575+
// and backward-kill-word, respectively.
576+
#ifndef MICROPY_REPL_EMACS_WORDS_MOVE
577+
#define MICROPY_REPL_EMACS_WORDS_MOVE (0)
578+
#endif
579+
580+
// Whether to include extra convenience keys for word movement/kill in readline REPL.
581+
// This adds Ctrl+Right, Ctrl+Left and Ctrl+W for forward-word, backward-word and backward-kill-word
582+
// respectively. Ctrl+Delete is not implemented because it's a very different escape sequence.
583+
// Depends on MICROPY_REPL_EMACS_WORDS_MOVE.
584+
#ifndef MICROPY_REPL_EMACS_EXTRA_WORDS_MOVE
585+
#define MICROPY_REPL_EMACS_EXTRA_WORDS_MOVE (0)
586+
#endif
587+
573588
// Whether to implement auto-indent in REPL
574589
#ifndef MICROPY_REPL_AUTO_INDENT
575590
#define MICROPY_REPL_AUTO_INDENT (0)

tests/cmdline/repl_words_move.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# word movement
2+
# backward-word, start in word
3+
234b1
4+
# backward-word, don't start in word
5+
234 b1
6+
# backward-word on start of line. if cursor is moved, this will result in a SyntaxError
7+
1 2 + 3b+
8+
# forward-word, start in word
9+
1+2 12+f+3
10+
# forward-word, don't start in word
11+
1+ 12 3f+
12+
# forward-word on eol. if cursor is moved, this will result in a SyntaxError
13+
1 + 2 3f+
14+
15+
# kill word
16+
# backward-kill-word, start in word
17+
100 + 45623
18+
# backward-kill-word, don't start in word
19+
100 + 456231
20+
# forward-kill-word, start in word
21+
100 + 256d3
22+
# forward-kill-word, don't start in word
23+
1 + 256d2
24+
25+
# extra move/kill shortcuts
26+
# ctrl-left
27+
2341
28+
# ctrl-right
29+
123
30+
# ctrl-w
31+
1231
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
MicroPython \.\+ version
2+
Use \.\+
3+
>>> # word movement
4+
>>> # backward-word, start in word
5+
>>> \.\+
6+
1234
7+
>>> # backward-word, don't start in word
8+
>>> \.\+
9+
1234
10+
>>> # backward-word on start of line. if cursor is moved, this will result in a SyntaxError
11+
>>> \.\+
12+
6
13+
>>> # forward-word, start in word
14+
>>> \.\+
15+
18
16+
>>> # forward-word, don't start in word
17+
>>> \.\+
18+
16
19+
>>> # forward-word on eol. if cursor is moved, this will result in a SyntaxError
20+
>>> \.\+
21+
6
22+
>>>
23+
>>> # kill word
24+
>>> # backward-kill-word, start in word
25+
>>> \.\+
26+
123
27+
>>> # backward-kill-word, don't start in word
28+
>>> \.\+
29+
101
30+
>>> # forward-kill-word, start in word
31+
>>> \.\+
32+
123
33+
>>> # forward-kill-word, don't start in word
34+
>>> \.\+
35+
3
36+
>>>
37+
>>> # extra move/kill shortcuts
38+
>>> # ctrl-left
39+
>>> \.\+
40+
1234
41+
>>> # ctrl-right
42+
>>> \.\+
43+
123
44+
>>> # ctrl-w
45+
>>> \.\+
46+
1
47+
>>>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# just check if ctrl+w is supported, because it makes sure that
2+
# both MICROPY_REPL_EMACS_WORDS_MOVE and MICROPY_REPL_EXTRA_WORDS_MOVE are enabled.
3+
t = 1231
4+
t == 1
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
MicroPython \.\+ version
2+
Use \.\+
3+
>>> # Check for emacs keys in REPL
4+
>>> t = \.\+
5+
>>> t == 2
6+
True
7+
>>>

tests/run-tests

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,9 +284,14 @@ def run_tests(pyb, tests, args, base_path="."):
284284

285285
# Check if emacs repl is supported, and skip such tests if it's not
286286
t = run_feature_check(pyb, args, base_path, 'repl_emacs_check.py')
287-
if not 'True' in str(t, 'ascii'):
287+
if 'True' not in str(t, 'ascii'):
288288
skip_tests.add('cmdline/repl_emacs_keys.py')
289289

290+
# Check if words movement in repl is supported, and skip such tests if it's not
291+
t = run_feature_check(pyb, args, base_path, 'repl_words_move_check.py')
292+
if 'True' not in str(t, 'ascii'):
293+
skip_tests.add('cmdline/repl_words_move.py')
294+
290295
upy_byteorder = run_feature_check(pyb, args, base_path, 'byteorder.py')
291296
upy_float_precision = run_feature_check(pyb, args, base_path, 'float.py')
292297
if upy_float_precision == b'CRASH':

0 commit comments

Comments
 (0)