diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb.c b/mrbgems/mruby-bin-mirb/tools/mirb/mirb.c index a66c105f9..e8cc70ea5 100644 --- a/mrbgems/mruby-bin-mirb/tools/mirb/mirb.c +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb.c @@ -29,6 +29,8 @@ #include #include +#include "mirb_completion.h" + /* obsolete configuration */ #ifdef ENABLE_READLINE # define MRB_USE_READLINE @@ -594,6 +596,15 @@ main(int argc, char **argv) mrb_ccontext_filename(mrb, cxt, "(mirb)"); if (args.verbose) cxt->dump_result = TRUE; + /* Setup tab completion */ +#ifdef MRB_USE_READLINE +#ifndef MRB_USE_LINENOISE + mirb_setup_readline_completion(mrb, cxt); +#else + mirb_setup_linenoise_completion(mrb, cxt); +#endif +#endif + ai = mrb_gc_arena_save(mrb); while (TRUE) { @@ -807,6 +818,16 @@ main(int argc, char **argv) mrb_free(mrb, args.libv); } mrb_ccontext_free(mrb, cxt); + + /* Cleanup tab completion */ +#ifdef MRB_USE_READLINE +#ifndef MRB_USE_LINENOISE + mirb_cleanup_readline_completion(); +#else + mirb_cleanup_linenoise_completion(); +#endif +#endif + mrb_close(mrb); return 0; diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_completion.c b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_completion.c new file mode 100644 index 000000000..3276c172e --- /dev/null +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_completion.c @@ -0,0 +1,625 @@ +/* +** mirb_completion.c - Tab completion support for mirb +** +** See Copyright Notice in mruby.h +*/ + +#include "mirb_completion.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#ifdef MRB_USE_READLINE +#ifndef MRB_USE_LINENOISE +#include MRB_READLINE_HEADER +#endif +#endif + +#ifdef MRB_USE_LINENOISE +#include +#endif + +/* Helper macros */ +#ifndef ISSPACE +#define ISSPACE(c) isspace((unsigned char)(c)) +#endif +#ifndef ISALNUM +#define ISALNUM(c) isalnum((unsigned char)(c)) +#endif + +/* Ruby keywords */ +static const char *ruby_keywords[] = { + "BEGIN", "END", "__ENCODING__", "__FILE__", "__LINE__", + "alias", "and", "begin", "break", "case", "class", "def", + "defined?", "do", "else", "elsif", "end", "ensure", "false", + "for", "if", "in", "module", "next", "nil", "not", "or", + "redo", "rescue", "retry", "return", "self", "super", "then", + "true", "undef", "unless", "until", "when", "while", "yield", + NULL +}; + +/* ============================================================ + * Core Completion Engine + * ============================================================ */ + +void +mirb_completion_init(mirb_completion_ctx *ctx, mrb_state *mrb, mrb_ccontext *cxt) +{ + memset(ctx, 0, sizeof(*ctx)); + ctx->mrb = mrb; + ctx->cxt = cxt; +} + +void +mirb_completion_free(mirb_completion_ctx *ctx) +{ + int i; + + /* Free match prefix */ + if (ctx->match_prefix) { + free(ctx->match_prefix); + ctx->match_prefix = NULL; + } + + /* Free completions */ + if (ctx->completions) { + for (i = 0; i < ctx->completion_count; i++) { + free(ctx->completions[i]); + } + free(ctx->completions); + ctx->completions = NULL; + } + + ctx->completion_count = 0; + ctx->completion_alloc = 0; + ctx->current_index = 0; +} + +/* ============================================================ + * Context Analysis + * ============================================================ */ + +mirb_completion_type +mirb_detect_completion_type(const char *line, int cursor_pos) +{ + int i; + + /* Scan backwards from cursor to find context */ + for (i = cursor_pos - 1; i >= 0; i--) { + if (line[i] == '.') { + /* After dot = method completion */ + return COMPLETION_METHOD; + } + if (line[i] == '$') { + /* Global variable */ + return COMPLETION_GLOBAL_VAR; + } + if (line[i] == '"' || line[i] == '\'') { + /* Check if in require/load context */ + if (mirb_in_file_context(line, i)) { + return COMPLETION_FILE; + } + break; /* In string, no completion */ + } + if (ISSPACE(line[i]) || line[i] == '(' || line[i] == ',' || + line[i] == '[' || line[i] == '{' || line[i] == ';') { + /* Start of new expression */ + break; + } + } + + /* Default: complete everything at top level */ + return COMPLETION_KEYWORD; /* Includes keywords, locals, constants */ +} + +mrb_bool +mirb_in_file_context(const char *line, int quote_pos) +{ + int i; + + /* Look backwards for require or load keyword */ + for (i = quote_pos - 1; i >= 0; i--) { + if (ISSPACE(line[i])) continue; + + /* Check for 'require' or 'load' */ + if (i >= 6 && strncmp(&line[i-6], "require", 7) == 0) return TRUE; + if (i >= 3 && strncmp(&line[i-3], "load", 4) == 0) return TRUE; + + break; + } + return FALSE; +} + +/* Extract receiver expression before the dot */ +char * +mirb_extract_receiver(const char *line, int cursor_pos, int *recv_end) +{ + int depth = 0; /* Parentheses/bracket depth */ + int i, start = -1; + char *receiver; + int len; + + /* Find the dot before cursor */ + for (i = cursor_pos - 1; i >= 0; i--) { + if (line[i] == '.' && depth == 0) { + *recv_end = i; + break; + } + /* Track nesting depth for complex expressions */ + if (line[i] == ')' || line[i] == ']' || line[i] == '}') depth++; + if (line[i] == '(' || line[i] == '[' || line[i] == '{') depth--; + } + + if (i < 0) return NULL; /* No dot found */ + + /* Now find start of receiver expression */ + depth = 0; + for (start = i - 1; start >= 0; start--) { + char c = line[start]; + + if (c == ')' || c == ']' || c == '}') depth++; + if (c == '(' || c == '[' || c == '{') depth--; + + if (depth < 0) { + start++; + break; + } + + /* Break on operators/keywords at depth 0 */ + if (depth == 0 && (ISSPACE(c) || c == '=' || c == ',' || c == ';')) { + start++; + break; + } + } + + if (start < 0) start = 0; + + /* Allocate and copy receiver */ + len = i - start; + receiver = (char*)malloc(len + 1); + if (!receiver) return NULL; + + memcpy(receiver, line + start, len); + receiver[len] = '\0'; + + return receiver; +} + +/* ============================================================ + * Receiver Evaluation + * ============================================================ */ + +mrb_value +mirb_eval_receiver(mrb_state *mrb, const char *receiver_expr) +{ + struct mrb_parser_state *parser; + struct RProc *proc; + mrb_value result; + int ai = mrb_gc_arena_save(mrb); + + /* Parse the receiver expression */ + parser = mrb_parse_string(mrb, receiver_expr, NULL); + if (!parser || parser->nerr > 0) { + if (parser) mrb_parser_free(parser); + return mrb_nil_value(); + } + + /* Generate and execute */ + proc = mrb_generate_code(mrb, parser); + mrb_parser_free(parser); + + if (!proc) { + return mrb_nil_value(); + } + + result = mrb_vm_run(mrb, proc, mrb_top_self(mrb), 0); + + /* Clear exception if any */ + if (mrb->exc) { + mrb->exc = NULL; + result = mrb_nil_value(); + } + + mrb_gc_arena_restore(mrb, ai); + return result; +} + +/* ============================================================ + * Method Completion + * ============================================================ */ + +/* Callback for mrb_mt_foreach */ +struct method_collector { + mirb_completion_ctx *ctx; + int count; +}; + +static int +collect_method_callback(mrb_state *mrb, mrb_sym sym, mrb_method_t method, void *data) +{ + struct method_collector *mc = (struct method_collector*)data; + const char *name = mrb_sym_name(mrb, sym); + + (void)method; /* Unused */ + + /* Skip internal methods (start with __) */ + if (name[0] == '_' && name[1] == '_') { + return 0; /* Continue iteration */ + } + + /* Add if matches prefix */ + mirb_add_completion(mc->ctx, name); + mc->count++; + + return 0; /* Continue */ +} + +void +mirb_complete_methods(mirb_completion_ctx *ctx, mrb_value receiver) +{ + struct RClass *klass = mrb_class(ctx->mrb, receiver); + struct method_collector mc = { ctx, 0 }; + + /* Walk up class hierarchy */ + while (klass) { + mrb_mt_foreach(ctx->mrb, klass, collect_method_callback, &mc); + klass = klass->super; + } +} + +/* ============================================================ + * Keyword and Variable Completion + * ============================================================ */ + +void +mirb_complete_keywords(mirb_completion_ctx *ctx) +{ + int i; + for (i = 0; ruby_keywords[i] != NULL; i++) { + mirb_add_completion(ctx, ruby_keywords[i]); + } +} + +void +mirb_complete_local_vars(mirb_completion_ctx *ctx) +{ + int i; + + /* Local variables from compiler context */ + if (ctx->cxt && ctx->cxt->syms) { + for (i = 0; i < (int)ctx->cxt->slen; i++) { + const char *name = mrb_sym_name(ctx->mrb, ctx->cxt->syms[i]); + if (name && name[0] != '_') { /* Skip underscore-only */ + mirb_add_completion(ctx, name); + } + } + } +} + +void +mirb_complete_global_vars(mirb_completion_ctx *ctx) +{ + mrb_value gvars; + mrb_int len, i; + int ai = mrb_gc_arena_save(ctx->mrb); + + /* Use Ruby to get global variables */ + gvars = mrb_funcall_argv(ctx->mrb, mrb_obj_value(ctx->mrb->kernel_module), + mrb_intern_lit(ctx->mrb, "global_variables"), + 0, NULL); + + if (ctx->mrb->exc) { + ctx->mrb->exc = NULL; + mrb_gc_arena_restore(ctx->mrb, ai); + return; + } + + if (mrb_array_p(gvars)) { + len = RARRAY_LEN(gvars); + + for (i = 0; i < len; i++) { + mrb_value sym = mrb_ary_entry(gvars, i); + mrb_sym s = mrb_symbol(sym); + const char *name = mrb_sym_name(ctx->mrb, s); + if (name) { + mirb_add_completion(ctx, name); + } + } + } + + mrb_gc_arena_restore(ctx->mrb, ai); +} + +void +mirb_complete_constants(mirb_completion_ctx *ctx, struct RClass *scope) +{ + mrb_value consts; + mrb_int len, i; + int ai = mrb_gc_arena_save(ctx->mrb); + + /* Use Ruby to get constants */ + consts = mrb_funcall_argv(ctx->mrb, + mrb_obj_value(scope ? scope : ctx->mrb->object_class), + mrb_intern_lit(ctx->mrb, "constants"), + 0, NULL); + + if (ctx->mrb->exc) { + ctx->mrb->exc = NULL; + mrb_gc_arena_restore(ctx->mrb, ai); + return; + } + + if (mrb_array_p(consts)) { + len = RARRAY_LEN(consts); + + for (i = 0; i < len; i++) { + mrb_value sym = mrb_ary_entry(consts, i); + mrb_sym s = mrb_symbol(sym); + const char *name = mrb_sym_name(ctx->mrb, s); + if (name) { + mirb_add_completion(ctx, name); + } + } + } + + mrb_gc_arena_restore(ctx->mrb, ai); +} + +void +mirb_complete_files(mirb_completion_ctx *ctx, const char *partial_path) +{ + /* File completion implementation would go here */ + /* For now, just a stub */ + (void)ctx; + (void)partial_path; +} + +/* ============================================================ + * Completion Management + * ============================================================ */ + +void +mirb_add_completion(mirb_completion_ctx *ctx, const char *text) +{ + char **new_completions; + int new_alloc; + + /* Check if matches prefix */ + if (ctx->prefix_len > 0) { + if (strncmp(text, ctx->match_prefix, ctx->prefix_len) != 0) { + return; /* Doesn't match */ + } + } + + /* Grow array if needed */ + if (ctx->completion_count >= ctx->completion_alloc) { + new_alloc = ctx->completion_alloc == 0 ? 16 : ctx->completion_alloc * 2; + new_completions = (char**)realloc(ctx->completions, + new_alloc * sizeof(char*)); + if (!new_completions) return; /* Out of memory */ + + ctx->completions = new_completions; + ctx->completion_alloc = new_alloc; + } + + /* Add completion */ + ctx->completions[ctx->completion_count] = strdup(text); + if (ctx->completions[ctx->completion_count]) { + ctx->completion_count++; + } +} + +void +mirb_generate_completions(mirb_completion_ctx *ctx, const char *line, int cursor_pos) +{ + mirb_completion_type type; + int i, recv_end; + char *receiver_expr; + mrb_value receiver; + + /* Store context */ + ctx->line_buf = line; + ctx->cursor_pos = cursor_pos; + + /* Extract prefix to match */ + for (i = cursor_pos - 1; i >= 0; i--) { + char c = line[i]; + if (!ISALNUM(c) && c != '_' && c != '?' && c != '!' && c != '$' && c != '@') { + break; + } + } + i++; /* Move to start of identifier */ + + if (ctx->match_prefix) { + free(ctx->match_prefix); + } + ctx->match_prefix = strndup(line + i, cursor_pos - i); + ctx->prefix_len = cursor_pos - i; + + /* Detect completion type */ + type = mirb_detect_completion_type(line, cursor_pos); + + /* Generate completions based on type */ + switch (type) { + case COMPLETION_METHOD: + receiver_expr = mirb_extract_receiver(line, cursor_pos, &recv_end); + if (receiver_expr) { + receiver = mirb_eval_receiver(ctx->mrb, receiver_expr); + if (!mrb_nil_p(receiver)) { + mirb_complete_methods(ctx, receiver); + } + free(receiver_expr); + } + break; + + case COMPLETION_GLOBAL_VAR: + mirb_complete_global_vars(ctx); + break; + + case COMPLETION_FILE: + mirb_complete_files(ctx, ctx->match_prefix); + break; + + case COMPLETION_LOCAL_VAR: + case COMPLETION_CONSTANT: + case COMPLETION_KEYWORD: + default: + /* Complete everything */ + mirb_complete_keywords(ctx); + mirb_complete_local_vars(ctx); + mirb_complete_constants(ctx, NULL); + break; + } +} + +/* ============================================================ + * Readline/Libedit Adapter + * ============================================================ */ + +#ifdef MRB_USE_READLINE +#ifndef MRB_USE_LINENOISE + +static mirb_completion_ctx *g_readline_ctx = NULL; + +static char * +mirb_readline_generator(const char *text, int state) +{ + (void)text; /* text is already in match_prefix */ + + /* state == 0: first call, generate completions */ + if (state == 0) { + mirb_completion_free(g_readline_ctx); + + /* Generate completions based on full line */ + mirb_generate_completions(g_readline_ctx, rl_line_buffer, rl_point); + + g_readline_ctx->current_index = 0; + } + + /* Return next completion or NULL when done */ + if (g_readline_ctx->current_index < g_readline_ctx->completion_count) { + char *completion = g_readline_ctx->completions[g_readline_ctx->current_index]; + g_readline_ctx->current_index++; + + /* readline will free this, so duplicate */ + return strdup(completion); + } + + return NULL; +} + +static char ** +mirb_readline_completion(const char *text, int start, int end) +{ + (void)start; + (void)end; + + /* Prevent default filename completion */ + rl_attempted_completion_over = 1; + + /* Use our generator */ + return rl_completion_matches(text, mirb_readline_generator); +} + +void +mirb_setup_readline_completion(mrb_state *mrb, mrb_ccontext *cxt) +{ + /* Initialize global context */ + g_readline_ctx = (mirb_completion_ctx*)malloc(sizeof(mirb_completion_ctx)); + if (!g_readline_ctx) return; + + mirb_completion_init(g_readline_ctx, mrb, cxt); + + /* Set completion function */ + rl_attempted_completion_function = mirb_readline_completion; + + /* Configure readline behavior - include . so "obj.method" are separate words */ + rl_basic_word_break_characters = " \t\n\"\\'`@$><=;|&{(."; + rl_completer_word_break_characters = " \t\n\"\\'`@$><=;|&{(."; +} + +void +mirb_cleanup_readline_completion(void) +{ + if (g_readline_ctx) { + mirb_completion_free(g_readline_ctx); + free(g_readline_ctx); + g_readline_ctx = NULL; + } +} + +#endif +#endif + +/* ============================================================ + * Linenoise Adapter + * ============================================================ */ + +#ifdef MRB_USE_LINENOISE + +static mirb_completion_ctx *g_linenoise_ctx = NULL; + +static void +mirb_linenoise_completion(const char *buf, linenoiseCompletions *lc) +{ + int cursor_pos = (int)strlen(buf); /* linenoise completes at end */ + int i, prefix_start; + char completion_line[1024]; + + /* Clear previous completions */ + mirb_completion_free(g_linenoise_ctx); + + /* Generate completions */ + mirb_generate_completions(g_linenoise_ctx, buf, cursor_pos); + + /* Add each completion to linenoise */ + for (i = 0; i < g_linenoise_ctx->completion_count; i++) { + /* Need to build full line with completion */ + prefix_start = cursor_pos - g_linenoise_ctx->prefix_len; + + /* Copy line up to prefix */ + if (prefix_start > 0) { + memcpy(completion_line, buf, prefix_start); + } + + /* Add completion */ + strcpy(completion_line + prefix_start, g_linenoise_ctx->completions[i]); + + linenoiseAddCompletion(lc, completion_line); + } +} + +void +mirb_setup_linenoise_completion(mrb_state *mrb, mrb_ccontext *cxt) +{ + /* Initialize global context */ + g_linenoise_ctx = (mirb_completion_ctx*)malloc(sizeof(mirb_completion_ctx)); + if (!g_linenoise_ctx) return; + + mirb_completion_init(g_linenoise_ctx, mrb, cxt); + + /* Set completion callback */ + linenoiseSetCompletionCallback(mirb_linenoise_completion); +} + +void +mirb_cleanup_linenoise_completion(void) +{ + if (g_linenoise_ctx) { + mirb_completion_free(g_linenoise_ctx); + free(g_linenoise_ctx); + g_linenoise_ctx = NULL; + } +} + +#endif diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_completion.h b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_completion.h new file mode 100644 index 000000000..4649e91d2 --- /dev/null +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_completion.h @@ -0,0 +1,121 @@ +/* +** mirb_completion.h - Tab completion support for mirb +** +** See Copyright Notice in mruby.h +*/ + +#ifndef MIRB_COMPLETION_H +#define MIRB_COMPLETION_H + +#include +#include + +/** + * @file mirb_completion.h + * + * Tab completion support for mirb. + * + * Architecture: + * - Core engine is library-agnostic + * - Adapters for readline/libedit and linenoise + * - Context detection based on input line analysis + * - Safe evaluation of receiver expressions + * + * Completion Types: + * - COMPLETION_METHOD: After dot operator + * - COMPLETION_KEYWORD: Ruby keywords + * - COMPLETION_LOCAL_VAR: Variables in scope + * - COMPLETION_GLOBAL_VAR: $variables + * - COMPLETION_CONSTANT: Constants and classes + * - COMPLETION_FILE: File paths (optional) + * + * Performance: + * - Completions generated on-demand + * - Results cached per tab press + * - Safe evaluation with exception handling + */ + +/* Completion types */ +typedef enum { + COMPLETION_METHOD, /* Object methods */ + COMPLETION_KEYWORD, /* Ruby keywords */ + COMPLETION_GLOBAL_VAR, /* $global */ + COMPLETION_LOCAL_VAR, /* local_var */ + COMPLETION_CONSTANT, /* CONSTANT or Class */ + COMPLETION_FILE, /* File paths */ +} mirb_completion_type; + +/* Completion context - shared state */ +typedef struct mirb_completion_ctx { + mrb_state *mrb; /* mruby VM state */ + mrb_ccontext *cxt; /* Compiler context for locals */ + const char *line_buf; /* Current input line */ + int cursor_pos; /* Cursor position in line */ + char *match_prefix; /* Text to match against */ + int prefix_len; /* Length of prefix */ + + /* Completion results */ + char **completions; /* Array of completion strings */ + int completion_count; /* Number of completions */ + int completion_alloc; /* Allocated size */ + int current_index; /* For generator pattern (readline) */ +} mirb_completion_ctx; + +/* Core Completion Engine Interface */ + +/* Initialize completion context */ +void mirb_completion_init(mirb_completion_ctx *ctx, mrb_state *mrb, + mrb_ccontext *cxt); + +/* Free completion context */ +void mirb_completion_free(mirb_completion_ctx *ctx); + +/* Analyze line and generate completions */ +void mirb_generate_completions(mirb_completion_ctx *ctx, + const char *line, int cursor_pos); + +/* Get completion type from context */ +mirb_completion_type mirb_detect_completion_type(const char *line, + int cursor_pos); + +/* Individual completion generators */ +void mirb_complete_methods(mirb_completion_ctx *ctx, mrb_value receiver); +void mirb_complete_keywords(mirb_completion_ctx *ctx); +void mirb_complete_local_vars(mirb_completion_ctx *ctx); +void mirb_complete_global_vars(mirb_completion_ctx *ctx); +void mirb_complete_constants(mirb_completion_ctx *ctx, struct RClass *scope); +void mirb_complete_files(mirb_completion_ctx *ctx, const char *partial_path); + +/* Helper functions */ + +/* Add completion if matches prefix */ +void mirb_add_completion(mirb_completion_ctx *ctx, const char *text); + +/* Extract receiver expression from line */ +char *mirb_extract_receiver(const char *line, int cursor_pos, int *recv_end); + +/* Evaluate receiver expression to get object */ +mrb_value mirb_eval_receiver(mrb_state *mrb, const char *receiver_expr); + +/* Check if in file completion context */ +mrb_bool mirb_in_file_context(const char *line, int quote_pos); + +/* Readline/Libedit adapter setup */ +#ifdef MRB_USE_READLINE +#ifndef MRB_USE_LINENOISE + +void mirb_setup_readline_completion(mrb_state *mrb, mrb_ccontext *cxt); +void mirb_cleanup_readline_completion(void); + +#endif +#endif + +/* Linenoise adapter setup */ +#ifdef MRB_USE_LINENOISE + +void mirb_setup_linenoise_completion(mrb_state *mrb, mrb_ccontext *cxt); +void mirb_cleanup_linenoise_completion(void); + +#endif + +#endif /* MIRB_COMPLETION_H */