Permalink
Browse files

cli: add minimal line completion to the CLI

  • Loading branch information...
1 parent 7832459 commit e43885e55922fa422f60c47d86555fc910e0e204 @saghul committed Jun 10, 2016
Showing with 188 additions and 1 deletion.
  1. +188 −1 src/cli/cli.c
View
@@ -8,9 +8,9 @@
#include "greet.h"
#include "sjs/sjs.h"
-
#define SJS_CLI_STDIN_BUF_SIZE 65536
+
typedef enum {
SJS_CLI_REPL = 0,
SJS_CLI_FILE,
@@ -32,6 +32,191 @@ typedef struct {
static cli_t cli;
+static int completion_idpart(unsigned char c) {
+ /* Very simplified "is identifier part" check. */
+ if ((c >= (unsigned char) 'a' && c <= (unsigned char) 'z') ||
+ (c >= (unsigned char) 'A' && c <= (unsigned char) 'Z') ||
+ (c >= (unsigned char) '0' && c <= (unsigned char) '9') ||
+ c == (unsigned char) '$' || c == (unsigned char) '_') {
+ return 1;
+ }
+ return 0;
+}
+
+
+static int completion_digit(unsigned char c) {
+ return (c >= (unsigned char) '0' && c <= (unsigned char) '9');
+}
+
+
+static duk_ret_t linenoise_completion_lookup(duk_context *ctx) {
+ duk_size_t len;
+ const char *orig;
+ const unsigned char *p;
+ const unsigned char *p_curr;
+ const unsigned char *p_end;
+ const char *key;
+ const char *prefix;
+ linenoiseCompletions *lc;
+ duk_idx_t idx_obj;
+
+ orig = duk_require_string(ctx, -3);
+ p_curr = (const unsigned char *) duk_require_lstring(ctx, -2, &len);
+ p_end = p_curr + len;
+ lc = duk_require_pointer(ctx, -1);
+
+ duk_push_global_object(ctx);
+ idx_obj = duk_require_top_index(ctx);
+
+ while (p_curr <= p_end) {
+ /* p_curr == p_end allowed on purpose, to handle 'Math.' for example. */
+ p = p_curr;
+ while (p < p_end && p[0] != (unsigned char) '.') {
+ p++;
+ }
+ /* 'p' points to a NUL (p == p_end) or a period. */
+ prefix = duk_push_lstring(ctx, (const char *) p_curr, (duk_size_t) (p - p_curr));
+
+#ifdef SJS_CLI_DEBUG
+ fprintf(stderr, "Completion check: '%s'\n", prefix);
+ fflush(stderr);
+#endif
+
+ if (p == p_end) {
+ /* 'idx_obj' points to the object matching the last
+ * full component, use [p_curr,p[ as a filter for
+ * that object.
+ */
+
+ duk_enum(ctx, idx_obj, DUK_ENUM_INCLUDE_NONENUMERABLE);
+ while (duk_next(ctx, -1, 0 /*get_value*/)) {
+ key = duk_get_string(ctx, -1);
+#ifdef SJS_CLI_DEBUG
+ fprintf(stderr, "Key: %s\n", key ? key : "");
+ fflush(stderr);
+#endif
+ if (!key) {
+ /* Should never happen, just in case. */
+ goto next;
+ }
+
+ /* Ignore array index keys: usually not desirable, and would
+ * also require ['0'] quoting.
+ */
+ if (completion_digit(key[0])) {
+ goto next;
+ }
+
+ /* XXX: There's no key quoting now, it would require replacing the
+ * last component with a ['foo\nbar'] style lookup when appropriate.
+ */
+
+ if (strlen(prefix) == 0) {
+ /* Partial ends in a period, e.g. 'Math.' -> complete all Math properties. */
+ duk_push_string(ctx, orig); /* original, e.g. 'Math.' */
+ duk_push_string(ctx, key);
+ duk_concat(ctx, 2);
+ linenoiseAddCompletion(lc, duk_require_string(ctx, -1));
+ duk_pop(ctx);
+ } else if (prefix && strcmp(key, prefix) == 0) {
+ /* Full completion, add a period, e.g. input 'Math' -> 'Math.'. */
+ duk_push_string(ctx, orig); /* original, including partial last component */
+ duk_push_string(ctx, ".");
+ duk_concat(ctx, 2);
+ linenoiseAddCompletion(lc, duk_require_string(ctx, -1));
+ duk_pop(ctx);
+ } else if (prefix && strncmp(key, prefix, strlen(prefix)) == 0) {
+ /* Last component is partial, complete. */
+ duk_push_string(ctx, orig); /* original, including partial last component */
+ duk_push_string(ctx, key + strlen(prefix)); /* completion to last component */
+ duk_concat(ctx, 2);
+ linenoiseAddCompletion(lc, duk_require_string(ctx, -1));
+ duk_pop(ctx);
+ }
+
+ next:
+ duk_pop(ctx);
+ }
+ return 0;
+ } else {
+ if (duk_get_prop(ctx, idx_obj)) {
+ duk_to_object(ctx, -1); /* for properties of plain strings etc */
+ duk_replace(ctx, idx_obj);
+ p_curr = p + 1;
+ } else {
+ /* Not found. */
+ return 0;
+ }
+ }
+ }
+
+ return 0;
+}
+
+
+static void linenoise_completion(const char *buf, linenoiseCompletions *lc) {
+ duk_context *ctx;
+ const unsigned char *p_start;
+ const unsigned char *p_end;
+ const unsigned char *p;
+
+ if (!buf) {
+ return;
+ }
+ ctx = sjs_vm_get_duk_ctx(cli.vm);
+ if (!ctx) {
+ return;
+ }
+
+ p_start = (const unsigned char *) buf;
+ p_end = (const unsigned char *) (buf + strlen(buf));
+ p = p_end;
+
+ /* Scan backwards for a maximal string which looks like a property
+ * chain (e.g. foo.bar.quux).
+ */
+
+ while (--p >= p_start) {
+ if (p[0] == (unsigned char) '.') {
+ if (p <= p_start) {
+ break;
+ }
+ if (!completion_idpart(p[-1])) {
+ /* Catches e.g. 'foo..bar' -> we want 'bar' only. */
+ break;
+ }
+ } else if (!completion_idpart(p[0])) {
+ break;
+ }
+ }
+ /* 'p' will either be p_start - 1 (ran out of buffer) or point to
+ * the first offending character.
+ */
+ p++;
+ if (p < p_start || p >= p_end) {
+ return; /* should never happen, but just in case */
+ }
+
+ /* 'p' now points to a string of the form 'foo.bar.quux'. Look up
+ * all the components except the last; treat the last component as
+ * a partial name which is used as a filter for the previous full
+ * component. All lookups are from the global object now.
+ */
+
+#ifdef SJS_CLI_DEBUG
+ fprintf(stderr, "Completion starting point: '%s'\n", p);
+ fflush(stderr);
+#endif
+
+ duk_push_string(ctx, (const char *) buf);
+ duk_push_lstring(ctx, (const char *) p, (duk_size_t) (p_end - p));
+ duk_push_pointer(ctx, (void *) lc);
+
+ (void) duk_safe_call(ctx, linenoise_completion_lookup, 3 /*nargs*/, 1 /*nrets*/);
+ duk_pop(ctx);
+}
+
+
static int handle_stdin(sjs_vm_t* vm) {
char *buf;
size_t bufsz;
@@ -106,6 +291,8 @@ static int handle_interactive(sjs_vm_t* vm) {
use_history = 0;
}
+ linenoiseSetCompletionCallback(linenoise_completion);
+
while((line = linenoise(prompt)) != NULL) {
/* reset sigint signal state */
cli.got_sigint = 0;

0 comments on commit e43885e

Please sign in to comment.