[njs] Interactive shell: object level completions.

Dmitry Volyntsev xeioex at nginx.com
Wed Oct 4 15:58:40 UTC 2017


details:   http://hg.nginx.org/njs/rev/d45f6f0079ab
branches:  
changeset: 411:d45f6f0079ab
user:      Dmitry Volyntsev <xeioex at nginx.com>
date:      Wed Oct 04 18:58:10 2017 +0300
description:
Interactive shell: object level completions.

diffstat:

 Makefile                     |    7 +-
 njs/njs.c                    |   89 +++++++++------
 njs/njs_builtin.c            |  247 ++++++++++++++++++++++++++++++++++++++----
 njs/njscript.h               |    2 +-
 njs/test/njs_expect_test.exp |  168 +++++++++++++++++++++++++++++
 nxt/auto/configure           |    1 +
 nxt/auto/expect              |   31 +++++
 7 files changed, 477 insertions(+), 68 deletions(-)

diffs (762 lines):

diff -r 94c42736a730 -r d45f6f0079ab Makefile
--- a/Makefile	Tue Oct 03 21:24:58 2017 +0300
+++ b/Makefile	Wed Oct 04 18:58:10 2017 +0300
@@ -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 -r 94c42736a730 -r d45f6f0079ab njs/njs.c
--- a/njs/njs.c	Tue Oct 03 21:24:58 2017 +0300
+++ b/njs/njs.c	Wed Oct 04 18:58:10 2017 +0300
@@ -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, 
 
     printf("interactive njscript\n\n");
 
-    printf("v<Tab> -> the properties of v object.\n");
-    printf("v.<Tab> -> all the available prototype methods.\n");
+    printf("v.<Tab> -> 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,
 }
 
 
+/* 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 *tex
 
     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 *tex
             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 *tex
             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 *tex
         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] != '.') {
+        suffix = njs_completion(cmpl->suffix_completions, cmpl->index++);
+
+        if (len != 0 && strncmp((char *) suffix->start, p,
+                                nxt_min(len, suffix->length)) != 0)
+        {
             continue;
         }
 
-        if (strncmp(name, p, len) != 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 -r 94c42736a730 -r d45f6f0079ab njs/njs_builtin.c
--- a/njs/njs_builtin.c	Tue Oct 03 21:24:58 2017 +0300
+++ b/njs/njs_builtin.c	Wed Oct 04 18:58:10 2017 +0300
@@ -8,6 +8,7 @@
 #include <nxt_types.h>
 #include <nxt_clang.h>
 #include <nxt_string.h>
+#include <nxt_djb_hash.h>
 #include <nxt_stub.h>
 #include <nxt_array.h>
 #include <nxt_lvlhsh.h>
@@ -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 (expression == NULL) {
+        if (njs_builtin_completions(vm, &size, NULL) != NXT_OK) {
+            return NULL;
+        }
+
+        completions = nxt_array_create(size, sizeof(nxt_str_t),
+                                       &njs_array_mem_proto,
+                                       vm->mem_cache_pool);
 
-    if (njs_builtin_completions(vm, &size, NULL) != NXT_OK) {
-        return NULL;
+        if (nxt_slow_path(completions == NULL)) {
+            return NULL;
+        }
+
+        if (njs_builtin_completions(vm, &size, completions->start) != NXT_OK) {
+            return NULL;
+        }
+
+        completions->items = size;
+
+        return completions;
     }
 
-    completions = nxt_mem_cache_zalloc(vm->mem_cache_pool,
-                                       sizeof(char *) * (size + 1));
-
-    if (completions == NULL) {
-        return NULL;
-    }
-
-    if (njs_builtin_completions(vm, NULL, completions) != NXT_OK) {
-        return NULL;
-    }
-
-    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, si
         }
 
         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, si
                 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, si
                 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, si
                 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, si
             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, si
                          (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, si
 }
 
 
+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 -r 94c42736a730 -r d45f6f0079ab njs/njscript.h
--- a/njs/njscript.h	Tue Oct 03 21:24:58 2017 +0300
+++ b/njs/njscript.h	Wed Oct 04 18:58:10 2017 +0300
@@ -123,7 +123,7 @@ NXT_EXPORT void *njs_value_data(njs_valu
 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 -r 94c42736a730 -r d45f6f0079ab njs/test/njs_expect_test.exp
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/njs/test/njs_expect_test.exp	Wed Oct 04 18:58:10 2017 +0300
@@ -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.<Tab> -> 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 -r 94c42736a730 -r d45f6f0079ab nxt/auto/configure
--- a/nxt/auto/configure	Tue Oct 03 21:24:58 2017 +0300
+++ b/nxt/auto/configure	Wed Oct 04 18:58:10 2017 +0300
@@ -56,3 +56,4 @@ END
 . ${NXT_AUTO}getrandom
 . ${NXT_AUTO}pcre
 . ${NXT_AUTO}editline
+. ${NXT_AUTO}expect
diff -r 94c42736a730 -r d45f6f0079ab nxt/auto/expect
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/nxt/auto/expect	Wed Oct 04 18:58:10 2017 +0300
@@ -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


More information about the nginx-devel mailing list