py, readline: Add tab autocompletion for REPL.

Can complete names in the global namespace, as well as a chain of
attributes, eg pyb.Pin.board.<tab> will give a list of all board pins.

Costs 700 bytes ROM on Thumb2 arch, but greatly increases usability of
REPL prompt.
This commit is contained in:
Damien George 2015-04-26 17:55:31 +01:00
parent b7a4f15b34
commit a1a2c411b2
3 changed files with 167 additions and 1 deletions

View File

@ -29,6 +29,7 @@
#include <string.h>
#include "py/mpstate.h"
#include "py/repl.h"
#include "readline.h"
#ifdef MICROPY_HAL_H
#include MICROPY_HAL_H
@ -134,6 +135,28 @@ int readline_process_char(int c) {
redraw_step_back = 1;
redraw_from_cursor = true;
}
#if MICROPY_HELPER_REPL
} else if (c == 9) {
// tab magic
const char *compl_str;
mp_uint_t compl_len = mp_repl_autocomplete(rl.line->buf + rl.orig_line_len, rl.cursor_pos - rl.orig_line_len, &mp_plat_print, &compl_str);
if (compl_len == 0) {
// no match
} else if (compl_len == (mp_uint_t)(-1)) {
// many matches
mp_hal_stdout_tx_str(rl.prompt);
mp_hal_stdout_tx_strn(rl.line->buf + rl.orig_line_len, rl.cursor_pos - rl.orig_line_len);
redraw_from_cursor = true;
} else {
// one match
for (int i = 0; i < compl_len; ++i) {
vstr_ins_byte(rl.line, rl.cursor_pos + i, *compl_str++);
}
// set redraw parameters
redraw_from_cursor = true;
redraw_step_forward = compl_len;
}
#endif
} else if (32 <= c && c <= 126) {
// printable character
vstr_ins_char(rl.line, rl.cursor_pos, c);

143
py/repl.c
View File

@ -3,7 +3,7 @@
*
* The MIT License (MIT)
*
* Copyright (c) 2013, 2014 Damien P. George
* Copyright (c) 2013-2015 Damien P. George
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@ -24,6 +24,9 @@
* THE SOFTWARE.
*/
#include <string.h>
#include "py/obj.h"
#include "py/runtime.h"
#include "py/repl.h"
#if MICROPY_HELPER_REPL
@ -105,4 +108,142 @@ bool mp_repl_continue_with_input(const char *input) {
return false;
}
mp_uint_t mp_repl_autocomplete(const char *str, mp_uint_t len, const mp_print_t *print, const char **compl_str) {
// scan backwards to find start of "a.b.c" chain
const char *top = str + len;
for (const char *s = top; --s >= str;) {
if (!(unichar_isalpha(*s) || unichar_isdigit(*s) || *s == '_' || *s == '.')) {
++s;
str = s;
break;
}
}
// begin search in locals dict
mp_obj_dict_t *dict = mp_locals_get();
for (;;) {
// get next word in string to complete
const char *s_start = str;
while (str < top && *str != '.') {
++str;
}
mp_uint_t s_len = str - s_start;
if (str < top) {
// a complete word, lookup in current dict
mp_obj_t obj = MP_OBJ_NULL;
for (mp_uint_t i = 0; i < dict->map.alloc; i++) {
if (MP_MAP_SLOT_IS_FILLED(&dict->map, i)) {
mp_uint_t d_len;
const char *d_str = mp_obj_str_get_data(dict->map.table[i].key, &d_len);
if (s_len == d_len && strncmp(s_start, d_str, d_len) == 0) {
obj = dict->map.table[i].value;
break;
}
}
}
if (obj == MP_OBJ_NULL) {
// lookup failed
return 0;
}
// found an object of this name; try to get its dict
if (MP_OBJ_IS_TYPE(obj, &mp_type_module)) {
dict = mp_obj_module_get_globals(obj);
} else {
mp_obj_type_t *type;
if (MP_OBJ_IS_TYPE(obj, &mp_type_type)) {
type = obj;
} else {
type = mp_obj_get_type(obj);
}
if (type->locals_dict != MP_OBJ_NULL && MP_OBJ_IS_TYPE(type->locals_dict, &mp_type_dict)) {
dict = type->locals_dict;
} else {
// obj has no dict
return 0;
}
}
// skip '.' to move to next word
++str;
} else {
// end of string, do completion on this partial name
// look for matches
int n_found = 0;
const char *match_str = NULL;
mp_uint_t match_len = 0;
for (mp_uint_t i = 0; i < dict->map.alloc; i++) {
if (MP_MAP_SLOT_IS_FILLED(&dict->map, i)) {
mp_uint_t d_len;
const char *d_str = mp_obj_str_get_data(dict->map.table[i].key, &d_len);
if (s_len <= d_len && strncmp(s_start, d_str, s_len) == 0) {
if (match_str == NULL) {
match_str = d_str;
match_len = d_len;
} else {
for (mp_uint_t i = s_len; i < match_len && i < d_len; ++i) {
if (match_str[i] != d_str[i]) {
match_len = i;
break;
}
}
}
++n_found;
}
}
}
// nothing found
if (n_found == 0) {
return 0;
}
// 1 match found, or multiple matches with a common prefix
if (n_found == 1 || match_len > s_len) {
*compl_str = match_str + s_len;
return match_len - s_len;
}
// multiple matches found, print them out
#define WORD_SLOT_LEN (16)
#define MAX_LINE_LEN (4 * WORD_SLOT_LEN)
int line_len = MAX_LINE_LEN; // force a newline for first word
for (mp_uint_t i = 0; i < dict->map.alloc; i++) {
if (MP_MAP_SLOT_IS_FILLED(&dict->map, i)) {
mp_uint_t d_len;
const char *d_str = mp_obj_str_get_data(dict->map.table[i].key, &d_len);
if (s_len <= d_len && strncmp(s_start, d_str, s_len) == 0) {
int gap = (line_len + WORD_SLOT_LEN - 1) / WORD_SLOT_LEN * WORD_SLOT_LEN - line_len;
if (gap < 2) {
gap += WORD_SLOT_LEN;
}
if (line_len + gap + d_len <= MAX_LINE_LEN) {
// TODO optimise printing of gap?
for (int i = 0; i < gap; ++i) {
mp_print_str(print, " ");
}
mp_print_str(print, d_str);
line_len += gap + d_len;
} else {
mp_printf(print, "\n%s", d_str);
line_len = d_len;
}
}
}
}
mp_print_str(print, "\n");
return (mp_uint_t)(-1); // indicate many matches
}
}
}
#endif // MICROPY_HELPER_REPL

View File

@ -28,9 +28,11 @@
#include "py/mpconfig.h"
#include "py/misc.h"
#include "py/mpprint.h"
#if MICROPY_HELPER_REPL
bool mp_repl_continue_with_input(const char *input);
mp_uint_t mp_repl_autocomplete(const char *str, mp_uint_t len, const mp_print_t *print, const char **compl_str);
#endif
#endif // __MICROPY_INCLUDED_PY_REPL_H__