mruby-bin-mirb: add tab completion support

implements context-aware tab completion for mirb supporting all readline
variants (GNU readline, libedit, linenoise) with graceful degradation
when no readline library is available.

completion features:
- method names on objects (e.g., "hello".re<Tab> completes to reverse, replace)
- local variables from compiler context
- global variables via Ruby introspection
- constants and class names
- Ruby keywords

architecture:
- core completion engine is library-agnostic
- thin adapters for readline/libedit and linenoise
- context detection based on cursor position analysis
- safe receiver evaluation with exception handling
- proper word break characters so "String.new" works correctly

implementation adds:
- mirb_completion.h: interface definitions and data structures
- mirb_completion.c: complete implementation (~670 lines)
- mirb.c: integration with setup/cleanup calls

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Yukihiro "Matz" Matsumoto
2025-12-04 14:39:20 +09:00
parent 0aafb83374
commit 2f152823f3
3 changed files with 767 additions and 0 deletions
+21
View File
@@ -29,6 +29,8 @@
#include <signal.h>
#include <setjmp.h>
#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;
@@ -0,0 +1,625 @@
/*
** mirb_completion.c - Tab completion support for mirb
**
** See Copyright Notice in mruby.h
*/
#include "mirb_completion.h"
#include <mruby/array.h>
#include <mruby/class.h>
#include <mruby/compile.h>
#include <mruby/error.h>
#include <mruby/gc.h>
#include <mruby/proc.h>
#include <mruby/string.h>
#include <mruby/value.h>
#include <mruby/variable.h>
#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#ifdef MRB_USE_READLINE
#ifndef MRB_USE_LINENOISE
#include MRB_READLINE_HEADER
#endif
#endif
#ifdef MRB_USE_LINENOISE
#include <linenoise.h>
#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
@@ -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 <mruby.h>
#include <mruby/compile.h>
/**
* @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 */