summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYonatan Goldschmidt <yon.goldschmidt@gmail.com>2019-12-14 23:42:03 +0200
committerDamien George <damien.p.george@gmail.com>2020-01-12 13:09:27 +1100
commit853aaa06f24c98191a44a38eedd4ec2a0e63d3eb (patch)
tree8ea2c47102ae540789b481f665518d898f79c61b
parentdce590c29dbefea253f4034c4bde3508f205364e (diff)
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.
-rw-r--r--lib/mp-readline/readline.c86
-rw-r--r--lib/mp-readline/readline.h1
-rw-r--r--ports/unix/variants/coverage/mpconfigvariant.h2
-rw-r--r--ports/unix/variants/dev/mpconfigvariant.h3
-rw-r--r--py/mpconfig.h15
-rw-r--r--tests/cmdline/repl_words_move.py31
-rw-r--r--tests/cmdline/repl_words_move.py.exp47
-rw-r--r--tests/feature_check/repl_words_move_check.py4
-rw-r--r--tests/feature_check/repl_words_move_check.py.exp7
-rwxr-xr-xtests/run-tests7
10 files changed, 202 insertions, 1 deletions
diff --git a/lib/mp-readline/readline.c b/lib/mp-readline/readline.c
index 1500873f6..296c8aa4a 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 00aa9622a..a19e1209a 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 f383a8370..719292453 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 84a3edd45..2b3097010 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 d6f4d9232..2c4169070 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 000000000..e1eb52953
--- /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 000000000..86f6b7788
--- /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 000000000..e74615e98
--- /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 000000000..82a4e28ee
--- /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 b02463dc3..b8f5b6b7a 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':