From d3fcb176bf21fceda968ad4c2142bdc3bfdb94da Mon Sep 17 00:00:00 2001 From: "Yukihiro \"Matz\" Matsumoto" Date: Sat, 13 Dec 2025 22:31:51 +0900 Subject: [PATCH] mruby-bin-mirb: restore tab completion for custom editor - restore mirb_completion.c/h from before readline removal - add editor adapter for tab completion (mirb_setup_editor_completion, mirb_get_completions, mirb_free_completions) - add TAB key handling in mirb_editor.c - fix string literal completion: properly detect when cursor is outside a string by scanning forward, allow string/array/hash literals as safe receivers for method completion Co-authored-by: Claude --- mrbgems/mruby-bin-mirb/tools/mirb/mirb.c | 23 + .../tools/mirb/mirb_completion.c | 776 ++++++++++++++++++ .../tools/mirb/mirb_completion.h | 132 +++ .../mruby-bin-mirb/tools/mirb/mirb_editor.c | 140 +++- .../mruby-bin-mirb/tools/mirb/mirb_editor.h | 26 + mrbgems/mruby-bin-mirb/tools/mirb/mirb_term.h | 1 + 6 files changed, 1086 insertions(+), 12 deletions(-) create mode 100644 mrbgems/mruby-bin-mirb/tools/mirb/mirb_completion.c create mode 100644 mrbgems/mruby-bin-mirb/tools/mirb/mirb_completion.h diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb.c b/mrbgems/mruby-bin-mirb/tools/mirb/mirb.c index 2360495c7..8f475122f 100644 --- a/mrbgems/mruby-bin-mirb/tools/mirb/mirb.c +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb.c @@ -36,6 +36,7 @@ #endif #include "mirb_editor.h" +#include "mirb_completion.h" /* obsolete configuration */ #ifdef DISABLE_MIRB_UNDERSCORE @@ -425,6 +426,24 @@ mirb_check_code_complete(const char *code, void *user_data) return complete; } +/* Tab completion callback for editor */ +static int +mirb_tab_complete(const char *line, int cursor_pos, + char ***completions_out, int *prefix_len_out, + void *user_data) +{ + (void)user_data; + return mirb_get_completions(line, cursor_pos, completions_out, prefix_len_out); +} + +/* Free tab completions */ +static void +mirb_tab_complete_free(char **completions, int count, void *user_data) +{ + (void)user_data; + mirb_free_completions(completions, count); +} + static void ctrl_c_handler(int signo) { @@ -534,6 +553,9 @@ main(int argc, char **argv) check_data.mrb = mrb; check_data.cxt = cxt; mirb_editor_set_check_complete(&editor, mirb_check_code_complete, &check_data); + /* Setup tab completion */ + mirb_setup_editor_completion(mrb, cxt); + mirb_editor_set_tab_complete(&editor, mirb_tab_complete, mirb_tab_complete_free, NULL); /* Enable colored prompts if terminal supports it */ if (isatty(fileno(stdout))) { const char *term = getenv("TERM"); @@ -773,6 +795,7 @@ main(int argc, char **argv) /* Cleanup editor */ if (use_editor) { + mirb_cleanup_editor_completion(); mirb_editor_cleanup(&editor); } 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..58e0358a3 --- /dev/null +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_completion.c @@ -0,0 +1,776 @@ +/* +** 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; + int in_string = 0; /* 0 = not in string, '"' or '\'' = in that string type */ + + /* First pass: determine if we're inside a string by scanning from start */ + for (i = 0; i < cursor_pos; i++) { + if (in_string) { + if (line[i] == '\\' && i + 1 < cursor_pos) { + i++; /* Skip escaped character */ + } + else if (line[i] == in_string) { + in_string = 0; /* End of string */ + } + } + else { + if (line[i] == '"' || line[i] == '\'') { + in_string = line[i]; /* Start of string */ + } + } + } + + /* If we're inside a string, check for file completion context */ + if (in_string) { + if (mirb_in_file_context(line, cursor_pos)) { + return COMPLETION_FILE; + } + return COMPLETION_KEYWORD; /* No completion inside strings */ + } + + /* 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] == '\'') { + /* This is a closing quote (we know we're not in a string) */ + /* Continue scanning to find if there's a dot before the string */ + continue; + } + 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 + * ============================================================ */ + +/* Check if receiver expression is simple (just a name, no method calls) */ +static mrb_bool +is_simple_receiver(const char *expr) +{ + int i; + int in_string = 0; + + /* Empty is not simple */ + if (!expr || expr[0] == '\0') return FALSE; + + /* Check if it's a safe expression to evaluate */ + for (i = 0; expr[i]; i++) { + char c = expr[i]; + + if (in_string) { + /* Inside string - allow anything except check for end */ + if (c == '\\' && expr[i+1]) { + i++; /* Skip escaped character */ + } + else if (c == in_string) { + in_string = 0; /* End of string */ + } + } + else { + /* Outside string */ + if (c == '"' || c == '\'') { + in_string = c; /* Start of string */ + } + else if (c == '(' || c == ')') { + /* Disallow method calls - could have side effects */ + return FALSE; + } + else if (!(ISALNUM(c) || c == '_' || c == ':' || c == '[' || c == ']' || + c == '{' || c == '}' || c == ',' || c == ' ' || c == '\t' || + c == '-' || c == '+' || c == '.' || c == '@')) { + /* Disallow unknown characters */ + return FALSE; + } + } + } + + /* Unclosed string is not valid */ + if (in_string) return FALSE; + + return TRUE; +} + +mrb_value +mirb_eval_receiver(mrb_state *mrb, const char *receiver_expr, mrb_ccontext *cxt) +{ + struct mrb_parser_state *parser; + struct RProc *proc; + mrb_value result; + int ai = mrb_gc_arena_save(mrb); + + /* Parse the receiver expression WITH compiler context to access local variables */ + parser = mrb_parse_string(mrb, receiver_expr, cxt); + 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) { + /* Only evaluate simple receivers to avoid corrupting VM state. + * Complex expressions like "obj.method()" are skipped for now. + * This prevents local variables from being cleared during tab completion. */ + if (is_simple_receiver(receiver_expr)) { + receiver = mirb_eval_receiver(ctx->mrb, receiver_expr, ctx->cxt); + 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 + +/* ============================================================ + * Custom Editor Adapter + * ============================================================ */ + +static mirb_completion_ctx *g_editor_ctx = NULL; + +void +mirb_setup_editor_completion(mrb_state *mrb, mrb_ccontext *cxt) +{ + /* Initialize global context */ + g_editor_ctx = (mirb_completion_ctx*)malloc(sizeof(mirb_completion_ctx)); + if (!g_editor_ctx) return; + + mirb_completion_init(g_editor_ctx, mrb, cxt); +} + +void +mirb_cleanup_editor_completion(void) +{ + if (g_editor_ctx) { + mirb_completion_free(g_editor_ctx); + free(g_editor_ctx); + g_editor_ctx = NULL; + } +} + +int +mirb_get_completions(const char *line, int cursor_pos, + char ***completions_out, int *prefix_len_out) +{ + int i; + + if (!g_editor_ctx) { + *completions_out = NULL; + *prefix_len_out = 0; + return 0; + } + + /* Clear previous completions */ + mirb_completion_free(g_editor_ctx); + + /* Generate completions */ + mirb_generate_completions(g_editor_ctx, line, cursor_pos); + + /* Return results */ + *prefix_len_out = g_editor_ctx->prefix_len; + + if (g_editor_ctx->completion_count == 0) { + *completions_out = NULL; + return 0; + } + + /* Copy completions (caller will free) */ + *completions_out = (char**)malloc(g_editor_ctx->completion_count * sizeof(char*)); + if (!*completions_out) return 0; + + for (i = 0; i < g_editor_ctx->completion_count; i++) { + (*completions_out)[i] = strdup(g_editor_ctx->completions[i]); + } + + return g_editor_ctx->completion_count; +} + +void +mirb_free_completions(char **completions, int count) +{ + int i; + if (completions) { + for (i = 0; i < count; i++) { + free(completions[i]); + } + free(completions); + } +} 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..81778bf1d --- /dev/null +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_completion.h @@ -0,0 +1,132 @@ +/* +** 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, mrb_ccontext *cxt);; + +/* 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 + +/* Custom editor adapter */ +void mirb_setup_editor_completion(mrb_state *mrb, mrb_ccontext *cxt); +void mirb_cleanup_editor_completion(void); + +/* Get completions for custom editor - returns number of completions */ +int mirb_get_completions(const char *line, int cursor_pos, + char ***completions_out, int *prefix_len_out); + +/* Free completions returned by mirb_get_completions */ +void mirb_free_completions(char **completions, int count); + +#endif /* MIRB_COMPLETION_H */ diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_editor.c b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_editor.c index ca13ba5c3..bb3207796 100644 --- a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_editor.c +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_editor.c @@ -293,6 +293,103 @@ mirb_editor_set_check_complete(mirb_editor *ed, mirb_check_complete_fn *fn, void ed->check_complete_data = user_data; } +/* + * Set tab completion callbacks + */ +void +mirb_editor_set_tab_complete(mirb_editor *ed, + mirb_tab_complete_fn *complete_fn, + mirb_tab_complete_free_fn *free_fn, + void *user_data) +{ + ed->tab_complete = complete_fn; + ed->tab_complete_free = free_fn; + ed->tab_complete_data = user_data; +} + +/* + * Handle tab completion + * Returns TRUE if completion was performed + */ +static mrb_bool +handle_tab_completion(mirb_editor *ed) +{ + char **completions = NULL; + int count, prefix_len; + const char *current_line; + int cursor_col; + + if (!ed->tab_complete) return FALSE; + + /* Get current line and cursor position */ + current_line = ed->buf.lines[ed->buf.cursor_line].data; + cursor_col = (int)ed->buf.cursor_col; + + /* Get completions */ + count = ed->tab_complete(current_line, cursor_col, &completions, &prefix_len, + ed->tab_complete_data); + + if (count == 0 || !completions) { + return FALSE; + } + + if (count == 1) { + /* Single completion - insert it */ + const char *completion = completions[0]; + int i; + + /* Delete the prefix we're replacing */ + for (i = 0; i < prefix_len; i++) { + mirb_buffer_delete_back(&ed->buf); + } + + /* Insert completion */ + mirb_buffer_insert_string(&ed->buf, completion, strlen(completion)); + } + else { + /* Multiple completions - find common prefix and show options */ + int common_len = (int)strlen(completions[0]); + int i, j; + + /* Find longest common prefix */ + for (i = 1; i < count; i++) { + for (j = 0; j < common_len && completions[i][j]; j++) { + if (completions[0][j] != completions[i][j]) { + common_len = j; + break; + } + } + if (j < common_len) common_len = j; + } + + if (common_len > prefix_len) { + /* Extend with common prefix */ + for (i = 0; i < prefix_len; i++) { + mirb_buffer_delete_back(&ed->buf); + } + mirb_buffer_insert_string(&ed->buf, completions[0], common_len); + } + else { + /* Show all completions */ + printf("\r\n"); + for (i = 0; i < count; i++) { + printf("%s ", completions[i]); + if ((i + 1) % 4 == 0 && i + 1 < count) printf("\r\n"); + } + printf("\r\n"); + /* Force full redraw */ + ed->prev_line_count = 0; + } + } + + /* Free completions */ + if (ed->tab_complete_free) { + ed->tab_complete_free(completions, count, ed->tab_complete_data); + } + + return TRUE; +} + /* * Enable/disable color */ @@ -432,6 +529,7 @@ handle_key(mirb_editor *ed, int key, mirb_edit_result *result) * If splitting in the middle and there was a trailing blank line, remove it */ mrb_bool inserting_in_middle = (ed->buf.cursor_line < ed->buf.line_count - 1); + mrb_bool move_to_blank = FALSE; if (inserting_in_middle) { /* Only consider smart navigation if cursor is at end of current line */ @@ -453,16 +551,14 @@ handle_key(mirb_editor *ed, int key, mirb_edit_result *result) if (ed->buf.cursor_col == current_line->len && next_is_last && next_is_blank) { /* Cursor at end of line, next is blank last line: just move to it */ - mirb_buffer_cursor_down(&ed->buf); - mirb_buffer_cursor_end(&ed->buf); - return TRUE; + move_to_blank = TRUE; } - - if (ed->buf.cursor_col < current_line->len && next_is_last && next_is_blank) { + else if (ed->buf.cursor_col < current_line->len && next_is_last && next_is_blank) { /* Cursor in middle of line, next is blank last line: remove it before split */ mirb_buffer_delete_line(&ed->buf, next_line_idx); + inserting_in_middle = FALSE; /* No longer inserting in middle after deletion */ } - /* Fall through to insert new line (split at cursor) */ + /* Fall through to check completion */ } /* Check if input is complete */ @@ -478,7 +574,7 @@ handle_key(mirb_editor *ed, int key, mirb_edit_result *result) * being affected by 'end' keywords on later lines. */ int indent; - if (inserting_in_middle) { + if (inserting_in_middle && !move_to_blank) { char *partial = buffer_to_string_upto_line(&ed->buf, ed->buf.cursor_line); indent = partial ? calc_indent_level(partial) : 0; free(partial); @@ -487,11 +583,26 @@ handle_key(mirb_editor *ed, int key, mirb_edit_result *result) indent = calc_indent_level(code); } free(code); - /* Add newline and continue editing */ - mirb_buffer_newline(&ed->buf); - /* Insert indentation spaces (2 spaces per level) */ - for (int i = 0; i < indent * 2; i++) { - mirb_buffer_insert_char(&ed->buf, ' '); + + if (move_to_blank) { + /* Move to existing blank line and set proper indentation */ + mirb_buffer_cursor_down(&ed->buf); + /* Clear existing whitespace and set correct indent */ + mirb_line *line = &ed->buf.lines[ed->buf.cursor_line]; + line->len = 0; + line->data[0] = '\0'; + ed->buf.cursor_col = 0; + for (int i = 0; i < indent * 2; i++) { + mirb_buffer_insert_char(&ed->buf, ' '); + } + } + else { + /* Add newline and continue editing */ + mirb_buffer_newline(&ed->buf); + /* Insert indentation spaces (2 spaces per level) */ + for (int i = 0; i < indent * 2; i++) { + mirb_buffer_insert_char(&ed->buf, ' '); + } } return TRUE; } @@ -615,6 +726,11 @@ handle_key(mirb_editor *ed, int key, mirb_edit_result *result) ed->prev_line_count = 0; return TRUE; + case MIRB_KEY_TAB: + /* Tab completion */ + handle_tab_completion(ed); + return TRUE; + default: /* Insert printable characters */ if (key >= 32 && key < 127) { diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_editor.h b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_editor.h index 40327111e..255bc8571 100644 --- a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_editor.h +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_editor.h @@ -29,6 +29,20 @@ typedef enum mirb_edit_result { */ typedef mrb_bool mirb_check_complete_fn(const char *code, void *user_data); +/* + * Callback for tab completion + * Returns number of completions, sets completions_out and prefix_len_out + * Caller must free completions using mirb_tab_complete_free_fn + */ +typedef int mirb_tab_complete_fn(const char *line, int cursor_pos, + char ***completions_out, int *prefix_len_out, + void *user_data); + +/* + * Callback to free tab completions + */ +typedef void mirb_tab_complete_free_fn(char **completions, int count, void *user_data); + /* * Editor state */ @@ -49,6 +63,10 @@ typedef struct mirb_editor { mirb_check_complete_fn *check_complete; /* completion checker */ void *check_complete_data; /* user data for checker */ + mirb_tab_complete_fn *tab_complete; /* tab completion callback */ + mirb_tab_complete_free_fn *tab_complete_free; /* free completions callback */ + void *tab_complete_data; /* user data for tab completion */ + size_t display_cursor_row; /* cursor row in buffer (for refresh tracking) */ size_t prev_line_count; /* line count from last refresh */ @@ -91,6 +109,14 @@ void mirb_editor_set_check_complete(mirb_editor *ed, mirb_check_complete_fn *fn, void *user_data); +/* + * Set tab completion callbacks + */ +void mirb_editor_set_tab_complete(mirb_editor *ed, + mirb_tab_complete_fn *complete_fn, + mirb_tab_complete_free_fn *free_fn, + void *user_data); + /* * Enable or disable colored output */ diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_term.h b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_term.h index edea88088..882fce83d 100644 --- a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_term.h +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_term.h @@ -23,6 +23,7 @@ enum mirb_key { MIRB_KEY_CTRL_D = 4, MIRB_KEY_CTRL_E = 5, MIRB_KEY_CTRL_F = 6, + MIRB_KEY_TAB = 9, MIRB_KEY_CTRL_K = 11, MIRB_KEY_CTRL_L = 12, MIRB_KEY_ENTER = 13,