From a1a2c411b25bc5a709f719a8a955727f9d6e228f Mon Sep 17 00:00:00 2001 From: Damien George Date: Sun, 26 Apr 2015 17:55:31 +0100 Subject: [PATCH] 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. will give a list of all board pins. Costs 700 bytes ROM on Thumb2 arch, but greatly increases usability of REPL prompt. --- lib/mp-readline/readline.c | 23 ++++++ py/repl.c | 143 ++++++++++++++++++++++++++++++++++++- py/repl.h | 2 + 3 files changed, 167 insertions(+), 1 deletion(-) diff --git a/lib/mp-readline/readline.c b/lib/mp-readline/readline.c index ce2a75905..f119fb620 100644 --- a/lib/mp-readline/readline.c +++ b/lib/mp-readline/readline.c @@ -29,6 +29,7 @@ #include #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); diff --git a/py/repl.c b/py/repl.c index c6b3e60ae..b03f0ed98 100644 --- a/py/repl.c +++ b/py/repl.c @@ -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 +#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 diff --git a/py/repl.h b/py/repl.h index db9256017..c34a5b869 100644 --- a/py/repl.h +++ b/py/repl.h @@ -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__