mirror of
https://github.com/mruby/mruby
synced 2026-06-08 16:11:16 +00:00
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:
@@ -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 */
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user