Permalink
Browse files

cli: add minimal line completion to the CLI

  • Loading branch information...
saghul committed Jun 10, 2016
1 parent 7832459 commit e43885e55922fa422f60c47d86555fc910e0e204
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.