From 98396b32a7c8e7635f1d07e262467d362451b35b Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Wed, 4 Oct 2017 18:58:10 +0300 Subject: [PATCH] Interactive shell: object level completions. --- Makefile | 7 +- njs/njs.c | 87 +++++++------ njs/njs_builtin.c | 241 +++++++++++++++++++++++++++++++---- njs/njscript.h | 2 +- njs/test/njs_expect_test.exp | 168 ++++++++++++++++++++++++ nxt/auto/configure | 1 + nxt/auto/expect | 31 +++++ 7 files changed, 473 insertions(+), 64 deletions(-) create mode 100644 njs/test/njs_expect_test.exp create mode 100644 nxt/auto/expect diff --git a/Makefile b/Makefile index c33f262d..6e9dc54a 100644 --- a/Makefile +++ b/Makefile @@ -78,13 +78,14 @@ all: test lib_test njs: $(NXT_BUILDDIR)/njs -test: \ +njs_interactive_test: njs_expect_test $(NXT_BUILDDIR)/njs_interactive_test + $(NXT_BUILDDIR)/njs_interactive_test + +test: njs_interactive_test \ $(NXT_BUILDDIR)/njs_unit_test \ - $(NXT_BUILDDIR)/njs_interactive_test \ $(NXT_BUILDDIR)/njs_benchmark \ $(NXT_BUILDDIR)/njs_unit_test d - $(NXT_BUILDDIR)/njs_interactive_test clean: rm -rf $(NXT_BUILDDIR) diff --git a/njs/njs.c b/njs/njs.c index cff179e8..f99491f3 100644 --- a/njs/njs.c +++ b/njs/njs.c @@ -50,7 +50,8 @@ typedef struct { size_t index; size_t length; njs_vm_t *vm; - const char **completions; + nxt_array_t *completions; + nxt_array_t *suffix_completions; nxt_lvlhsh_each_t lhe; njs_completion_phase_t phase; } njs_completion_t; @@ -257,8 +258,7 @@ njs_interactive_shell(njs_opts_t *opts, njs_vm_opt_t *vm_options) printf("interactive njscript\n\n"); - printf("v -> the properties of v object.\n"); - printf("v. -> all the available prototype methods.\n"); + printf("v. -> the properties and prototype methods of v.\n"); printf("type console.help() for more information\n\n"); for ( ;; ) { @@ -487,7 +487,7 @@ njs_editline_init(njs_vm_t *vm) rl_attempted_completion_function = njs_completion_handler; rl_basic_word_break_characters = (char *) " \t\n\"\\'`@$><=;,|&{("; - njs_completion.completions = njs_vm_completions(vm); + njs_completion.completions = njs_vm_completions(vm, NULL); if (njs_completion.completions == NULL) { return NXT_ERROR; } @@ -507,12 +507,18 @@ njs_completion_handler(const char *text, int start, int end) } +/* editline frees the buffer every time. */ +#define njs_editline(s) strndup((char *) (s)->start, (s)->length) + +#define njs_completion(c, i) &(((nxt_str_t *) (c)->start)[i]) + static char * njs_completion_generator(const char *text, int state) { char *completion; size_t len; - const char *name, *p; + nxt_str_t expression, *suffix; + const char *p; njs_variable_t *var; njs_completion_t *cmpl; @@ -528,20 +534,20 @@ njs_completion_generator(const char *text, int state) if (cmpl->phase == NJS_COMPLETION_GLOBAL) { for ( ;; ) { - name = cmpl->completions[cmpl->index]; - if (name == NULL) { + if (cmpl->index >= cmpl->completions->items) { break; } - cmpl->index++; + suffix = njs_completion(cmpl->completions, cmpl->index++); - if (name[0] == '.') { + if (suffix->start[0] == '.' || suffix->length < cmpl->length) { continue; } - if (strncmp(name, text, cmpl->length) == 0) { - /* editline frees the buffer every time. */ - return strdup(name); + if (strncmp(text, (char *) suffix->start, + nxt_min(suffix->length, cmpl->length)) == 0) + { + return njs_editline(suffix); } } @@ -549,22 +555,14 @@ njs_completion_generator(const char *text, int state) for ( ;; ) { var = nxt_lvlhsh_each(&cmpl->vm->parser->scope->variables, &cmpl->lhe); - if (var == NULL) { + if (var == NULL || var->name.length < cmpl->length) { break; } - if (strncmp((char *) var->name.start, text, cmpl->length) - == 0) + if (strncmp(text, (char *) var->name.start, + nxt_min(var->name.length, cmpl->length)) == 0) { - completion = malloc(var->name.length + 1); - if (completion == NULL) { - return NULL; - } - - memcpy(completion, var->name.start, var->name.length); - completion[var->name.length] = '\0'; - - return completion; + return njs_editline(&var->name); } } } @@ -573,11 +571,30 @@ njs_completion_generator(const char *text, int state) return NULL; } + /* Getting the longest prefix before a '.' */ + + p = &text[cmpl->length - 1]; + while (p > text && *p != '.') { p--; } + + if (*p != '.') { + return NULL; + } + + expression.start = (u_char *) text; + expression.length = p - text; + + cmpl->suffix_completions = njs_vm_completions(cmpl->vm, &expression); + if (cmpl->suffix_completions == NULL) { + return NULL; + } + cmpl->index = 0; cmpl->phase = NJS_COMPLETION_SUFFIX; } - len = 1; + /* Getting the right-most suffix after a '.' */ + + len = 0; p = &text[cmpl->length - 1]; while (p > text && *p != '.') { @@ -585,31 +602,29 @@ njs_completion_generator(const char *text, int state) len++; } - if (*p != '.') { - return NULL; - } + p++; for ( ;; ) { - name = cmpl->completions[cmpl->index++]; - if (name == NULL) { + if (cmpl->index >= cmpl->suffix_completions->items) { break; } - if (name[0] != '.') { - continue; - } + suffix = njs_completion(cmpl->suffix_completions, cmpl->index++); - if (strncmp(name, p, len) != 0) { + if (len != 0 && strncmp((char *) suffix->start, p, + nxt_min(len, suffix->length)) != 0) + { continue; } - len = strlen(name) + (p - text) + 2; + len = suffix->length + (p - text) + 1; completion = malloc(len); if (completion == NULL) { return NULL; } - snprintf(completion, len, "%.*s%s", (int) (p - text), text, name); + snprintf(completion, len, "%.*s%.*s", (int) (p - text), text, + (int) suffix->length, suffix->start); return completion; } diff --git a/njs/njs_builtin.c b/njs/njs_builtin.c index 6aae9e99..1d36d447 100644 --- a/njs/njs_builtin.c +++ b/njs/njs_builtin.c @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -39,7 +40,10 @@ typedef struct { static nxt_int_t njs_builtin_completions(njs_vm_t *vm, size_t *size, - const char **completions); + nxt_str_t *completions); +static nxt_array_t *njs_vm_expression_completions(njs_vm_t *vm, + nxt_str_t *expression); +static nxt_array_t *njs_object_completions(njs_vm_t *vm, njs_object_t *object); const njs_object_init_t *njs_object_init[] = { @@ -346,33 +350,40 @@ njs_builtin_objects_clone(njs_vm_t *vm) } -const char ** -njs_vm_completions(njs_vm_t *vm) +nxt_array_t * +njs_vm_completions(njs_vm_t *vm, nxt_str_t *expression) { - size_t size; - const char **completions; + size_t size; + nxt_array_t *completions; - if (njs_builtin_completions(vm, &size, NULL) != NXT_OK) { - return NULL; - } + if (expression == NULL) { + if (njs_builtin_completions(vm, &size, NULL) != NXT_OK) { + return NULL; + } - completions = nxt_mem_cache_zalloc(vm->mem_cache_pool, - sizeof(char *) * (size + 1)); + completions = nxt_array_create(size, sizeof(nxt_str_t), + &njs_array_mem_proto, + vm->mem_cache_pool); - if (completions == NULL) { - return NULL; - } + if (nxt_slow_path(completions == NULL)) { + return NULL; + } - if (njs_builtin_completions(vm, NULL, completions) != NXT_OK) { - return NULL; + if (njs_builtin_completions(vm, &size, completions->start) != NXT_OK) { + return NULL; + } + + completions->items = size; + + return completions; } - return completions; + return njs_vm_expression_completions(vm, expression); } static nxt_int_t -njs_builtin_completions(njs_vm_t *vm, size_t *size, const char **completions) +njs_builtin_completions(njs_vm_t *vm, size_t *size, nxt_str_t *completions) { char *compl; size_t n, len; @@ -398,7 +409,7 @@ njs_builtin_completions(njs_vm_t *vm, size_t *size, const char **completions) } if (completions != NULL) { - completions[n++] = (char *) keyword->name.start; + completions[n++] = keyword->name; } else { n++; @@ -433,7 +444,8 @@ njs_builtin_completions(njs_vm_t *vm, size_t *size, const char **completions) snprintf(compl, len, "%s.%s", njs_object_init[i]->name.start, string.start); - completions[n++] = (char *) compl; + completions[n].length = len; + completions[n++].start = (u_char *) compl; } else { n++; @@ -465,13 +477,16 @@ njs_builtin_completions(njs_vm_t *vm, size_t *size, const char **completions) snprintf(compl, len, ".%s", string.start); for (k = 0; k < n; k++) { - if (strncmp(completions[k], compl, len) == 0) { + if (strncmp((char *) completions[k].start, compl, len) + == 0) + { break; } } if (k == n) { - completions[n++] = (char *) compl; + completions[n].length = len; + completions[n++].start = (u_char *) compl; } } else { @@ -504,7 +519,8 @@ njs_builtin_completions(njs_vm_t *vm, size_t *size, const char **completions) snprintf(compl, len, "%s.%s", njs_constructor_init[i]->name.start, string.start); - completions[n++] = (char *) compl; + completions[n].length = len; + completions[n++].start = (u_char *) compl; } else { n++; @@ -533,7 +549,8 @@ njs_builtin_completions(njs_vm_t *vm, size_t *size, const char **completions) snprintf(compl, len, "%.*s", (int) ext_object->name.length, ext_object->name.start); - completions[n++] = (char *) compl; + completions[n].length = len; + completions[n++].start = (u_char *) compl; } else { n++; @@ -557,7 +574,8 @@ njs_builtin_completions(njs_vm_t *vm, size_t *size, const char **completions) (int) ext_object->name.length, ext_object->name.start, (int) ext_prop->name.length, ext_prop->name.start); - completions[n++] = (char *) compl; + completions[n].length = len; + completions[n++].start = (u_char *) compl; } else { n++; @@ -573,6 +591,181 @@ njs_builtin_completions(njs_vm_t *vm, size_t *size, const char **completions) } +static nxt_array_t * +njs_vm_expression_completions(njs_vm_t *vm, nxt_str_t *expression) +{ + u_char *p, *end; + nxt_int_t ret; + njs_value_t *value; + njs_variable_t *var; + njs_object_prop_t *prop; + nxt_lvlhsh_query_t lhq; + + if (nxt_slow_path(vm->parser == NULL)) { + return NULL; + } + + p = expression->start; + end = p + expression->length; + + lhq.key.start = p; + + while (p < end && *p != '.') { p++; } + + lhq.proto = &njs_variables_hash_proto; + lhq.key.length = p - lhq.key.start; + lhq.key_hash = nxt_djb_hash(lhq.key.start, lhq.key.length); + + ret = nxt_lvlhsh_find(&vm->parser->scope->variables, &lhq); + if (nxt_slow_path(ret != NXT_OK)) { + return NULL; + } + + var = lhq.value; + value = njs_vmcode_operand(vm, var->index); + + if (!njs_is_object(value)) { + return NULL; + } + + lhq.proto = &njs_object_hash_proto; + + for ( ;; ) { + + if (p == end) { + break; + } + + lhq.key.start = ++p; + + while (p < end && *p != '.') { p++; } + + lhq.key.length = p - lhq.key.start; + lhq.key_hash = nxt_djb_hash(lhq.key.start, lhq.key.length); + + ret = nxt_lvlhsh_find(&value->data.u.object->hash, &lhq); + if (nxt_slow_path(ret != NXT_OK)) { + return NULL; + } + + prop = lhq.value; + + if (!njs_is_object(&prop->value)) { + return NULL; + } + + value = &prop->value; + } + + return njs_object_completions(vm, value->data.u.object); +} + + +static nxt_array_t * +njs_object_completions(njs_vm_t *vm, njs_object_t *object) +{ + size_t size; + nxt_uint_t n, k; + nxt_str_t *compl; + nxt_array_t *completions; + njs_object_t *o; + njs_object_prop_t *prop; + nxt_lvlhsh_each_t lhe; + + size = 0; + o = object; + + do { + nxt_lvlhsh_each_init(&lhe, &njs_object_hash_proto); + + for ( ;; ) { + prop = nxt_lvlhsh_each(&o->hash, &lhe); + if (prop == NULL) { + break; + } + + size++; + } + + nxt_lvlhsh_each_init(&lhe, &njs_object_hash_proto); + + for ( ;; ) { + prop = nxt_lvlhsh_each(&o->shared_hash, &lhe); + if (prop == NULL) { + break; + } + + size++; + } + + o = o->__proto__; + + } while (o != NULL); + + completions = nxt_array_create(size, sizeof(nxt_str_t), + &njs_array_mem_proto, vm->mem_cache_pool); + + if (nxt_slow_path(completions == NULL)) { + return NULL; + } + + n = 0; + o = object; + compl = completions->start; + + do { + nxt_lvlhsh_each_init(&lhe, &njs_object_hash_proto); + + for ( ;; ) { + prop = nxt_lvlhsh_each(&o->hash, &lhe); + if (prop == NULL) { + break; + } + + njs_string_get(&prop->name, &compl[n]); + + for (k = 0; k < n; k++) { + if (nxt_strstr_eq(&compl[k], &compl[n])) { + break; + } + } + + if (k == n) { + n++; + } + } + + nxt_lvlhsh_each_init(&lhe, &njs_object_hash_proto); + + for ( ;; ) { + prop = nxt_lvlhsh_each(&o->shared_hash, &lhe); + if (prop == NULL) { + break; + } + + njs_string_get(&prop->name, &compl[n]); + + for (k = 0; k < n; k++) { + if (nxt_strstr_eq(&compl[k], &compl[n])) { + break; + } + } + + if (k == n) { + n++; + } + } + + o = o->__proto__; + + } while (o != NULL); + + completions->items = n; + + return completions; +} + + nxt_int_t njs_builtin_match_native_function(njs_vm_t *vm, njs_function_t *function, nxt_str_t *name) diff --git a/njs/njscript.h b/njs/njscript.h index 041f3344..bd3b376f 100644 --- a/njs/njscript.h +++ b/njs/njscript.h @@ -123,7 +123,7 @@ NXT_EXPORT void *njs_value_data(njs_value_t *value); NXT_EXPORT nxt_int_t njs_value_string_copy(njs_vm_t *vm, nxt_str_t *retval, njs_value_t *value, uintptr_t *next); -NXT_EXPORT const char **njs_vm_completions(njs_vm_t *vm); +NXT_EXPORT nxt_array_t *njs_vm_completions(njs_vm_t *vm, nxt_str_t *expression); extern const nxt_mem_proto_t njs_vm_mem_cache_pool_proto; diff --git a/njs/test/njs_expect_test.exp b/njs/test/njs_expect_test.exp new file mode 100644 index 00000000..30c85191 --- /dev/null +++ b/njs/test/njs_expect_test.exp @@ -0,0 +1,168 @@ +# +# Copyright (C) Dmitry Volyntsev +# Copyright (C) NGINX, Inc. +# + +proc njs_test {body} { + spawn -nottycopy njs + expect "interactive njscript\r +\r +v. -> the properties and prototype methods of v.\r +type console.help() for more information\r +\r +>> " + + set len [llength $body] + for {set i 0} {$i < $len} {incr i} { + set pair [lindex $body $i] + send [lindex $pair 0] + expect [lindex $pair 1] + } + + # Ctrl-C + send \x03 + expect eof +} + +# simple multi line interation +njs_test { + {"var a = 1\r\n" + "var a = 1\r\nundefined\r\n>> "} + {"a *= 2\r\n" + "a *= 2\r\n2\r\n>> "} +} + +# Global completions, no +njs_test { + {"\t\tn" + "\a\r\nDisplay all*possibilities? (y or n)*>> "} +} + +# Global completions, yes +njs_test { + {"\t\ty" + "\a\r\nDisplay all*possibilities? (y or n)*abstract"} +} + +# Global completions, single partial match +njs_test { + {"O\t" + "O\abject"} +} + +njs_test { + {"M\t" + "M\aath"} +} + +njs_test { + {"conso\t" + "conso\ale"} +} + +# Global completions, multiple partial match +njs_test { + {"cons\t\t" + "console*console.help*console.log*const"} +} + +njs_test { + {"O\t" + "O\abject"} + {".\t\t" + "Object.create*Object.isSealed"} +} + +njs_test { + {"Object.\t\t" + "Object.create*Object.isSealed"} +} + +njs_test { + {"Object.g\t" + "Object.g\aet"} + {"\t" + "Object.getOwnPropertyDescriptor*Object.getPrototypeOf"} +} + +njs_test { + {"M\t" + "M\aath"} + {".\t\t" + "Math.__proto__*Math.cbrt*Math.fround*Math.log2"} +} + +# Global completions, no matches +njs_test { + {"1.\t\t" + "1."} +} + +njs_test { + {"1..\t\t" + "1.."} +} + +njs_test { + {"'abc'.\t\t" + "'abc'."} +} + +# Global completions, global vars +njs_test { + {"var a = 1; var aa = 2\r\n" + "var a = 1; var aa = 2\r\nundefined\r\n>> "} + {"a\t\t" + "a*aa*abstract"} +} + +njs_test { + {"var zz = 1\r\n" + "var zz = 1\r\nundefined\r\n>> "} + {"1 + z\t\r\n" + "1 + zz\r\n2"} +} + +njs_test { + {"unknown_var\t\t" + "unknown_var"} +} + +njs_test { + {"unknown_var.\t\t" + "unknown_var."} +} + +# An object's level completions +njs_test { + {"var o = {zz:1, zb:2}\r\n" + "var o = {zz:1, zb:2}\r\nundefined\r\n>> "} + {"o.z\t\t" + "o.zb*o.zz"} +} + +njs_test { + {"var d = new Date()\r\n" + "var d = new Date()\r\nundefined\r\n>> "} + {"d.to\t\t" + "d.toDateString*d.toLocaleDateString*d.toString"} +} + +njs_test { + {"var o = {a:new Date()}\r\n" + "var o = {a:new Date()}\r\nundefined\r\n>> "} + {"o.a.to\t\t" + "o.a.toDateString*o.a.toLocaleDateString*o.a.toString"} +} + +# console object +njs_test { + {"console.log()\r\n" + "console.log()\r\n\r\nundefined\r\n>> "} + {"console.log(1)\r\n" + "console.log(1)\r\n1\r\nundefined\r\n>> "} + {"console.log('abc')\r\n" + "console.log('abc')\r\nabc\r\nundefined\r\n>> "} + {"console.help()\r\n" + "console.help()\r\nVM built-in objects:"} +} diff --git a/nxt/auto/configure b/nxt/auto/configure index 0ea33a7b..933e2a6b 100755 --- a/nxt/auto/configure +++ b/nxt/auto/configure @@ -56,3 +56,4 @@ END . ${NXT_AUTO}getrandom . ${NXT_AUTO}pcre . ${NXT_AUTO}editline +. ${NXT_AUTO}expect diff --git a/nxt/auto/expect b/nxt/auto/expect new file mode 100644 index 00000000..b924bbd5 --- /dev/null +++ b/nxt/auto/expect @@ -0,0 +1,31 @@ + +# Copyright (C) Dmitry Volyntsev +# Copyright (C) NGINX, Inc. + +nxt_found=no +$nxt_echo -n "checking for expect ..." + +if /bin/sh -c "(expect -v)" >> $NXT_AUTOCONF_ERR 2>&1; then + nxt_found=yes +fi + +if [ $nxt_found = yes ]; then + $nxt_echo " found" + $nxt_echo " + Expect version: `expect -v`" + cat << END >> $NXT_MAKEFILE_CONF + +njs_expect_test: njs njs/test/njs_expect_test.exp + PATH=\$(PATH):\$(NXT_BUILDDIR) expect -f njs/test/njs_expect_test.exp +END + +else + $nxt_echo " not found" + $nxt_echo " - expect tests are disabled" + + cat << END >> $NXT_MAKEFILE_CONF + +njs_expect_test: + @echo "Skipping expect tests" +END + +fi -- 2.47.3