From 853aaa06f24c98191a44a38eedd4ec2a0e63d3eb Mon Sep 17 00:00:00 2001 From: Yonatan Goldschmidt Date: Sat, 14 Dec 2019 23:42:03 +0200 Subject: [PATCH] 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. --- lib/mp-readline/readline.c | 86 +++++++++++++++++++ lib/mp-readline/readline.h | 1 + .../unix/variants/coverage/mpconfigvariant.h | 2 + ports/unix/variants/dev/mpconfigvariant.h | 3 + py/mpconfig.h | 15 ++++ tests/cmdline/repl_words_move.py | 31 +++++++ tests/cmdline/repl_words_move.py.exp | 47 ++++++++++ tests/feature_check/repl_words_move_check.py | 4 + .../repl_words_move_check.py.exp | 7 ++ tests/run-tests | 7 +- 10 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 tests/cmdline/repl_words_move.py create mode 100644 tests/cmdline/repl_words_move.py.exp create mode 100644 tests/feature_check/repl_words_move_check.py create mode 100644 tests/feature_check/repl_words_move_check.py.exp diff --git a/lib/mp-readline/readline.c b/lib/mp-readline/readline.c index 1500873f69..296c8aa4ab 100644 --- a/lib/mp-readline/readline.c +++ b/lib/mp-readline/readline.c @@ -99,6 +99,35 @@ typedef struct _readline_t { STATIC readline_t rl; +#if MICROPY_REPL_EMACS_WORDS_MOVE +STATIC size_t cursor_count_word(int forward) { + const char *line_buf = vstr_str(rl.line); + size_t pos = rl.cursor_pos; + bool in_word = false; + + for (;;) { + // if moving backwards and we've reached 0... break + if (!forward && pos == 0) { + break; + } + // or if moving forwards and we've reached to the end of line... break + else if (forward && pos == vstr_len(rl.line)) { + break; + } + + if (unichar_isalnum(line_buf[pos + (forward - 1)])) { + in_word = true; + } else if (in_word) { + break; + } + + pos += forward ? forward : -1; + } + + return forward ? pos - rl.cursor_pos : rl.cursor_pos - pos; +} +#endif + int readline_process_char(int c) { size_t last_line_len = rl.line->len; int redraw_step_back = 0; @@ -149,6 +178,10 @@ int readline_process_char(int c) { redraw_step_back = rl.cursor_pos - rl.orig_line_len; redraw_from_cursor = true; #endif + #if MICROPY_REPL_EMACS_EXTRA_WORDS_MOVE + } else if (c == CHAR_CTRL_W) { + goto backward_kill_word; + #endif } else if (c == '\r') { // newline mp_hal_stdout_tx_str("\r\n"); @@ -222,9 +255,40 @@ int readline_process_char(int c) { case 'O': rl.escape_seq = ESEQ_ESC_O; break; + #if MICROPY_REPL_EMACS_WORDS_MOVE + case 'b': +#if MICROPY_REPL_EMACS_EXTRA_WORDS_MOVE +backward_word: +#endif + redraw_step_back = cursor_count_word(0); + rl.escape_seq = ESEQ_NONE; + break; + case 'f': +#if MICROPY_REPL_EMACS_EXTRA_WORDS_MOVE +forward_word: +#endif + redraw_step_forward = cursor_count_word(1); + rl.escape_seq = ESEQ_NONE; + break; + case 'd': + vstr_cut_out_bytes(rl.line, rl.cursor_pos, cursor_count_word(1)); + redraw_from_cursor = true; + rl.escape_seq = ESEQ_NONE; + break; + case 127: +#if MICROPY_REPL_EMACS_EXTRA_WORDS_MOVE +backward_kill_word: +#endif + redraw_step_back = cursor_count_word(0); + vstr_cut_out_bytes(rl.line, rl.cursor_pos - redraw_step_back, redraw_step_back); + redraw_from_cursor = true; + rl.escape_seq = ESEQ_NONE; + break; + #endif default: DEBUG_printf("(ESC %d)", c); rl.escape_seq = ESEQ_NONE; + break; } } else if (rl.escape_seq == ESEQ_ESC_BRACKET) { if ('0' <= c && c <= '9') { @@ -312,6 +376,24 @@ delete_key: } else { DEBUG_printf("(ESC [ %c %d)", rl.escape_seq_buf[0], c); } + #if MICROPY_REPL_EMACS_EXTRA_WORDS_MOVE + } else if (c == ';' && rl.escape_seq_buf[0] == '1') { + // ';' is used to separate parameters. so first parameter was '1', + // that's used for sequences like ctrl+left, which we will try to parse. + // escape_seq state is reset back to ESEQ_ESC_BRACKET, as if we've just received + // the opening bracket, because more parameters are to come. + // we don't track the parameters themselves to keep low on logic and code size. that + // might be required in the future if more complex sequences are added. + rl.escape_seq = ESEQ_ESC_BRACKET; + // goto away from the state-machine, as rl.escape_seq will be overridden. + goto redraw; + } else if (rl.escape_seq_buf[0] == '5' && c == 'C') { + // ctrl+right + goto forward_word; + } else if (rl.escape_seq_buf[0] == '5' && c == 'D') { + // ctrl+left + goto backward_word; + #endif } else { DEBUG_printf("(ESC [ %c %d)", rl.escape_seq_buf[0], c); } @@ -330,6 +412,10 @@ delete_key: rl.escape_seq = ESEQ_NONE; } +#if MICROPY_REPL_EMACS_EXTRA_WORDS_MOVE +redraw: +#endif + // redraw command prompt, efficiently if (redraw_step_back > 0) { mp_hal_move_cursor_back(redraw_step_back); diff --git a/lib/mp-readline/readline.h b/lib/mp-readline/readline.h index 00aa9622a8..a19e1209ab 100644 --- a/lib/mp-readline/readline.h +++ b/lib/mp-readline/readline.h @@ -36,6 +36,7 @@ #define CHAR_CTRL_N (14) #define CHAR_CTRL_P (16) #define CHAR_CTRL_U (21) +#define CHAR_CTRL_W (23) void readline_init0(void); int readline(vstr_t *line, const char *prompt); diff --git a/ports/unix/variants/coverage/mpconfigvariant.h b/ports/unix/variants/coverage/mpconfigvariant.h index f383a83700..7192924534 100644 --- a/ports/unix/variants/coverage/mpconfigvariant.h +++ b/ports/unix/variants/coverage/mpconfigvariant.h @@ -34,6 +34,8 @@ #define MICROPY_FLOAT_HIGH_QUALITY_HASH (1) #define MICROPY_ENABLE_SCHEDULER (1) #define MICROPY_READER_VFS (1) +#define MICROPY_REPL_EMACS_WORDS_MOVE (1) +#define MICROPY_REPL_EMACS_EXTRA_WORDS_MOVE (1) #define MICROPY_WARNINGS_CATEGORY (1) #define MICROPY_MODULE_GETATTR (1) #define MICROPY_PY_DELATTR_SETATTR (1) diff --git a/ports/unix/variants/dev/mpconfigvariant.h b/ports/unix/variants/dev/mpconfigvariant.h index 84a3edd454..2b30970106 100644 --- a/ports/unix/variants/dev/mpconfigvariant.h +++ b/ports/unix/variants/dev/mpconfigvariant.h @@ -24,4 +24,7 @@ * THE SOFTWARE. */ +#define MICROPY_REPL_EMACS_WORDS_MOVE (1) +#define MICROPY_REPL_EMACS_EXTRA_WORDS_MOVE (1) + #define MICROPY_PY_SYS_SETTRACE (1) diff --git a/py/mpconfig.h b/py/mpconfig.h index d6f4d92328..2c4169070d 100644 --- a/py/mpconfig.h +++ b/py/mpconfig.h @@ -570,6 +570,21 @@ #define MICROPY_REPL_EMACS_KEYS (0) #endif +// Whether to include emacs-style word movement/kill readline behavior in REPL. +// This adds Alt+F, Alt+B, Alt+D and Alt+Backspace for forward-word, backward-word, forward-kill-word +// and backward-kill-word, respectively. +#ifndef MICROPY_REPL_EMACS_WORDS_MOVE +#define MICROPY_REPL_EMACS_WORDS_MOVE (0) +#endif + +// Whether to include extra convenience keys for word movement/kill in readline REPL. +// This adds Ctrl+Right, Ctrl+Left and Ctrl+W for forward-word, backward-word and backward-kill-word +// respectively. Ctrl+Delete is not implemented because it's a very different escape sequence. +// Depends on MICROPY_REPL_EMACS_WORDS_MOVE. +#ifndef MICROPY_REPL_EMACS_EXTRA_WORDS_MOVE +#define MICROPY_REPL_EMACS_EXTRA_WORDS_MOVE (0) +#endif + // Whether to implement auto-indent in REPL #ifndef MICROPY_REPL_AUTO_INDENT #define MICROPY_REPL_AUTO_INDENT (0) diff --git a/tests/cmdline/repl_words_move.py b/tests/cmdline/repl_words_move.py new file mode 100644 index 0000000000..e1eb529535 --- /dev/null +++ b/tests/cmdline/repl_words_move.py @@ -0,0 +1,31 @@ +# word movement +# backward-word, start in word +234b1 +# backward-word, don't start in word +234 b1 +# backward-word on start of line. if cursor is moved, this will result in a SyntaxError +1 2 + 3b+ +# forward-word, start in word +1+2 12+f+3 +# forward-word, don't start in word +1+ 12 3f+ +# forward-word on eol. if cursor is moved, this will result in a SyntaxError +1 + 2 3f+ + +# kill word +# backward-kill-word, start in word +100 + 45623 +# backward-kill-word, don't start in word +100 + 456231 +# forward-kill-word, start in word +100 + 256d3 +# forward-kill-word, don't start in word +1 + 256d2 + +# extra move/kill shortcuts +# ctrl-left +2341 +# ctrl-right +123 +# ctrl-w +1231 diff --git a/tests/cmdline/repl_words_move.py.exp b/tests/cmdline/repl_words_move.py.exp new file mode 100644 index 0000000000..86f6b77889 --- /dev/null +++ b/tests/cmdline/repl_words_move.py.exp @@ -0,0 +1,47 @@ +MicroPython \.\+ version +Use \.\+ +>>> # word movement +>>> # backward-word, start in word +>>> \.\+ +1234 +>>> # backward-word, don't start in word +>>> \.\+ +1234 +>>> # backward-word on start of line. if cursor is moved, this will result in a SyntaxError +>>> \.\+ +6 +>>> # forward-word, start in word +>>> \.\+ +18 +>>> # forward-word, don't start in word +>>> \.\+ +16 +>>> # forward-word on eol. if cursor is moved, this will result in a SyntaxError +>>> \.\+ +6 +>>> +>>> # kill word +>>> # backward-kill-word, start in word +>>> \.\+ +123 +>>> # backward-kill-word, don't start in word +>>> \.\+ +101 +>>> # forward-kill-word, start in word +>>> \.\+ +123 +>>> # forward-kill-word, don't start in word +>>> \.\+ +3 +>>> +>>> # extra move/kill shortcuts +>>> # ctrl-left +>>> \.\+ +1234 +>>> # ctrl-right +>>> \.\+ +123 +>>> # ctrl-w +>>> \.\+ +1 +>>> diff --git a/tests/feature_check/repl_words_move_check.py b/tests/feature_check/repl_words_move_check.py new file mode 100644 index 0000000000..e74615e98c --- /dev/null +++ b/tests/feature_check/repl_words_move_check.py @@ -0,0 +1,4 @@ +# just check if ctrl+w is supported, because it makes sure that +# both MICROPY_REPL_EMACS_WORDS_MOVE and MICROPY_REPL_EXTRA_WORDS_MOVE are enabled. +t = 1231 +t == 1 diff --git a/tests/feature_check/repl_words_move_check.py.exp b/tests/feature_check/repl_words_move_check.py.exp new file mode 100644 index 0000000000..82a4e28ee4 --- /dev/null +++ b/tests/feature_check/repl_words_move_check.py.exp @@ -0,0 +1,7 @@ +MicroPython \.\+ version +Use \.\+ +>>> # Check for emacs keys in REPL +>>> t = \.\+ +>>> t == 2 +True +>>> diff --git a/tests/run-tests b/tests/run-tests index b02463dc37..b8f5b6b7a1 100755 --- a/tests/run-tests +++ b/tests/run-tests @@ -284,9 +284,14 @@ def run_tests(pyb, tests, args, base_path="."): # Check if emacs repl is supported, and skip such tests if it's not t = run_feature_check(pyb, args, base_path, 'repl_emacs_check.py') - if not 'True' in str(t, 'ascii'): + if 'True' not in str(t, 'ascii'): skip_tests.add('cmdline/repl_emacs_keys.py') + # Check if words movement in repl is supported, and skip such tests if it's not + t = run_feature_check(pyb, args, base_path, 'repl_words_move_check.py') + if 'True' not in str(t, 'ascii'): + skip_tests.add('cmdline/repl_words_move.py') + upy_byteorder = run_feature_check(pyb, args, base_path, 'byteorder.py') upy_float_precision = run_feature_check(pyb, args, base_path, 'float.py') if upy_float_precision == b'CRASH':