1/*
2 * This file is part of the MicroPython project, http://micropython.org/
3 *
4 * The MIT License (MIT)
5 *
6 * Copyright (c) 2013-2015 Damien P. George
7 *
8 * Permission is hereby granted, free of charge, to any person obtaining a copy
9 * of this software and associated documentation files (the "Software"), to deal
10 * in the Software without restriction, including without limitation the rights
11 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 * copies of the Software, and to permit persons to whom the Software is
13 * furnished to do so, subject to the following conditions:
14 *
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
17 *
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 * THE SOFTWARE.
25 */
26
27#include <string.h>
28#include "py/obj.h"
29#include "py/runtime.h"
30#include "py/builtin.h"
31#include "py/repl.h"
32
33#if MICROPY_HELPER_REPL
34
35STATIC bool str_startswith_word(const char *str, const char *head) {
36 size_t i;
37 for (i = 0; str[i] && head[i]; i++) {
38 if (str[i] != head[i]) {
39 return false;
40 }
41 }
42 return head[i] == '\0' && (str[i] == '\0' || !unichar_isident(str[i]));
43}
44
45bool mp_repl_continue_with_input(const char *input) {
46 // check for blank input
47 if (input[0] == '\0') {
48 return false;
49 }
50
51 // check if input starts with a certain keyword
52 bool starts_with_compound_keyword =
53 input[0] == '@'
54 || str_startswith_word(input, "if")
55 || str_startswith_word(input, "while")
56 || str_startswith_word(input, "for")
57 || str_startswith_word(input, "try")
58 || str_startswith_word(input, "with")
59 || str_startswith_word(input, "def")
60 || str_startswith_word(input, "class")
61 #if MICROPY_PY_ASYNC_AWAIT
62 || str_startswith_word(input, "async")
63 #endif
64 ;
65
66 // check for unmatched open bracket, quote or escape quote
67 #define Q_NONE (0)
68 #define Q_1_SINGLE (1)
69 #define Q_1_DOUBLE (2)
70 #define Q_3_SINGLE (3)
71 #define Q_3_DOUBLE (4)
72 int n_paren = 0;
73 int n_brack = 0;
74 int n_brace = 0;
75 int in_quote = Q_NONE;
76 const char *i;
77 for (i = input; *i; i++) {
78 if (*i == '\'') {
79 if ((in_quote == Q_NONE || in_quote == Q_3_SINGLE) && i[1] == '\'' && i[2] == '\'') {
80 i += 2;
81 in_quote = Q_3_SINGLE - in_quote;
82 } else if (in_quote == Q_NONE || in_quote == Q_1_SINGLE) {
83 in_quote = Q_1_SINGLE - in_quote;
84 }
85 } else if (*i == '"') {
86 if ((in_quote == Q_NONE || in_quote == Q_3_DOUBLE) && i[1] == '"' && i[2] == '"') {
87 i += 2;
88 in_quote = Q_3_DOUBLE - in_quote;
89 } else if (in_quote == Q_NONE || in_quote == Q_1_DOUBLE) {
90 in_quote = Q_1_DOUBLE - in_quote;
91 }
92 } else if (*i == '\\' && (i[1] == '\'' || i[1] == '"' || i[1] == '\\')) {
93 if (in_quote != Q_NONE) {
94 i++;
95 }
96 } else if (in_quote == Q_NONE) {
97 switch (*i) {
98 case '(':
99 n_paren += 1;
100 break;
101 case ')':
102 n_paren -= 1;
103 break;
104 case '[':
105 n_brack += 1;
106 break;
107 case ']':
108 n_brack -= 1;
109 break;
110 case '{':
111 n_brace += 1;
112 break;
113 case '}':
114 n_brace -= 1;
115 break;
116 default:
117 break;
118 }
119 }
120 }
121
122 // continue if unmatched 3-quotes
123 if (in_quote == Q_3_SINGLE || in_quote == Q_3_DOUBLE) {
124 return true;
125 }
126
127 // continue if unmatched brackets, but only if not in a 1-quote
128 if ((n_paren > 0 || n_brack > 0 || n_brace > 0) && in_quote == Q_NONE) {
129 return true;
130 }
131
132 // continue if last character was backslash (for line continuation)
133 if (i[-1] == '\\') {
134 return true;
135 }
136
137 // continue if compound keyword and last line was not empty
138 if (starts_with_compound_keyword && i[-1] != '\n') {
139 return true;
140 }
141
142 // otherwise, don't continue
143 return false;
144}
145
146size_t mp_repl_autocomplete(const char *str, size_t len, const mp_print_t *print, const char **compl_str) {
147 // scan backwards to find start of "a.b.c" chain
148 const char *org_str = str;
149 const char *top = str + len;
150 for (const char *s = top; --s >= str;) {
151 if (!(unichar_isalpha(*s) || unichar_isdigit(*s) || *s == '_' || *s == '.')) {
152 ++s;
153 str = s;
154 break;
155 }
156 }
157
158 size_t nqstr = QSTR_TOTAL();
159
160 // begin search in outer global dict which is accessed from __main__
161 mp_obj_t obj = MP_OBJ_FROM_PTR(&mp_module___main__);
162 mp_obj_t dest[2];
163
164 for (;;) {
165 // get next word in string to complete
166 const char *s_start = str;
167 while (str < top && *str != '.') {
168 ++str;
169 }
170 size_t s_len = str - s_start;
171
172 if (str < top) {
173 // a complete word, lookup in current object
174 qstr q = qstr_find_strn(s_start, s_len);
175 if (q == MP_QSTRnull) {
176 // lookup will fail
177 return 0;
178 }
179 mp_load_method_protected(obj, q, dest, true);
180 obj = dest[0]; // attribute, method, or MP_OBJ_NULL if nothing found
181
182 if (obj == MP_OBJ_NULL) {
183 // lookup failed
184 return 0;
185 }
186
187 // skip '.' to move to next word
188 ++str;
189
190 } else {
191 // end of string, do completion on this partial name
192
193 // look for matches
194 const char *match_str = NULL;
195 size_t match_len = 0;
196 qstr q_first = 0, q_last = 0;
197 for (qstr q = MP_QSTR_ + 1; q < nqstr; ++q) {
198 size_t d_len;
199 const char *d_str = (const char *)qstr_data(q, &d_len);
200 if (s_len <= d_len && strncmp(s_start, d_str, s_len) == 0) {
201 mp_load_method_protected(obj, q, dest, true);
202 if (dest[0] != MP_OBJ_NULL) {
203 if (match_str == NULL) {
204 match_str = d_str;
205 match_len = d_len;
206 } else {
207 // search for longest common prefix of match_str and d_str
208 // (assumes these strings are null-terminated)
209 for (size_t j = s_len; j <= match_len && j <= d_len; ++j) {
210 if (match_str[j] != d_str[j]) {
211 match_len = j;
212 break;
213 }
214 }
215 }
216 if (q_first == 0) {
217 q_first = q;
218 }
219 q_last = q;
220 }
221 }
222 }
223
224 // nothing found
225 if (q_first == 0) {
226 // If there're no better alternatives, and if it's first word
227 // in the line, try to complete "import".
228 if (s_start == org_str) {
229 static const char import_str[] = "import ";
230 if (memcmp(s_start, import_str, s_len) == 0) {
231 *compl_str = import_str + s_len;
232 return sizeof(import_str) - 1 - s_len;
233 }
234 }
235
236 return 0;
237 }
238
239 // 1 match found, or multiple matches with a common prefix
240 if (q_first == q_last || match_len > s_len) {
241 *compl_str = match_str + s_len;
242 return match_len - s_len;
243 }
244
245 // multiple matches found, print them out
246
247 #define WORD_SLOT_LEN (16)
248 #define MAX_LINE_LEN (4 * WORD_SLOT_LEN)
249
250 int line_len = MAX_LINE_LEN; // force a newline for first word
251 for (qstr q = q_first; q <= q_last; ++q) {
252 size_t d_len;
253 const char *d_str = (const char *)qstr_data(q, &d_len);
254 if (s_len <= d_len && strncmp(s_start, d_str, s_len) == 0) {
255 mp_load_method_protected(obj, q, dest, true);
256 if (dest[0] != MP_OBJ_NULL) {
257 int gap = (line_len + WORD_SLOT_LEN - 1) / WORD_SLOT_LEN * WORD_SLOT_LEN - line_len;
258 if (gap < 2) {
259 gap += WORD_SLOT_LEN;
260 }
261 if (line_len + gap + d_len <= MAX_LINE_LEN) {
262 // TODO optimise printing of gap?
263 for (int j = 0; j < gap; ++j) {
264 mp_print_str(print, " ");
265 }
266 mp_print_str(print, d_str);
267 line_len += gap + d_len;
268 } else {
269 mp_printf(print, "\n%s", d_str);
270 line_len = d_len;
271 }
272 }
273 }
274 }
275 mp_print_str(print, "\n");
276
277 return (size_t)(-1); // indicate many matches
278 }
279 }
280}
281
282#endif // MICROPY_HELPER_REPL
283