Skip to content

Readline word sequences #5420

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions lib/mp-readline/readline.c
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ STATIC void mp_hal_move_cursor_back(uint pos) {
// snprintf needs space for the terminating null character
int n = snprintf(&vt100_command[0], sizeof(vt100_command), "\x1b[%u", pos);
if (n > 0) {
assert((unsigned)n < sizeof(vt100_command));
vt100_command[n] = 'D'; // replace null char
mp_hal_stdout_tx_strn(vt100_command, n + 1);
}
Expand All @@ -98,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;

while (1) {
// 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 ?: -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;
Expand Down Expand Up @@ -148,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_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");
Expand Down Expand Up @@ -221,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_EXTRA_WORDS_MOVE
backward_word:
#endif
redraw_step_back = cursor_count_word(0);
rl.escape_seq = ESEQ_NONE;
break;
case 'f':
#if MICROPY_REPL_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_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') {
Expand Down Expand Up @@ -311,6 +376,24 @@ int readline_process_char(int c) {
} else {
DEBUG_printf("(ESC [ %c %d)", rl.escape_seq_buf[0], c);
}
#if MICROPY_REPL_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 overrided.
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);
}
Expand All @@ -329,6 +412,10 @@ int readline_process_char(int c) {
rl.escape_seq = ESEQ_NONE;
}

#if MICROPY_REPL_EXTRA_WORDS_MOVE
redraw:
#endif

// redraw command prompt, efficiently
if (redraw_step_back > 0) {
mp_hal_move_cursor_back(redraw_step_back);
Expand Down
1 change: 1 addition & 0 deletions lib/mp-readline/readline.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions ports/unix/mpconfigport.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@
#define MICROPY_USE_READLINE_HISTORY (1)
#define MICROPY_HELPER_REPL (1)
#define MICROPY_REPL_EMACS_KEYS (1)
#define MICROPY_REPL_EMACS_WORDS_MOVE (1)
#define MICROPY_REPL_EXTRA_WORDS_MOVE (1)
#define MICROPY_REPL_AUTO_INDENT (1)
#define MICROPY_HELPER_LEXER_UNIX (1)
#define MICROPY_ENABLE_SOURCE_LINE (1)
Expand Down
1 change: 1 addition & 0 deletions py/misc.h
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ bool unichar_isprint(unichar c);
bool unichar_isdigit(unichar c);
bool unichar_isxdigit(unichar c);
bool unichar_isident(unichar c);
bool unichar_isalnum(unichar c);
bool unichar_isupper(unichar c);
bool unichar_islower(unichar c);
unichar unichar_tolower(unichar c);
Expand Down
15 changes: 15 additions & 0 deletions py/mpconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,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_EXTRA_WORDS_MOVE
#define MICROPY_REPL_EXTRA_WORDS_MOVE (0)
#endif

// Whether to implement auto-indent in REPL
#ifndef MICROPY_REPL_AUTO_INDENT
#define MICROPY_REPL_AUTO_INDENT (0)
Expand Down
4 changes: 4 additions & 0 deletions py/unicode.c
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ bool unichar_isident(unichar c) {
return c < 128 && ((attr[c] & (FL_ALPHA | FL_DIGIT)) != 0 || c == '_');
}

bool unichar_isalnum(unichar c) {
return c < 128 && ((attr[c] & (FL_ALPHA | FL_DIGIT)) != 0);
}

bool unichar_isupper(unichar c) {
return c < 128 && (attr[c] & FL_UPPER) != 0;
}
Expand Down
31 changes: 31 additions & 0 deletions tests/cmdline/repl_words_move.py
Original file line number Diff line number Diff line change
@@ -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
47 changes: 47 additions & 0 deletions tests/cmdline/repl_words_move.py.exp
Original file line number Diff line number Diff line change
@@ -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
>>>
4 changes: 4 additions & 0 deletions tests/feature_check/repl_words_move_check.py
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions tests/feature_check/repl_words_move_check.py.exp
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
MicroPython \.\+ version
Use \.\+
>>> # Check for emacs keys in REPL
>>> t = \.\+
>>> t == 2
True
>>>
7 changes: 6 additions & 1 deletion tests/run-tests
Original file line number Diff line number Diff line change
Expand Up @@ -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 = int(run_feature_check(pyb, args, base_path, 'float.py'))
has_complex = run_feature_check(pyb, args, base_path, 'complex.py') == b'complex\n'
Expand Down
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy