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 | |
35 | STATIC 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 | |
45 | bool 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 | |
146 | size_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 | |