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 <noreply@anthropic.com>
This commit is contained in:
Yukihiro "Matz" Matsumoto
2025-12-13 22:31:51 +09:00
parent d47dedd17d
commit d3fcb176bf
6 changed files with 1086 additions and 12 deletions
+23
View File
@@ -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);
}
@@ -0,0 +1,776 @@
/*
** 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;
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);
}
}
@@ -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 <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, 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 */
+128 -12
View File
@@ -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) {
@@ -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
*/
@@ -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,