mruby-bin-mirb: replace readline with custom multi-line editor

Remove readline/linenoise dependency and implement custom multi-line
editor with:
- Terminal raw mode handling (POSIX termios)
- Multi-line buffer with cursor navigation
- Auto-indentation for Ruby blocks
- Auto-dedentation when typing 'end' or '}'
- Natural terminal scrolling behavior
- Emacs-style keybindings (Ctrl+A/E/K/U/W/Y, Alt+B/F/D)

This eliminates GPL licensing concerns from readline while providing
better multi-line editing than the previous single-line implementation.

The MRUBY_MIRB_READLINE environment variable is no longer supported
as readline integration has been completely removed; ref #6626

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Yukihiro "Matz" Matsumoto
2025-12-10 16:30:18 +09:00
parent b36e0b4090
commit 527018cf07
10 changed files with 2271 additions and 1303 deletions
-133
View File
@@ -1,140 +1,7 @@
# Environment Variable Configuration:
#
# MRUBY_MIRB_READLINE - Control which readline library mirb uses:
# auto (default) - Auto-detect: try readline, then edit, then linenoise
# readline, gnu - Force GNU readline only
# edit, libedit - Force libedit only
# linenoise - Force linenoise only
# none, off, false, disabled - Use plain input mode (no readline)
#
# Example:
# MRUBY_MIRB_READLINE=none rake
# MRUBY_MIRB_READLINE=linenoise rake
MRuby::Gem::Specification.new('mruby-bin-mirb') do |spec|
spec.license = 'MIT'
spec.author = 'mruby developers'
spec.summary = 'mirb command'
# Allow user to override readline detection via environment variable
readline_mode = (ENV['MRUBY_MIRB_READLINE'] || 'auto').downcase
case readline_mode
when 'auto'
# Auto-detect: try readline, then edit, then linenoise (default behavior)
if spec.build.cc.search_header_path 'readline/readline.h'
spec.cc.defines << "MRB_USE_READLINE"
spec.cc.defines << "MRB_READLINE_HEADER='<readline/readline.h>'"
spec.cc.defines << "MRB_READLINE_HISTORY='<readline/history.h>'"
if spec.build.cc.search_header_path 'termcap.h'
if MRUBY_BUILD_HOST_IS_CYGWIN || MRUBY_BUILD_HOST_IS_OPENBSD
if spec.build.cc.search_header_path 'termcap.h'
if MRUBY_BUILD_HOST_IS_CYGWIN then
spec.linker.libraries << 'ncurses'
else
spec.linker.libraries << 'termcap'
end
end
end
end
if RUBY_PLATFORM.include?('netbsd')
spec.linker.libraries << 'edit'
else
spec.linker.libraries << 'readline'
if RUBY_PLATFORM.include?('darwin')
# Workaround to build with Homebrew's readline on Mac (#4537)
lib_path = spec.build.cc.header_search_paths.find do |include_path|
lib_path = File.expand_path("#{include_path}/../lib")
break lib_path if File.exist?("#{lib_path}/libreadline.dylib") ||
File.exist?("#{lib_path}/libreadline.a")
end
spec.linker.library_paths << lib_path if lib_path
elsif spec.build.cc.search_header_path 'curses.h'
spec.linker.libraries << 'ncurses'
if spec.build.cc.search_header_path 'term.h'
spec.linker.libraries << 'tinfo'
end
elsif spec.build.cc.search_header_path 'ncursesw/curses.h'
spec.linker.libraries << 'ncursesw'
if spec.build.cc.search_header_path 'ncursesw/term.h'
spec.linker.libraries << 'tinfow'
end
end
end
elsif spec.build.cc.search_header_path 'edit/readline/readline.h'
spec.cc.defines << "MRB_USE_READLINE"
spec.cc.defines << "MRB_READLINE_HEADER='<edit/readline/readline.h>'"
spec.cc.defines << "MRB_READLINE_HISTORY='<edit/readline/history.h>'"
spec.linker.libraries << "edit"
elsif spec.build.cc.search_header_path 'linenoise.h'
spec.cc.defines << "MRB_USE_LINENOISE"
end
when 'readline', 'gnu'
# Force GNU readline only
if spec.build.cc.search_header_path 'readline/readline.h'
spec.cc.defines << "MRB_USE_READLINE"
spec.cc.defines << "MRB_READLINE_HEADER='<readline/readline.h>'"
spec.cc.defines << "MRB_READLINE_HISTORY='<readline/history.h>'"
if spec.build.cc.search_header_path 'termcap.h'
if MRUBY_BUILD_HOST_IS_CYGWIN || MRUBY_BUILD_HOST_IS_OPENBSD
if spec.build.cc.search_header_path 'termcap.h'
if MRUBY_BUILD_HOST_IS_CYGWIN then
spec.linker.libraries << 'ncurses'
else
spec.linker.libraries << 'termcap'
end
end
end
end
if RUBY_PLATFORM.include?('netbsd')
spec.linker.libraries << 'edit'
else
spec.linker.libraries << 'readline'
if RUBY_PLATFORM.include?('darwin')
lib_path = spec.build.cc.header_search_paths.find do |include_path|
lib_path = File.expand_path("#{include_path}/../lib")
break lib_path if File.exist?("#{lib_path}/libreadline.dylib") ||
File.exist?("#{lib_path}/libreadline.a")
end
spec.linker.library_paths << lib_path if lib_path
elsif spec.build.cc.search_header_path 'curses.h'
spec.linker.libraries << 'ncurses'
if spec.build.cc.search_header_path 'term.h'
spec.linker.libraries << 'tinfo'
end
elsif spec.build.cc.search_header_path 'ncursesw/curses.h'
spec.linker.libraries << 'ncursesw'
if spec.build.cc.search_header_path 'ncursesw/term.h'
spec.linker.libraries << 'tinfow'
end
end
end
end
when 'edit', 'libedit'
# Force libedit only
if spec.build.cc.search_header_path 'edit/readline/readline.h'
spec.cc.defines << "MRB_USE_READLINE"
spec.cc.defines << "MRB_READLINE_HEADER='<edit/readline/readline.h>'"
spec.cc.defines << "MRB_READLINE_HISTORY='<edit/readline/history.h>'"
spec.linker.libraries << "edit"
end
when 'linenoise'
# Force linenoise only
if spec.build.cc.search_header_path 'linenoise.h'
spec.cc.defines << "MRB_USE_LINENOISE"
end
when 'none', 'off', 'false', 'disabled'
# Disable readline - use plain input mode
else
fail "Invalid MRUBY_MIRB_READLINE='#{readline_mode}'. " \
"Valid values: auto, readline, gnu, edit, libedit, linenoise, none, off, false, disabled"
end
spec.bins = %w(mirb)
spec.add_dependency('mruby-compiler', :core => 'mruby-compiler')
end
+127 -399
View File
@@ -27,7 +27,6 @@
#include <ctype.h>
#include <signal.h>
#include <setjmp.h>
#ifdef _WIN32
#include <io.h>
@@ -36,256 +35,19 @@
#include <unistd.h>
#endif
#include "mirb_completion.h"
#include "mirb_editor.h"
/* obsolete configuration */
#ifdef ENABLE_READLINE
# define MRB_USE_READLINE
#endif
#ifdef ENABLE_LINENOISE
# define MRB_USE_LINENOISE
#endif
#ifdef DISABLE_MIRB_UNDERSCORE
# define MRB_NO_MIRB_UNDERSCORE
#endif
#ifdef MRB_USE_READLINE
#include MRB_READLINE_HEADER
#include MRB_READLINE_HISTORY
#define MIRB_ADD_HISTORY(line) add_history(line)
#define MIRB_READLINE(ch) readline(ch)
#if !defined(RL_READLINE_VERSION) || RL_READLINE_VERSION < 0x600
/* libedit & older readline do not have rl_free() */
#define MIRB_LINE_FREE(line) free(line)
#else
#define MIRB_LINE_FREE(line) rl_free(line)
#endif
#define MIRB_WRITE_HISTORY(path) write_history(path)
#define MIRB_READ_HISTORY(path) read_history(path)
#define MIRB_USING_HISTORY() using_history()
#elif defined(MRB_USE_LINENOISE)
#define MRB_USE_READLINE
#include <linenoise.h>
#define MIRB_ADD_HISTORY(line) linenoiseHistoryAdd(line)
#define MIRB_READLINE(ch) linenoise(ch)
#define MIRB_LINE_FREE(line) linenoiseFree(line)
#define MIRB_WRITE_HISTORY(path) linenoiseHistorySave(path)
#define MIRB_READ_HISTORY(path) linenoiseHistoryLoad(history_path)
#define MIRB_USING_HISTORY()
#endif
#if !defined(_WIN32) && defined(_POSIX_C_SOURCE)
#define MIRB_SIGSETJMP(env) sigsetjmp(env, 1)
#define MIRB_SIGLONGJMP(env, val) siglongjmp(env, val)
#define SIGJMP_BUF sigjmp_buf
#else
#define MIRB_SIGSETJMP(env) setjmp(env)
#define MIRB_SIGLONGJMP(env, val) longjmp(env, val)
#define SIGJMP_BUF jmp_buf
#endif
#ifdef MRB_USE_READLINE
static const char history_file_name[] = ".mirb_history";
/* Auto-indent support for GNU readline (not linenoise) */
#if !defined(MRB_USE_LINENOISE) && defined(RL_READLINE_VERSION)
static int mirb_indent_level = 0;
static mrb_bool mirb_use_ansi = FALSE;
static int
mirb_startup_hook(void)
{
if (mirb_indent_level > 0) {
int i;
for (i = 0; i < mirb_indent_level; i++) {
rl_insert_text(" ");
}
}
return 0;
}
/* Check if terminal supports ANSI escape sequences */
static mrb_bool
supports_escape_sequences(void)
{
const char *term;
if (!isatty(fileno(stdout))) return FALSE;
term = getenv("TERM");
if (!term || strcmp(term, "dumb") == 0) return FALSE;
/* Respect NO_COLOR convention */
if (getenv("NO_COLOR")) return FALSE;
return TRUE;
}
/* ANSI color codes - safe colors that work on light and dark themes */
#define MIRB_COLOR_RESET "\033[0m"
#define MIRB_COLOR_BOLD "\033[1m"
#define MIRB_COLOR_RED "\033[31m"
#define MIRB_COLOR_GREEN "\033[32m"
/* Color helpers - return empty string if colors disabled */
static const char *col_reset(void) { return mirb_use_ansi ? MIRB_COLOR_RESET : ""; }
static const char *col_bold(void) { return mirb_use_ansi ? MIRB_COLOR_BOLD : ""; }
static const char *col_red(void) { return mirb_use_ansi ? MIRB_COLOR_RED : ""; }
/* Check if line starts with a mid-block keyword (else, elsif, rescue, ensure, when)
* These keywords should be at the same indent level as the opening keyword */
static mrb_bool
is_midblock_keyword(const char *line)
{
/* skip leading whitespace */
while (*line == ' ' || *line == '\t') line++;
if (strncmp(line, "else", 4) == 0 && !ISALNUM(line[4]) && line[4] != '_') return TRUE;
if (strncmp(line, "elsif ", 6) == 0) return TRUE;
if (strncmp(line, "rescue", 6) == 0 && !ISALNUM(line[6]) && line[6] != '_') return TRUE;
if (strncmp(line, "ensure", 6) == 0 && !ISALNUM(line[6]) && line[6] != '_') return TRUE;
if (strncmp(line, "when ", 5) == 0) return TRUE;
return FALSE;
}
/* Reprint line with corrected indentation using ANSI escapes */
static void
reprint_line_with_indent(int line_num, const char *line, int indent)
{
const char *content = line;
const char *end;
int i;
/* Skip original leading whitespace to get actual content */
while (*content == ' ' || *content == '\t') content++;
/* Find end of content (exclude trailing newline) */
end = content + strlen(content);
while (end > content && (end[-1] == '\n' || end[-1] == '\r')) end--;
/* Move cursor up, clear line, return to beginning */
printf("\033[A\r\033[K");
/* Print prompt (green for all prompts) */
printf("%s%d*%s ", mirb_use_ansi ? MIRB_COLOR_GREEN : "", line_num, col_reset());
/* Print corrected indentation */
for (i = 0; i < indent; i++) {
printf(" ");
}
/* Print content (no newline - cursor stays at end of line) */
printf("%.*s", (int)(end - content), content);
/* Move cursor back down to next line */
printf("\n");
fflush(stdout);
}
/* Calculate indent level by counting open blocks in code */
static int
calc_indent_level(const char *code)
{
int level = 0;
const char *p = code;
while (*p) {
/* Skip strings */
if (*p == '"' || *p == '\'') {
char quote = *p++;
while (*p && *p != quote) {
if (*p == '\\' && p[1]) p++;
p++;
}
if (*p) p++;
continue;
}
/* Skip comments */
if (*p == '#') {
while (*p && *p != '\n') p++;
continue;
}
/* Check for keywords */
if (p == code || !ISALNUM(p[-1])) {
if (strncmp(p, "def ", 4) == 0 ||
strncmp(p, "class ", 6) == 0 ||
strncmp(p, "module ", 7) == 0 ||
strncmp(p, "do\n", 3) == 0 ||
strncmp(p, "do ", 3) == 0 ||
(strncmp(p, "do", 2) == 0 && (p[2] == '\0' || p[2] == '\n')) ||
strncmp(p, "if ", 3) == 0 ||
strncmp(p, "unless ", 7) == 0 ||
strncmp(p, "case ", 5) == 0 ||
strncmp(p, "while ", 6) == 0 ||
strncmp(p, "until ", 6) == 0 ||
strncmp(p, "for ", 4) == 0 ||
strncmp(p, "begin\n", 6) == 0 ||
(strncmp(p, "begin", 5) == 0 && (p[5] == '\0' || p[5] == '\n'))) {
level++;
}
else if (strncmp(p, "end\n", 4) == 0 ||
strncmp(p, "end ", 4) == 0 ||
(strncmp(p, "end", 3) == 0 && (p[3] == '\0' || p[3] == '\n'))) {
if (level > 0) level--;
}
}
/* Check for block opening with { */
if (*p == '{') {
level++;
}
else if (*p == '}') {
if (level > 0) level--;
}
p++;
}
return level;
}
#endif /* !MRB_USE_LINENOISE && RL_READLINE_VERSION */
static char *
get_history_path(mrb_state *mrb)
{
char *path = NULL;
const char *home = getenv("HOME");
#ifdef _WIN32
if (home != NULL) {
home = getenv("USERPROFILE");
}
#endif
if (home != NULL) {
int len = snprintf(NULL, 0, "%s/%s", home, history_file_name);
if (len >= 0) {
size_t size = len + 1;
path = (char*)mrb_malloc_simple(mrb, size);
if (path != NULL) {
int n = snprintf(path, size, "%s/%s", home, history_file_name);
if (n != len) {
mrb_free(mrb, path);
path = NULL;
}
}
}
}
return path;
}
#endif
static void
p(mrb_state *mrb, mrb_value obj)
{
mrb_value val = mrb_funcall_argv(mrb, obj, MRB_SYM(inspect), 0, NULL);
if (!mrb->exc) {
#if !defined(MRB_USE_LINENOISE) && defined(RL_READLINE_VERSION)
printf(" %s=>%s ", col_bold(), col_reset());
#else
fputs(" => ", stdout);
#endif
}
else {
val = mrb_exc_get_output(mrb, mrb->exc);
@@ -307,10 +69,6 @@ p_error(mrb_state *mrb, struct RObject* exc, mrb_ccontext *cxt)
val = mrb_obj_as_string(mrb, val);
}
#if !defined(MRB_USE_LINENOISE) && defined(RL_READLINE_VERSION)
printf("%s", col_red());
#endif
/* get first line of backtrace for location info */
mrb_value bt = mrb_exc_backtrace(mrb, mrb_obj_value(exc));
if (mrb_array_p(bt) && RARRAY_LEN(bt) > 0) {
@@ -346,11 +104,7 @@ p_error(mrb_state *mrb, struct RObject* exc, mrb_ccontext *cxt)
char* msg = mrb_locale_from_utf8(RSTRING_PTR(val), (int)RSTRING_LEN(val));
fwrite(msg, strlen(msg), 1, stdout);
mrb_locale_free(msg);
#if !defined(MRB_USE_LINENOISE) && defined(RL_READLINE_VERSION)
printf("%s\n", col_reset());
#else
putc('\n', stdout);
#endif
}
/* Guess if the user might want to enter more
@@ -611,7 +365,6 @@ extract_line(const char *str, int target_line, size_t *line_len)
return line_start;
}
#ifndef MRB_USE_READLINE
/* Print the command line prompt of the REPL */
static void
print_cmdline(int code_block_open, int line_num)
@@ -619,7 +372,6 @@ print_cmdline(int code_block_open, int line_num)
printf("%d%c ", line_num, code_block_open ? '*' : '>');
fflush(stdout);
}
#endif
static int
check_keyword(const char *buf, const char *word)
@@ -644,25 +396,44 @@ check_keyword(const char *buf, const char *word)
return 1;
}
#ifndef MRB_USE_READLINE
volatile sig_atomic_t input_canceled = 0;
void
/* Data for completion checker callback */
typedef struct {
mrb_state *mrb;
mrb_ccontext *cxt;
} mirb_check_data;
/* Check if code is syntactically complete (for multi-line editor) */
static mrb_bool
mirb_check_code_complete(const char *code, void *user_data)
{
mirb_check_data *data = (mirb_check_data *)user_data;
struct mrb_parser_state *parser;
mrb_bool complete;
parser = mrb_parser_new(data->mrb);
if (parser == NULL) return TRUE; /* error - accept input */
parser->s = code;
parser->send = code + strlen(code);
parser->lineno = data->cxt->lineno;
mrb_parser_parse(parser, data->cxt);
complete = !is_code_block_open(parser);
mrb_parser_free(parser);
return complete;
}
static void
ctrl_c_handler(int signo)
{
input_canceled = 1;
}
#else
SIGJMP_BUF ctrl_c_buf;
void
ctrl_c_handler(int signo)
{
MIRB_SIGLONGJMP(ctrl_c_buf, 1);
}
#endif
#ifndef MRB_NO_MIRB_UNDERSCORE
void decl_lv_underscore(mrb_state *mrb, mrb_ccontext *cxt)
static void
decl_lv_underscore(mrb_state *mrb, mrb_ccontext *cxt)
{
struct RProc *proc;
struct mrb_parser_state *parser;
@@ -686,13 +457,11 @@ main(int argc, char **argv)
{
char ruby_code[4096] = { 0 };
char last_code_line[1024] = { 0 };
#ifndef MRB_USE_READLINE
int last_char;
size_t char_index;
#else
char *history_path;
char* line;
#endif
mirb_editor editor;
mirb_check_data check_data;
mrb_bool use_editor = FALSE;
mrb_ccontext *cxt;
struct mrb_parser_state *parser;
mrb_state *mrb;
@@ -732,18 +501,6 @@ main(int argc, char **argv)
mrb_define_global_const(mrb, "ARGV", ARGV);
mrb_gv_set(mrb, mrb_intern_lit(mrb, "$DEBUG"), mrb_bool_value(args.debug));
#ifdef MRB_USE_READLINE
history_path = get_history_path(mrb);
if (history_path == NULL) {
fputs("failed to get history path\n", stderr);
mrb_close(mrb);
return EXIT_FAILURE;
}
MIRB_USING_HISTORY();
MIRB_READ_HISTORY(history_path);
#endif
print_hint();
cxt = mrb_ccontext_new(mrb);
@@ -771,21 +528,20 @@ main(int argc, char **argv)
mrb_ccontext_filename(mrb, cxt, "(mirb)");
if (args.verbose) cxt->dump_result = TRUE;
/* Setup tab completion and auto-indent */
#ifdef MRB_USE_READLINE
#ifndef MRB_USE_LINENOISE
mirb_setup_readline_completion(mrb, cxt);
#if defined(RL_READLINE_VERSION)
/* Only enable auto-indent for interactive input */
if (isatty(fileno(stdin))) {
rl_startup_hook = mirb_startup_hook;
mirb_use_ansi = supports_escape_sequences();
/* Initialize multi-line editor */
if (isatty(fileno(stdin)) && mirb_editor_init(&editor)) {
use_editor = TRUE;
check_data.mrb = mrb;
check_data.cxt = cxt;
mirb_editor_set_check_complete(&editor, mirb_check_code_complete, &check_data);
/* Enable colored prompts if terminal supports it */
if (isatty(fileno(stdout))) {
const char *term = getenv("TERM");
if (term && strcmp(term, "dumb") != 0 && !getenv("NO_COLOR")) {
mirb_editor_set_color(&editor, TRUE);
}
}
}
#endif
#else
mirb_setup_linenoise_completion(mrb, cxt);
#endif
#endif
ai = mrb_gc_arena_save(mrb);
@@ -798,83 +554,87 @@ main(int argc, char **argv)
break;
}
#ifndef MRB_USE_READLINE
print_cmdline(code_block_open, line_num);
if (use_editor && mirb_editor_supported(&editor)) {
/* Use multi-line editor */
char *input;
char prompt[16], prompt_cont[16];
mirb_edit_result res;
signal(SIGINT, ctrl_c_handler);
char_index = 0;
while ((last_char = getchar()) != '\n') {
if (last_char == EOF) break;
if (char_index >= sizeof(last_code_line)-2) {
fputs("input string too long\n", stderr);
snprintf(prompt, sizeof(prompt), "%d> ", line_num);
snprintf(prompt_cont, sizeof(prompt_cont), "%d* ", line_num);
mirb_editor_set_prompts(&editor, prompt, prompt_cont);
res = mirb_editor_read(&editor, &input);
if (res == MIRB_EDIT_EOF) {
break;
}
if (res == MIRB_EDIT_INTERRUPT) {
puts("^C");
continue;
}
if (res != MIRB_EDIT_OK || input == NULL) {
continue;
}
last_code_line[char_index++] = last_char;
}
signal(SIGINT, SIG_DFL);
if (input_canceled) {
ruby_code[0] = '\0';
last_code_line[0] = '\0';
code_block_open = FALSE;
line_num = 1;
puts("^C");
input_canceled = 0;
continue;
}
if (last_char == EOF) {
fputs("\n", stdout);
break;
}
last_code_line[char_index++] = '\n';
last_code_line[char_index] = '\0';
#else
if (MIRB_SIGSETJMP(ctrl_c_buf) == 0) {
;
/* The editor returns complete multi-line input */
if (strlen(input) >= sizeof(ruby_code) - 1) {
fputs("input string too long\n", stderr);
free(input);
continue;
}
strcpy(ruby_code, input);
free(input);
/* Count lines for line number update */
{
const char *p = ruby_code;
while (*p) {
if (*p++ == '\n') line_num++;
}
}
/* Check for quit/exit commands */
if (check_keyword(ruby_code, "quit") || check_keyword(ruby_code, "exit")) {
break;
}
/* Skip to evaluation (editor already handles multi-line) */
code_block_open = FALSE;
goto evaluate;
}
else {
ruby_code[0] = '\0';
last_code_line[0] = '\0';
code_block_open = FALSE;
line_num = 1;
puts("^C");
}
signal(SIGINT, ctrl_c_handler);
{
char prompt[64];
#if !defined(MRB_USE_LINENOISE) && defined(RL_READLINE_VERSION)
/* Set indent level for auto-indent on continuation lines */
mirb_indent_level = code_block_open ? calc_indent_level(ruby_code) : 0;
/* Build prompt with colors - readline needs \001 \002 around non-printing chars */
if (mirb_use_ansi) {
snprintf(prompt, sizeof(prompt), "\001" MIRB_COLOR_GREEN "\002%d%c\001" MIRB_COLOR_RESET "\002 ",
line_num, code_block_open ? '*' : '>');
}
else {
snprintf(prompt, sizeof(prompt), "%d%c ", line_num, code_block_open ? '*' : '>');
}
#else
snprintf(prompt, sizeof(prompt), "%d%c ", line_num, code_block_open ? '*' : '>');
#endif
line = MIRB_READLINE(prompt);
}
signal(SIGINT, SIG_DFL);
/* Fallback to simple line-by-line input */
print_cmdline(code_block_open, line_num);
if (line == NULL) {
printf("\n");
break;
signal(SIGINT, ctrl_c_handler);
char_index = 0;
while ((last_char = getchar()) != '\n') {
if (last_char == EOF) break;
if (char_index >= sizeof(last_code_line)-2) {
fputs("input string too long\n", stderr);
continue;
}
last_code_line[char_index++] = last_char;
}
signal(SIGINT, SIG_DFL);
if (input_canceled) {
ruby_code[0] = '\0';
last_code_line[0] = '\0';
code_block_open = FALSE;
line_num = 1;
puts("^C");
input_canceled = 0;
continue;
}
if (last_char == EOF) {
fputs("\n", stdout);
break;
}
last_code_line[char_index++] = '\n';
last_code_line[char_index] = '\0';
}
if (strlen(line) > sizeof(last_code_line)-2) {
fputs("input string too long\n", stderr);
continue;
}
strcpy(last_code_line, line);
strcat(last_code_line, "\n");
if (strlen(line) > 0) {
MIRB_ADD_HISTORY(line);
}
MIRB_LINE_FREE(line);
#endif
line_num++;
done:
@@ -884,20 +644,6 @@ main(int argc, char **argv)
continue;
}
strcat(ruby_code, last_code_line);
#if !defined(MRB_USE_LINENOISE) && defined(RL_READLINE_VERSION)
/* Check if indent level decreased or mid-block keyword needs dedent */
if (mirb_use_ansi && mirb_indent_level > 0) {
int new_level = calc_indent_level(ruby_code);
if (new_level < mirb_indent_level) {
/* Block closed (end, }) - use new lower level */
reprint_line_with_indent(line_num - 1, last_code_line, new_level);
}
else if (is_midblock_keyword(last_code_line)) {
/* Mid-block keyword (else, elsif, rescue, ensure, when) - dedent by 1 */
reprint_line_with_indent(line_num - 1, last_code_line, mirb_indent_level - 1);
}
}
#endif
}
else {
if (check_keyword(last_code_line, "quit") || check_keyword(last_code_line, "exit")) {
@@ -906,6 +652,7 @@ main(int argc, char **argv)
strcpy(ruby_code, last_code_line);
}
evaluate:
utf8 = mrb_utf8_from_locale(ruby_code, -1);
if (!utf8) abort();
@@ -929,11 +676,7 @@ main(int argc, char **argv)
if (0 < parser->nwarn) {
/* warning */
char* msg = mrb_locale_from_utf8(parser->warn_buffer[0].message, -1);
#if !defined(MRB_USE_LINENOISE) && defined(RL_READLINE_VERSION)
printf("%swarning: line %d: %s%s\n", col_red(), parser->warn_buffer[0].lineno, msg, col_reset());
#else
printf("warning: line %d: %s\n", parser->warn_buffer[0].lineno, msg);
#endif
mrb_locale_free(msg);
}
if (0 < parser->nerr) {
@@ -946,9 +689,6 @@ main(int argc, char **argv)
int relative_line = err_line - cxt->lineno + 1;
/* show error with line:column (using relative line number) */
#if !defined(MRB_USE_LINENOISE) && defined(RL_READLINE_VERSION)
printf("%s", col_red());
#endif
printf("line %d:%d: %s\n", relative_line, err_col, msg);
/* show source line and caret if available */
@@ -959,15 +699,12 @@ main(int argc, char **argv)
if (line_len > 0) {
printf(" %.*s\n", (int)line_len, line_start);
printf(" ");
for (int i = 0; i < err_col; i++) {
for (int j = 0; j < err_col; j++) {
printf(" ");
}
printf("^\n");
}
}
#if !defined(MRB_USE_LINENOISE) && defined(RL_READLINE_VERSION)
printf("%s", col_reset());
#endif
mrb_locale_free(msg);
line_num = 1;
@@ -1023,11 +760,6 @@ main(int argc, char **argv)
cxt->lineno++;
}
#ifdef MRB_USE_READLINE
MIRB_WRITE_HISTORY(history_path);
mrb_free(mrb, history_path);
#endif
if (args.rfp) fclose(args.rfp);
mrb_free(mrb, args.argv);
if (args.libv) {
@@ -1038,14 +770,10 @@ main(int argc, char **argv)
}
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
/* Cleanup editor */
if (use_editor) {
mirb_editor_cleanup(&editor);
}
mrb_close(mrb);
@@ -0,0 +1,780 @@
/*
** mirb_buffer.c - Multi-line buffer for mirb editor
**
** See Copyright Notice in mruby.h
*/
#include "mirb_buffer.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
/*
* Helper: Initialize a single line
*/
static mrb_bool
line_init(mirb_line *line)
{
line->data = (char*)malloc(MIRB_BUF_LINE_INIT);
if (line->data == NULL) return FALSE;
line->data[0] = '\0';
line->len = 0;
line->cap = MIRB_BUF_LINE_INIT;
return TRUE;
}
/*
* Helper: Free a single line
*/
static void
line_free(mirb_line *line)
{
free(line->data);
line->data = NULL;
line->len = 0;
line->cap = 0;
}
/*
* Helper: Ensure line has capacity for additional chars
*/
static mrb_bool
line_ensure_cap(mirb_line *line, size_t additional)
{
size_t needed = line->len + additional + 1; /* +1 for null */
if (needed <= line->cap) return TRUE;
size_t new_cap = line->cap * 2;
while (new_cap < needed) new_cap *= 2;
if (new_cap > MIRB_BUF_LINE_MAX) new_cap = MIRB_BUF_LINE_MAX;
if (new_cap < needed) return FALSE;
char *new_data = (char*)realloc(line->data, new_cap);
if (new_data == NULL) return FALSE;
line->data = new_data;
line->cap = new_cap;
return TRUE;
}
/*
* Helper: Insert character at position in line
*/
static mrb_bool
line_insert_at(mirb_line *line, size_t pos, char c)
{
if (pos > line->len) return FALSE;
if (!line_ensure_cap(line, 1)) return FALSE;
memmove(line->data + pos + 1, line->data + pos, line->len - pos + 1);
line->data[pos] = c;
line->len++;
return TRUE;
}
/*
* Helper: Delete character at position in line
*/
static mrb_bool
line_delete_at(mirb_line *line, size_t pos)
{
if (pos >= line->len) return FALSE;
memmove(line->data + pos, line->data + pos + 1, line->len - pos);
line->len--;
return TRUE;
}
/*
* Helper: Set line content
*/
static mrb_bool
line_set(mirb_line *line, const char *str, size_t len)
{
if (len + 1 > line->cap) {
size_t new_cap = MIRB_BUF_LINE_INIT;
while (new_cap < len + 1) new_cap *= 2;
if (new_cap > MIRB_BUF_LINE_MAX) return FALSE;
char *new_data = (char*)realloc(line->data, new_cap);
if (new_data == NULL) return FALSE;
line->data = new_data;
line->cap = new_cap;
}
memcpy(line->data, str, len);
line->data[len] = '\0';
line->len = len;
return TRUE;
}
/*
* Initialize buffer
*/
mrb_bool
mirb_buffer_init(mirb_buffer *buf)
{
memset(buf, 0, sizeof(*buf));
buf->lines = (mirb_line*)malloc(sizeof(mirb_line) * MIRB_BUF_LINES_INIT);
if (buf->lines == NULL) return FALSE;
buf->line_cap = MIRB_BUF_LINES_INIT;
/* Start with one empty line */
if (!line_init(&buf->lines[0])) {
free(buf->lines);
return FALSE;
}
buf->line_count = 1;
buf->kill_buf = (char*)malloc(MIRB_BUF_KILL_SIZE);
if (buf->kill_buf == NULL) {
line_free(&buf->lines[0]);
free(buf->lines);
return FALSE;
}
buf->kill_buf[0] = '\0';
buf->kill_len = 0;
return TRUE;
}
/*
* Free buffer resources
*/
void
mirb_buffer_free(mirb_buffer *buf)
{
if (buf->lines) {
for (size_t i = 0; i < buf->line_count; i++) {
line_free(&buf->lines[i]);
}
free(buf->lines);
buf->lines = NULL;
}
free(buf->kill_buf);
buf->kill_buf = NULL;
}
/*
* Clear buffer content
*/
void
mirb_buffer_clear(mirb_buffer *buf)
{
/* Free all lines except first */
for (size_t i = 1; i < buf->line_count; i++) {
line_free(&buf->lines[i]);
}
/* Clear first line */
buf->lines[0].data[0] = '\0';
buf->lines[0].len = 0;
buf->line_count = 1;
buf->cursor_line = 0;
buf->cursor_col = 0;
buf->modified = FALSE;
}
/*
* Get total character count
*/
size_t
mirb_buffer_total_len(mirb_buffer *buf)
{
size_t total = 0;
for (size_t i = 0; i < buf->line_count; i++) {
total += buf->lines[i].len;
if (i < buf->line_count - 1) total++; /* newline */
}
return total;
}
/*
* Get buffer as string
*/
char *
mirb_buffer_to_string(mirb_buffer *buf)
{
size_t total = mirb_buffer_total_len(buf);
char *str = (char*)malloc(total + 1);
if (str == NULL) return NULL;
char *p = str;
for (size_t i = 0; i < buf->line_count; i++) {
memcpy(p, buf->lines[i].data, buf->lines[i].len);
p += buf->lines[i].len;
if (i < buf->line_count - 1) *p++ = '\n';
}
*p = '\0';
return str;
}
/*
* Set buffer from string
*/
mrb_bool
mirb_buffer_set_string(mirb_buffer *buf, const char *str)
{
mirb_buffer_clear(buf);
if (str == NULL || *str == '\0') return TRUE;
const char *start = str;
const char *p = str;
size_t line_idx = 0;
while (*p) {
if (*p == '\n') {
/* Set current line */
if (line_idx >= buf->line_count) {
/* Need to add new line */
if (buf->line_count >= buf->line_cap) {
size_t new_cap = buf->line_cap * 2;
if (new_cap > MIRB_BUF_LINES_MAX) return FALSE;
mirb_line *new_lines = (mirb_line*)realloc(buf->lines, sizeof(mirb_line) * new_cap);
if (new_lines == NULL) return FALSE;
buf->lines = new_lines;
buf->line_cap = new_cap;
}
if (!line_init(&buf->lines[buf->line_count])) return FALSE;
buf->line_count++;
}
if (!line_set(&buf->lines[line_idx], start, p - start)) return FALSE;
start = p + 1;
line_idx++;
p++;
}
else {
p++;
}
}
/* Handle last line (may not end with newline) */
if (start < p || line_idx == 0) {
if (line_idx >= buf->line_count) {
if (buf->line_count >= buf->line_cap) {
size_t new_cap = buf->line_cap * 2;
if (new_cap > MIRB_BUF_LINES_MAX) return FALSE;
mirb_line *new_lines = (mirb_line*)realloc(buf->lines, sizeof(mirb_line) * new_cap);
if (new_lines == NULL) return FALSE;
buf->lines = new_lines;
buf->line_cap = new_cap;
}
if (!line_init(&buf->lines[buf->line_count])) return FALSE;
buf->line_count++;
}
if (!line_set(&buf->lines[line_idx], start, p - start)) return FALSE;
}
buf->cursor_line = 0;
buf->cursor_col = 0;
buf->modified = FALSE;
return TRUE;
}
/*
* Insert character at cursor
*/
mrb_bool
mirb_buffer_insert_char(mirb_buffer *buf, char c)
{
mirb_line *line = &buf->lines[buf->cursor_line];
if (!line_insert_at(line, buf->cursor_col, c)) return FALSE;
buf->cursor_col++;
buf->modified = TRUE;
return TRUE;
}
/*
* Insert string at cursor
*/
mrb_bool
mirb_buffer_insert_string(mirb_buffer *buf, const char *str, size_t len)
{
for (size_t i = 0; i < len; i++) {
if (str[i] == '\n') {
if (!mirb_buffer_newline(buf)) return FALSE;
}
else {
if (!mirb_buffer_insert_char(buf, str[i])) return FALSE;
}
}
return TRUE;
}
/*
* Delete character before cursor
*/
mrb_bool
mirb_buffer_delete_back(mirb_buffer *buf)
{
if (buf->cursor_col > 0) {
/* Delete within line */
mirb_line *line = &buf->lines[buf->cursor_line];
if (line_delete_at(line, buf->cursor_col - 1)) {
buf->cursor_col--;
buf->modified = TRUE;
return TRUE;
}
}
else if (buf->cursor_line > 0) {
/* Join with previous line */
mirb_line *prev = &buf->lines[buf->cursor_line - 1];
mirb_line *curr = &buf->lines[buf->cursor_line];
size_t prev_len = prev->len;
/* Append current line to previous */
if (!line_ensure_cap(prev, curr->len)) return FALSE;
memcpy(prev->data + prev->len, curr->data, curr->len + 1);
prev->len += curr->len;
/* Remove current line */
line_free(curr);
memmove(&buf->lines[buf->cursor_line],
&buf->lines[buf->cursor_line + 1],
sizeof(mirb_line) * (buf->line_count - buf->cursor_line - 1));
buf->line_count--;
buf->cursor_line--;
buf->cursor_col = prev_len;
buf->modified = TRUE;
return TRUE;
}
return FALSE;
}
/*
* Delete character at cursor
*/
mrb_bool
mirb_buffer_delete_forward(mirb_buffer *buf)
{
mirb_line *line = &buf->lines[buf->cursor_line];
if (buf->cursor_col < line->len) {
/* Delete within line */
if (line_delete_at(line, buf->cursor_col)) {
buf->modified = TRUE;
return TRUE;
}
}
else if (buf->cursor_line < buf->line_count - 1) {
/* Join with next line */
mirb_line *curr = &buf->lines[buf->cursor_line];
mirb_line *next = &buf->lines[buf->cursor_line + 1];
/* Append next line to current */
if (!line_ensure_cap(curr, next->len)) return FALSE;
memcpy(curr->data + curr->len, next->data, next->len + 1);
curr->len += next->len;
/* Remove next line */
line_free(next);
memmove(&buf->lines[buf->cursor_line + 1],
&buf->lines[buf->cursor_line + 2],
sizeof(mirb_line) * (buf->line_count - buf->cursor_line - 2));
buf->line_count--;
buf->modified = TRUE;
return TRUE;
}
return FALSE;
}
/*
* Insert newline (split line)
*/
mrb_bool
mirb_buffer_newline(mirb_buffer *buf)
{
/* Ensure we have room for a new line */
if (buf->line_count >= buf->line_cap) {
size_t new_cap = buf->line_cap * 2;
if (new_cap > MIRB_BUF_LINES_MAX) return FALSE;
mirb_line *new_lines = (mirb_line*)realloc(buf->lines, sizeof(mirb_line) * new_cap);
if (new_lines == NULL) return FALSE;
buf->lines = new_lines;
buf->line_cap = new_cap;
}
mirb_line *curr = &buf->lines[buf->cursor_line];
size_t split_pos = buf->cursor_col;
/* Make room for new line */
memmove(&buf->lines[buf->cursor_line + 2],
&buf->lines[buf->cursor_line + 1],
sizeof(mirb_line) * (buf->line_count - buf->cursor_line - 1));
/* Initialize new line with content after cursor */
mirb_line *new_line = &buf->lines[buf->cursor_line + 1];
if (!line_init(new_line)) {
/* Restore lines array */
memmove(&buf->lines[buf->cursor_line + 1],
&buf->lines[buf->cursor_line + 2],
sizeof(mirb_line) * (buf->line_count - buf->cursor_line - 1));
return FALSE;
}
if (!line_set(new_line, curr->data + split_pos, curr->len - split_pos)) {
line_free(new_line);
memmove(&buf->lines[buf->cursor_line + 1],
&buf->lines[buf->cursor_line + 2],
sizeof(mirb_line) * (buf->line_count - buf->cursor_line - 1));
return FALSE;
}
/* Truncate current line */
curr->data[split_pos] = '\0';
curr->len = split_pos;
buf->line_count++;
buf->cursor_line++;
buf->cursor_col = 0;
buf->modified = TRUE;
return TRUE;
}
/*
* Move cursor left
*/
mrb_bool
mirb_buffer_cursor_left(mirb_buffer *buf)
{
if (buf->cursor_col > 0) {
buf->cursor_col--;
return TRUE;
}
else if (buf->cursor_line > 0) {
buf->cursor_line--;
buf->cursor_col = buf->lines[buf->cursor_line].len;
return TRUE;
}
return FALSE;
}
/*
* Move cursor right
*/
mrb_bool
mirb_buffer_cursor_right(mirb_buffer *buf)
{
mirb_line *line = &buf->lines[buf->cursor_line];
if (buf->cursor_col < line->len) {
buf->cursor_col++;
return TRUE;
}
else if (buf->cursor_line < buf->line_count - 1) {
buf->cursor_line++;
buf->cursor_col = 0;
return TRUE;
}
return FALSE;
}
/*
* Move cursor up
*/
mrb_bool
mirb_buffer_cursor_up(mirb_buffer *buf)
{
if (buf->cursor_line > 0) {
buf->cursor_line--;
/* Clamp column to line length */
if (buf->cursor_col > buf->lines[buf->cursor_line].len) {
buf->cursor_col = buf->lines[buf->cursor_line].len;
}
return TRUE;
}
return FALSE;
}
/*
* Move cursor down
*/
mrb_bool
mirb_buffer_cursor_down(mirb_buffer *buf)
{
if (buf->cursor_line < buf->line_count - 1) {
buf->cursor_line++;
/* Clamp column to line length */
if (buf->cursor_col > buf->lines[buf->cursor_line].len) {
buf->cursor_col = buf->lines[buf->cursor_line].len;
}
return TRUE;
}
return FALSE;
}
/*
* Move to beginning of line
*/
void
mirb_buffer_cursor_home(mirb_buffer *buf)
{
buf->cursor_col = 0;
}
/*
* Move to end of line
*/
void
mirb_buffer_cursor_end(mirb_buffer *buf)
{
buf->cursor_col = buf->lines[buf->cursor_line].len;
}
/*
* Move to start of buffer
*/
void
mirb_buffer_cursor_start(mirb_buffer *buf)
{
buf->cursor_line = 0;
buf->cursor_col = 0;
}
/*
* Move to end of buffer
*/
void
mirb_buffer_cursor_finish(mirb_buffer *buf)
{
buf->cursor_line = buf->line_count - 1;
buf->cursor_col = buf->lines[buf->cursor_line].len;
}
/*
* Helper: Check if character is word character
*/
static mrb_bool
is_word_char(char c)
{
return isalnum((unsigned char)c) || c == '_';
}
/*
* Move cursor back one word
*/
mrb_bool
mirb_buffer_cursor_word_back(mirb_buffer *buf)
{
mrb_bool moved = FALSE;
/* Skip any whitespace/non-word chars going back */
while (buf->cursor_col > 0 || buf->cursor_line > 0) {
if (buf->cursor_col == 0) {
if (buf->cursor_line == 0) break;
buf->cursor_line--;
buf->cursor_col = buf->lines[buf->cursor_line].len;
moved = TRUE;
continue;
}
char c = buf->lines[buf->cursor_line].data[buf->cursor_col - 1];
if (is_word_char(c)) break;
buf->cursor_col--;
moved = TRUE;
}
/* Move through word chars */
while (buf->cursor_col > 0) {
char c = buf->lines[buf->cursor_line].data[buf->cursor_col - 1];
if (!is_word_char(c)) break;
buf->cursor_col--;
moved = TRUE;
}
return moved;
}
/*
* Move cursor forward one word
*/
mrb_bool
mirb_buffer_cursor_word_forward(mirb_buffer *buf)
{
mrb_bool moved = FALSE;
mirb_line *line = &buf->lines[buf->cursor_line];
/* Move through current word chars */
while (buf->cursor_col < line->len) {
if (!is_word_char(line->data[buf->cursor_col])) break;
buf->cursor_col++;
moved = TRUE;
}
/* Skip whitespace/non-word chars */
while (buf->cursor_col < line->len || buf->cursor_line < buf->line_count - 1) {
if (buf->cursor_col >= line->len) {
if (buf->cursor_line >= buf->line_count - 1) break;
buf->cursor_line++;
buf->cursor_col = 0;
line = &buf->lines[buf->cursor_line];
moved = TRUE;
continue;
}
if (is_word_char(line->data[buf->cursor_col])) break;
buf->cursor_col++;
moved = TRUE;
}
return moved;
}
/*
* Helper: Save text to kill buffer
*/
static void
save_to_kill(mirb_buffer *buf, const char *str, size_t len)
{
if (len >= MIRB_BUF_KILL_SIZE) len = MIRB_BUF_KILL_SIZE - 1;
memcpy(buf->kill_buf, str, len);
buf->kill_buf[len] = '\0';
buf->kill_len = len;
}
/*
* Kill to end of line
*/
void
mirb_buffer_kill_to_end(mirb_buffer *buf)
{
mirb_line *line = &buf->lines[buf->cursor_line];
if (buf->cursor_col < line->len) {
/* Kill text to end of line */
save_to_kill(buf, line->data + buf->cursor_col, line->len - buf->cursor_col);
line->data[buf->cursor_col] = '\0';
line->len = buf->cursor_col;
buf->modified = TRUE;
}
else if (buf->cursor_line < buf->line_count - 1) {
/* At end of line: kill newline (join with next line) */
save_to_kill(buf, "\n", 1);
mirb_buffer_delete_forward(buf);
}
}
/*
* Kill to start of line
*/
void
mirb_buffer_kill_to_start(mirb_buffer *buf)
{
mirb_line *line = &buf->lines[buf->cursor_line];
if (buf->cursor_col > 0) {
save_to_kill(buf, line->data, buf->cursor_col);
memmove(line->data, line->data + buf->cursor_col, line->len - buf->cursor_col + 1);
line->len -= buf->cursor_col;
buf->cursor_col = 0;
buf->modified = TRUE;
}
}
/*
* Kill word backward
*/
void
mirb_buffer_kill_word_back(mirb_buffer *buf)
{
size_t start_line = buf->cursor_line;
size_t start_col = buf->cursor_col;
if (!mirb_buffer_cursor_word_back(buf)) return;
if (buf->cursor_line == start_line) {
/* Same line */
mirb_line *line = &buf->lines[buf->cursor_line];
size_t kill_len = start_col - buf->cursor_col;
save_to_kill(buf, line->data + buf->cursor_col, kill_len);
memmove(line->data + buf->cursor_col,
line->data + start_col,
line->len - start_col + 1);
line->len -= kill_len;
buf->modified = TRUE;
}
/* Cross-line kill is more complex; simplified: just delete chars */
}
/*
* Kill word forward
*/
void
mirb_buffer_kill_word_forward(mirb_buffer *buf)
{
size_t start_col = buf->cursor_col;
mirb_line *line = &buf->lines[buf->cursor_line];
/* Find end of word */
size_t end_col = start_col;
/* Skip word chars */
while (end_col < line->len && is_word_char(line->data[end_col])) {
end_col++;
}
/* Skip non-word chars */
while (end_col < line->len && !is_word_char(line->data[end_col])) {
end_col++;
}
if (end_col > start_col) {
save_to_kill(buf, line->data + start_col, end_col - start_col);
memmove(line->data + start_col,
line->data + end_col,
line->len - end_col + 1);
line->len -= (end_col - start_col);
buf->modified = TRUE;
}
}
/*
* Yank (paste) from kill buffer
*/
mrb_bool
mirb_buffer_yank(mirb_buffer *buf)
{
if (buf->kill_len == 0) return FALSE;
return mirb_buffer_insert_string(buf, buf->kill_buf, buf->kill_len);
}
/*
* Get current line content
*/
const char *
mirb_buffer_current_line(mirb_buffer *buf)
{
return buf->lines[buf->cursor_line].data;
}
/*
* Get line at index
*/
const char *
mirb_buffer_line_at(mirb_buffer *buf, size_t index)
{
if (index >= buf->line_count) return NULL;
return buf->lines[index].data;
}
/*
* Get line length at index
*/
size_t
mirb_buffer_line_len(mirb_buffer *buf, size_t index)
{
if (index >= buf->line_count) return 0;
return buf->lines[index].len;
}
@@ -0,0 +1,168 @@
/*
** mirb_buffer.h - Multi-line buffer for mirb editor
**
** See Copyright Notice in mruby.h
*/
#ifndef MIRB_BUFFER_H
#define MIRB_BUFFER_H
#include <mruby.h>
/*
* Default sizes for buffer allocation
*/
#define MIRB_BUF_LINE_INIT 128 /* initial line buffer size */
#define MIRB_BUF_LINE_MAX 4096 /* maximum line length */
#define MIRB_BUF_LINES_INIT 8 /* initial number of lines */
#define MIRB_BUF_LINES_MAX 1024 /* maximum number of lines */
#define MIRB_BUF_KILL_SIZE 4096 /* kill buffer size */
/*
* A single line in the buffer
*/
typedef struct mirb_line {
char *data; /* line content (null-terminated) */
size_t len; /* current length (excluding null) */
size_t cap; /* allocated capacity */
} mirb_line;
/*
* Multi-line buffer with cursor tracking
*/
typedef struct mirb_buffer {
mirb_line *lines; /* array of lines */
size_t line_count; /* number of lines */
size_t line_cap; /* allocated line slots */
size_t cursor_line; /* current line (0-indexed) */
size_t cursor_col; /* current column (0-indexed) */
char *kill_buf; /* kill buffer for cut/paste */
size_t kill_len; /* length of kill buffer content */
mrb_bool modified; /* buffer has been modified */
} mirb_buffer;
/*
* Initialize buffer
* Returns TRUE on success
*/
mrb_bool mirb_buffer_init(mirb_buffer *buf);
/*
* Free buffer resources
*/
void mirb_buffer_free(mirb_buffer *buf);
/*
* Clear buffer content (reset to single empty line)
*/
void mirb_buffer_clear(mirb_buffer *buf);
/*
* Get total character count across all lines
*/
size_t mirb_buffer_total_len(mirb_buffer *buf);
/*
* Get buffer content as a single string
* Lines are joined with newlines
* Caller must free the returned string
*/
char *mirb_buffer_to_string(mirb_buffer *buf);
/*
* Set buffer content from string
* String may contain newlines
*/
mrb_bool mirb_buffer_set_string(mirb_buffer *buf, const char *str);
/*
* Insert a character at cursor position
*/
mrb_bool mirb_buffer_insert_char(mirb_buffer *buf, char c);
/*
* Insert a string at cursor position
*/
mrb_bool mirb_buffer_insert_string(mirb_buffer *buf, const char *str, size_t len);
/*
* Delete character before cursor (backspace)
* Returns TRUE if a character was deleted
*/
mrb_bool mirb_buffer_delete_back(mirb_buffer *buf);
/*
* Delete character at cursor (delete key)
* Returns TRUE if a character was deleted
*/
mrb_bool mirb_buffer_delete_forward(mirb_buffer *buf);
/*
* Insert newline at cursor position (split current line)
*/
mrb_bool mirb_buffer_newline(mirb_buffer *buf);
/*
* Cursor movement functions
* Return TRUE if cursor moved
*/
mrb_bool mirb_buffer_cursor_left(mirb_buffer *buf);
mrb_bool mirb_buffer_cursor_right(mirb_buffer *buf);
mrb_bool mirb_buffer_cursor_up(mirb_buffer *buf);
mrb_bool mirb_buffer_cursor_down(mirb_buffer *buf);
/*
* Move cursor to beginning/end of current line
*/
void mirb_buffer_cursor_home(mirb_buffer *buf);
void mirb_buffer_cursor_end(mirb_buffer *buf);
/*
* Move cursor to beginning/end of buffer
*/
void mirb_buffer_cursor_start(mirb_buffer *buf);
void mirb_buffer_cursor_finish(mirb_buffer *buf);
/*
* Word movement (like Emacs Alt+B, Alt+F)
*/
mrb_bool mirb_buffer_cursor_word_back(mirb_buffer *buf);
mrb_bool mirb_buffer_cursor_word_forward(mirb_buffer *buf);
/*
* Kill operations (cut to kill buffer)
* Ctrl+K: kill to end of line
* Ctrl+U: kill to beginning of line
* Ctrl+W: kill word backward
* Alt+D: kill word forward
*/
void mirb_buffer_kill_to_end(mirb_buffer *buf);
void mirb_buffer_kill_to_start(mirb_buffer *buf);
void mirb_buffer_kill_word_back(mirb_buffer *buf);
void mirb_buffer_kill_word_forward(mirb_buffer *buf);
/*
* Yank (paste from kill buffer)
* Ctrl+Y
*/
mrb_bool mirb_buffer_yank(mirb_buffer *buf);
/*
* Get current line content
*/
const char *mirb_buffer_current_line(mirb_buffer *buf);
/*
* Get line at index
*/
const char *mirb_buffer_line_at(mirb_buffer *buf, size_t index);
/*
* Get length of line at index
*/
size_t mirb_buffer_line_len(mirb_buffer *buf, size_t index);
#endif /* MIRB_BUFFER_H */
@@ -1,650 +0,0 @@
/*
** 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
* ============================================================ */
/* Check if receiver expression is simple (just a name, no method calls) */
static mrb_bool
is_simple_receiver(const char *expr)
{
int i;
/* Empty is not simple */
if (!expr || expr[0] == '\0') return FALSE;
/* Check if it contains only alphanumeric, underscore, or scope resolution */
for (i = 0; expr[i]; i++) {
char c = expr[i];
if (!(ISALNUM(c) || c == '_' || c == ':')) {
return FALSE; /* Contains operators, parentheses, etc. */
}
}
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
@@ -1,121 +0,0 @@
/*
** 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
#endif /* MIRB_COMPLETION_H */
@@ -0,0 +1,607 @@
/*
** mirb_editor.c - Multi-line editor for mirb
**
** See Copyright Notice in mruby.h
*/
#include "mirb_editor.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ANSI color codes */
#define COLOR_GREEN "\033[32m"
#define COLOR_RESET "\033[0m"
/*
* Check if line contains only whitespace before given column
*/
static mrb_bool
line_is_blank_before(const char *line, size_t col)
{
for (size_t i = 0; i < col; i++) {
if (line[i] != ' ' && line[i] != '\t') {
return FALSE;
}
}
return TRUE;
}
/*
* Get leading whitespace count on current line
*/
static size_t
leading_spaces(const char *line)
{
size_t count = 0;
while (line[count] == ' ' || line[count] == '\t') {
count++;
}
return count;
}
/*
* Calculate indent level by counting open blocks in code
*/
static int
calc_indent_level(const char *code)
{
int level = 0;
const char *p = code;
int at_line_start = 1;
while (*p) {
/* Skip strings */
if (*p == '"' || *p == '\'') {
char quote = *p++;
while (*p && *p != quote) {
if (*p == '\\' && p[1]) p++;
p++;
}
if (*p) p++;
at_line_start = 0;
continue;
}
/* Skip comments */
if (*p == '#') {
while (*p && *p != '\n') p++;
continue;
}
/* Track line starts for keyword detection */
if (*p == '\n') {
at_line_start = 1;
p++;
continue;
}
/* Skip whitespace but don't change at_line_start yet */
if (*p == ' ' || *p == '\t') {
p++;
continue;
}
/* Check for block-opening keywords at word boundary */
if (at_line_start || (p > code && !((p[-1] >= 'a' && p[-1] <= 'z') ||
(p[-1] >= 'A' && p[-1] <= 'Z') ||
(p[-1] >= '0' && p[-1] <= '9') ||
p[-1] == '_'))) {
/* Check block-opening keywords */
if ((strncmp(p, "def ", 4) == 0) ||
(strncmp(p, "class ", 6) == 0) ||
(strncmp(p, "module ", 7) == 0) ||
(strncmp(p, "if ", 3) == 0) ||
(strncmp(p, "unless ", 7) == 0) ||
(strncmp(p, "case ", 5) == 0) ||
(strncmp(p, "while ", 6) == 0) ||
(strncmp(p, "until ", 6) == 0) ||
(strncmp(p, "for ", 4) == 0) ||
(strncmp(p, "begin", 5) == 0 && (p[5] == '\0' || p[5] == '\n' || p[5] == ' ' || p[5] == '#')) ||
(strncmp(p, "do", 2) == 0 && (p[2] == '\0' || p[2] == '\n' || p[2] == ' ' || p[2] == '#' || p[2] == '|'))) {
level++;
}
/* Check block-closing keyword */
else if (strncmp(p, "end", 3) == 0 &&
(p[3] == '\0' || p[3] == '\n' || p[3] == ' ' || p[3] == '#' || p[3] == '.' || p[3] == ')')) {
if (level > 0) level--;
}
}
/* Check for block opening/closing with braces */
if (*p == '{') {
level++;
}
else if (*p == '}') {
if (level > 0) level--;
}
at_line_start = 0;
p++;
}
return level;
}
/*
* Check if we should dedent after typing a character
* Returns TRUE if current line starts with 'end' or '}' after only whitespace
*/
static mrb_bool
should_dedent(mirb_buffer *buf, char last_char)
{
const char *line = mirb_buffer_current_line(buf);
size_t col = buf->cursor_col;
/* Check for '}' - dedent immediately when typed at line start */
if (last_char == '}') {
if (col == 1 || (col > 1 && line_is_blank_before(line, col - 1))) {
return TRUE;
}
}
/* Check for 'end' - dedent when 'd' completes "end" */
if (last_char == 'd' && col >= 3) {
/* Check if we just completed "end" */
if (line[col - 3] == 'e' && line[col - 2] == 'n' && line[col - 1] == 'd') {
/* Verify only whitespace before "end" */
if (col == 3 || line_is_blank_before(line, col - 3)) {
/* Verify "end" is not part of a longer word */
if (col == buf->lines[buf->cursor_line].len ||
line[col] == ' ' || line[col] == '\t' || line[col] == '\0' ||
line[col] == '\n' || line[col] == '.' || line[col] == ')') {
return TRUE;
}
}
}
}
return FALSE;
}
/*
* Perform dedentation - remove one level (2 spaces) of leading whitespace
*/
static void
perform_dedent(mirb_buffer *buf)
{
const char *line = mirb_buffer_current_line(buf);
size_t spaces = leading_spaces(line);
/* Remove up to 2 spaces */
size_t to_remove = (spaces >= 2) ? 2 : spaces;
if (to_remove > 0) {
size_t saved_col = buf->cursor_col;
/* Move cursor to start of line and delete leading spaces */
buf->cursor_col = 0;
for (size_t i = 0; i < to_remove; i++) {
mirb_buffer_delete_forward(buf);
}
/* Restore cursor position, adjusted for removed spaces */
buf->cursor_col = (saved_col > to_remove) ? (saved_col - to_remove) : 0;
}
}
/*
* Initialize editor
*/
mrb_bool
mirb_editor_init(mirb_editor *ed)
{
memset(ed, 0, sizeof(*ed));
if (!mirb_term_init(&ed->term)) {
/* Terminal init may fail but we can still work in simple mode */
}
if (!mirb_buffer_init(&ed->buf)) {
mirb_term_cleanup(&ed->term);
return FALSE;
}
ed->prompt = "> ";
ed->prompt_cont = "* ";
ed->prompt_len = 2;
ed->prompt_cont_len = 2;
ed->use_color = FALSE;
ed->initialized = TRUE;
return TRUE;
}
/*
* Cleanup editor
*/
void
mirb_editor_cleanup(mirb_editor *ed)
{
if (!ed->initialized) return;
mirb_buffer_free(&ed->buf);
mirb_term_cleanup(&ed->term);
ed->initialized = FALSE;
}
/*
* Set prompts
*/
void
mirb_editor_set_prompts(mirb_editor *ed, const char *prompt, const char *prompt_cont)
{
ed->prompt = prompt;
ed->prompt_cont = prompt_cont;
ed->prompt_len = strlen(prompt);
ed->prompt_cont_len = strlen(prompt_cont);
}
/*
* Set completion checker
*/
void
mirb_editor_set_check_complete(mirb_editor *ed, mirb_check_complete_fn *fn, void *user_data)
{
ed->check_complete = fn;
ed->check_complete_data = user_data;
}
/*
* Enable/disable color
*/
void
mirb_editor_set_color(mirb_editor *ed, mrb_bool enable)
{
ed->use_color = enable;
}
/*
* Check if multi-line editing is supported
*/
mrb_bool
mirb_editor_supported(mirb_editor *ed)
{
return ed->term.supported;
}
/*
* Print prompt for given line
*/
static void
print_prompt(mirb_editor *ed, size_t line_idx)
{
const char *p = (line_idx == 0) ? ed->prompt : ed->prompt_cont;
if (ed->use_color) {
printf("%s%s%s", COLOR_GREEN, p, COLOR_RESET);
}
else {
printf("%s", p);
}
}
/*
* Refresh display - uses natural terminal scrolling like irb
*
* Strategy:
* - Track which screen row we started on
* - Move cursor back to start, clear everything below, redraw all lines
* - This allows terminal to scroll naturally without corrupting history
*/
static void
refresh_display(mirb_editor *ed)
{
size_t prompt_len;
size_t lines_to_go_up;
/* Calculate how many lines up we need to go to reach start of input */
/* We're currently on cursor_line, and prev_line_count tells us total displayed */
if (ed->prev_line_count > 0) {
/* Go up from current position to first line of input */
lines_to_go_up = ed->display_cursor_row;
if (lines_to_go_up > 0) {
mirb_term_cursor_up((int)lines_to_go_up);
}
}
/* Move to column 1 and clear from here to end of screen */
mirb_term_cursor_col(1);
mirb_term_clear_below();
/* Redraw all lines */
for (size_t i = 0; i < ed->buf.line_count; i++) {
print_prompt(ed, i);
printf("%s", mirb_buffer_line_at(&ed->buf, i));
if (i < ed->buf.line_count - 1) {
printf("\r\n");
}
}
/* Now position cursor correctly */
/* We're at the end of last line, need to go to cursor position */
size_t lines_up_from_end = ed->buf.line_count - 1 - ed->buf.cursor_line;
if (lines_up_from_end > 0) {
mirb_term_cursor_up((int)lines_up_from_end);
}
/* Position column on cursor line */
prompt_len = (ed->buf.cursor_line == 0) ? ed->prompt_len : ed->prompt_cont_len;
mirb_term_cursor_col((int)(prompt_len + ed->buf.cursor_col + 1));
/* Update tracking */
ed->prev_line_count = ed->buf.line_count;
ed->display_cursor_row = ed->buf.cursor_line;
mirb_term_flush();
}
/*
* Handle a keypress
* Returns TRUE to continue editing, FALSE to finish
*/
static mrb_bool
handle_key(mirb_editor *ed, int key, mirb_edit_result *result)
{
switch (key) {
case MIRB_KEY_ENTER:
/* Check if input is complete */
if (ed->check_complete) {
char *code = mirb_buffer_to_string(&ed->buf);
if (code) {
mrb_bool complete = ed->check_complete(code, ed->check_complete_data);
if (!complete) {
/* Calculate indent level before adding newline */
int 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, ' ');
}
return TRUE;
}
free(code);
}
}
*result = MIRB_EDIT_OK;
return FALSE;
case MIRB_KEY_CTRL_C:
*result = MIRB_EDIT_INTERRUPT;
return FALSE;
case MIRB_KEY_CTRL_D:
if (mirb_buffer_total_len(&ed->buf) == 0) {
*result = MIRB_EDIT_EOF;
return FALSE;
}
/* Delete forward if not empty */
mirb_buffer_delete_forward(&ed->buf);
return TRUE;
case MIRB_KEY_BACKSPACE:
mirb_buffer_delete_back(&ed->buf);
return TRUE;
case MIRB_KEY_DELETE:
mirb_buffer_delete_forward(&ed->buf);
return TRUE;
case MIRB_KEY_LEFT:
case MIRB_KEY_CTRL_B:
mirb_buffer_cursor_left(&ed->buf);
return TRUE;
case MIRB_KEY_RIGHT:
case MIRB_KEY_CTRL_F:
mirb_buffer_cursor_right(&ed->buf);
return TRUE;
case MIRB_KEY_UP:
case MIRB_KEY_CTRL_P:
mirb_buffer_cursor_up(&ed->buf);
return TRUE;
case MIRB_KEY_DOWN:
case MIRB_KEY_CTRL_N:
mirb_buffer_cursor_down(&ed->buf);
return TRUE;
case MIRB_KEY_HOME:
case MIRB_KEY_CTRL_A:
mirb_buffer_cursor_home(&ed->buf);
return TRUE;
case MIRB_KEY_END:
case MIRB_KEY_CTRL_E:
mirb_buffer_cursor_end(&ed->buf);
return TRUE;
case MIRB_KEY_CTRL_K:
mirb_buffer_kill_to_end(&ed->buf);
return TRUE;
case MIRB_KEY_CTRL_U:
mirb_buffer_kill_to_start(&ed->buf);
return TRUE;
case MIRB_KEY_CTRL_W:
mirb_buffer_kill_word_back(&ed->buf);
return TRUE;
case MIRB_KEY_CTRL_Y:
mirb_buffer_yank(&ed->buf);
return TRUE;
case MIRB_KEY_ALT_B:
mirb_buffer_cursor_word_back(&ed->buf);
return TRUE;
case MIRB_KEY_ALT_F:
mirb_buffer_cursor_word_forward(&ed->buf);
return TRUE;
case MIRB_KEY_ALT_D:
mirb_buffer_kill_word_forward(&ed->buf);
return TRUE;
case MIRB_KEY_CTRL_L:
/* Clear screen and refresh */
mirb_term_clear_screen();
ed->prev_line_count = 0;
return TRUE;
default:
/* Insert printable characters */
if (key >= 32 && key < 127) {
mirb_buffer_insert_char(&ed->buf, (char)key);
/* Check for auto-dedent after typing 'end' or '}' */
if (should_dedent(&ed->buf, (char)key)) {
perform_dedent(&ed->buf);
}
}
return TRUE;
}
}
/*
* Read input with multi-line editing
*/
mirb_edit_result
mirb_editor_read(mirb_editor *ed, char **out_str)
{
mirb_edit_result result;
int key;
*out_str = NULL;
/* Fall back to simple mode if raw mode not supported */
if (!ed->term.supported) {
return mirb_editor_read_simple(ed, out_str);
}
/* Clear buffer for new input */
mirb_buffer_clear(&ed->buf);
ed->prev_line_count = 0;
ed->display_cursor_row = 0;
/* Enable raw mode */
if (!mirb_term_raw_enable(&ed->term)) {
return mirb_editor_read_simple(ed, out_str);
}
/* Initial display */
print_prompt(ed, 0);
mirb_term_flush();
ed->prev_line_count = 1;
ed->display_cursor_row = 0;
/* Main editing loop */
result = MIRB_EDIT_ERROR;
while (1) {
key = mirb_term_read_key(&ed->term);
if (key == MIRB_KEY_NONE) {
result = MIRB_EDIT_ERROR;
break;
}
if (!handle_key(ed, key, &result)) {
break;
}
refresh_display(ed);
}
/* Disable raw mode */
mirb_term_raw_disable(&ed->term);
/* Move to end and print newline */
if (ed->buf.cursor_line < ed->buf.line_count - 1) {
mirb_term_cursor_down((int)(ed->buf.line_count - 1 - ed->buf.cursor_line));
}
printf("\n");
/* Return result string */
if (result == MIRB_EDIT_OK) {
*out_str = mirb_buffer_to_string(&ed->buf);
if (*out_str == NULL) {
result = MIRB_EDIT_ERROR;
}
}
return result;
}
/*
* Simple single-line input (fallback)
*/
mirb_edit_result
mirb_editor_read_simple(mirb_editor *ed, char **out_str)
{
char line[4096];
size_t total_len = 0;
size_t total_cap = 4096;
char *total = (char*)malloc(total_cap);
mrb_bool first_line = TRUE;
*out_str = NULL;
if (total == NULL) return MIRB_EDIT_ERROR;
total[0] = '\0';
while (1) {
/* Print prompt */
print_prompt(ed, first_line ? 0 : 1);
fflush(stdout);
/* Read line */
if (fgets(line, sizeof(line), stdin) == NULL) {
if (total_len == 0) {
free(total);
return MIRB_EDIT_EOF;
}
break;
}
/* Remove trailing newline */
size_t len = strlen(line);
if (len > 0 && line[len - 1] == '\n') {
line[--len] = '\0';
}
/* Append to total */
if (!first_line) {
/* Add newline separator */
if (total_len + 1 >= total_cap) {
total_cap *= 2;
char *new_total = (char*)realloc(total, total_cap);
if (new_total == NULL) {
free(total);
return MIRB_EDIT_ERROR;
}
total = new_total;
}
total[total_len++] = '\n';
}
if (total_len + len >= total_cap) {
total_cap *= 2;
char *new_total = (char*)realloc(total, total_cap);
if (new_total == NULL) {
free(total);
return MIRB_EDIT_ERROR;
}
total = new_total;
}
memcpy(total + total_len, line, len + 1);
total_len += len;
first_line = FALSE;
/* Check if complete */
if (ed->check_complete) {
if (ed->check_complete(total, ed->check_complete_data)) {
break;
}
}
else {
break; /* No checker, single line mode */
}
}
*out_str = total;
return MIRB_EDIT_OK;
}
@@ -0,0 +1,102 @@
/*
** mirb_editor.h - Multi-line editor for mirb
**
** See Copyright Notice in mruby.h
*/
#ifndef MIRB_EDITOR_H
#define MIRB_EDITOR_H
#include <mruby.h>
#include "mirb_term.h"
#include "mirb_buffer.h"
/*
* Editor result codes
*/
typedef enum mirb_edit_result {
MIRB_EDIT_OK = 0, /* Input ready (Enter pressed) */
MIRB_EDIT_CONTINUE, /* Need more input (multi-line) */
MIRB_EDIT_EOF, /* End of file (Ctrl+D on empty) */
MIRB_EDIT_INTERRUPT, /* Interrupted (Ctrl+C) */
MIRB_EDIT_ERROR /* Error occurred */
} mirb_edit_result;
/*
* Callback to check if input is complete
* Returns TRUE if the code is syntactically complete
*/
typedef mrb_bool mirb_check_complete_fn(const char *code, void *user_data);
/*
* Editor state
*/
typedef struct mirb_editor {
mirb_term term; /* terminal state */
mirb_buffer buf; /* editing buffer */
const char *prompt; /* primary prompt (e.g., "> ") */
const char *prompt_cont; /* continuation prompt (e.g., "* ") */
size_t prompt_len; /* length of primary prompt */
size_t prompt_cont_len; /* length of continuation prompt */
mirb_check_complete_fn *check_complete; /* completion checker */
void *check_complete_data; /* user data for checker */
size_t display_cursor_row; /* cursor row in buffer (for refresh tracking) */
size_t prev_line_count; /* line count from last refresh */
mrb_bool initialized; /* editor is initialized */
mrb_bool use_color; /* use colored output */
} mirb_editor;
/*
* Initialize editor
* Returns TRUE on success
*/
mrb_bool mirb_editor_init(mirb_editor *ed);
/*
* Cleanup editor
*/
void mirb_editor_cleanup(mirb_editor *ed);
/*
* Set prompts
*/
void mirb_editor_set_prompts(mirb_editor *ed,
const char *prompt,
const char *prompt_cont);
/*
* Set completion checker callback
*/
void mirb_editor_set_check_complete(mirb_editor *ed,
mirb_check_complete_fn *fn,
void *user_data);
/*
* Enable or disable colored output
*/
void mirb_editor_set_color(mirb_editor *ed, mrb_bool enable);
/*
* Check if multi-line editing is supported
*/
mrb_bool mirb_editor_supported(mirb_editor *ed);
/*
* Read input with multi-line editing
*
* Returns result code (OK, EOF, INTERRUPT, ERROR)
* On success (OK), caller must free the returned string
*/
mirb_edit_result mirb_editor_read(mirb_editor *ed, char **out_str);
/*
* Simple single-line input (fallback when raw mode not supported)
* Used internally but can be called directly
*/
mirb_edit_result mirb_editor_read_simple(mirb_editor *ed, char **out_str);
#endif /* MIRB_EDITOR_H */
@@ -0,0 +1,361 @@
/*
** mirb_term.c - Terminal control for mirb multi-line editor
**
** See Copyright Notice in mruby.h
*/
#include "mirb_term.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if !defined(_WIN32) && !defined(_WIN64)
#include <unistd.h>
#include <termios.h>
#include <sys/ioctl.h>
#include <errno.h>
/*
* Initialize terminal state
*/
mrb_bool
mirb_term_init(mirb_term *term)
{
memset(term, 0, sizeof(*term));
/* Check if stdin/stdout are terminals */
if (!isatty(STDIN_FILENO) || !isatty(STDOUT_FILENO)) {
term->supported = FALSE;
return FALSE;
}
term->supported = TRUE;
term->orig_termios = malloc(sizeof(struct termios));
if (term->orig_termios == NULL) {
term->supported = FALSE;
return FALSE;
}
mirb_term_get_size(term);
return TRUE;
}
/*
* Cleanup terminal state
*/
void
mirb_term_cleanup(mirb_term *term)
{
if (term->raw_mode) {
mirb_term_raw_disable(term);
}
free(term->orig_termios);
term->orig_termios = NULL;
}
/*
* Enable raw mode
*/
mrb_bool
mirb_term_raw_enable(mirb_term *term)
{
struct termios raw;
if (!term->supported) return FALSE;
if (term->raw_mode) return TRUE;
/* Save original settings */
if (tcgetattr(STDIN_FILENO, (struct termios*)term->orig_termios) == -1) {
return FALSE;
}
raw = *(struct termios*)term->orig_termios;
/*
* Input flags: disable break signal, CR to NL conversion,
* parity checking, strip high bit, and software flow control
*/
raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
/* Output flags: disable post-processing */
raw.c_oflag &= ~(OPOST);
/* Control flags: set 8-bit characters */
raw.c_cflag |= (CS8);
/*
* Local flags: disable echo, canonical mode,
* extended input processing, and signal generation
*/
raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
/* Control characters: return immediately with any available input */
raw.c_cc[VMIN] = 1;
raw.c_cc[VTIME] = 0;
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1) {
return FALSE;
}
term->raw_mode = TRUE;
return TRUE;
}
/*
* Disable raw mode
*/
void
mirb_term_raw_disable(mirb_term *term)
{
if (term->raw_mode && term->orig_termios) {
tcsetattr(STDIN_FILENO, TCSAFLUSH, (struct termios*)term->orig_termios);
term->raw_mode = FALSE;
}
}
/*
* Read a single key, handling escape sequences
*/
int
mirb_term_read_key(mirb_term *term)
{
unsigned char c;
ssize_t nread;
(void)term; /* unused in POSIX implementation */
/* Read first character */
while ((nread = read(STDIN_FILENO, &c, 1)) != 1) {
if (nread == -1 && errno != EAGAIN && errno != EINTR) {
return MIRB_KEY_NONE;
}
}
/* Handle escape sequences */
if (c == 27) {
unsigned char seq[3];
fd_set fds;
struct timeval tv;
/* Use select to check if more characters are available */
FD_ZERO(&fds);
FD_SET(STDIN_FILENO, &fds);
tv.tv_sec = 0;
tv.tv_usec = 50000; /* 50ms timeout */
if (select(STDIN_FILENO + 1, &fds, NULL, NULL, &tv) <= 0) {
return MIRB_KEY_ESC; /* Just ESC key */
}
if (read(STDIN_FILENO, &seq[0], 1) != 1) return MIRB_KEY_ESC;
/* Alt+key combinations (ESC followed by letter) */
if (seq[0] >= 'a' && seq[0] <= 'z') {
switch (seq[0]) {
case 'b': return MIRB_KEY_ALT_B;
case 'f': return MIRB_KEY_ALT_F;
case 'd': return MIRB_KEY_ALT_D;
default: return MIRB_KEY_ESC;
}
}
/* CSI sequences: ESC [ ... */
if (seq[0] == '[') {
if (read(STDIN_FILENO, &seq[1], 1) != 1) return MIRB_KEY_ESC;
/* Numeric sequences: ESC [ N ~ */
if (seq[1] >= '0' && seq[1] <= '9') {
if (read(STDIN_FILENO, &seq[2], 1) != 1) return MIRB_KEY_ESC;
if (seq[2] == '~') {
switch (seq[1]) {
case '1': return MIRB_KEY_HOME;
case '3': return MIRB_KEY_DELETE;
case '4': return MIRB_KEY_END;
case '7': return MIRB_KEY_HOME;
case '8': return MIRB_KEY_END;
}
}
return MIRB_KEY_ESC;
}
/* Letter sequences: ESC [ A/B/C/D/H/F */
switch (seq[1]) {
case 'A': return MIRB_KEY_UP;
case 'B': return MIRB_KEY_DOWN;
case 'C': return MIRB_KEY_RIGHT;
case 'D': return MIRB_KEY_LEFT;
case 'H': return MIRB_KEY_HOME;
case 'F': return MIRB_KEY_END;
}
return MIRB_KEY_ESC;
}
/* SS3 sequences: ESC O ... */
if (seq[0] == 'O') {
if (read(STDIN_FILENO, &seq[1], 1) != 1) return MIRB_KEY_ESC;
switch (seq[1]) {
case 'A': return MIRB_KEY_UP;
case 'B': return MIRB_KEY_DOWN;
case 'C': return MIRB_KEY_RIGHT;
case 'D': return MIRB_KEY_LEFT;
case 'H': return MIRB_KEY_HOME;
case 'F': return MIRB_KEY_END;
}
return MIRB_KEY_ESC;
}
return MIRB_KEY_ESC;
}
/* Handle Ctrl+H as backspace (some terminals send this) */
if (c == 8) return MIRB_KEY_BACKSPACE;
return (int)c;
}
/*
* Get terminal size
*/
void
mirb_term_get_size(mirb_term *term)
{
struct winsize ws;
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) {
term->cols = ws.ws_col;
term->rows = ws.ws_row;
}
else {
/* Default fallback */
term->cols = 80;
term->rows = 24;
}
}
#else /* Windows */
/*
* Windows implementation (minimal stub)
* Full Windows console support would require significant additional code
*/
mrb_bool
mirb_term_init(mirb_term *term)
{
memset(term, 0, sizeof(*term));
term->supported = FALSE; /* Not implemented for Windows yet */
term->cols = 80;
term->rows = 24;
return FALSE;
}
void
mirb_term_cleanup(mirb_term *term)
{
(void)term;
}
mrb_bool
mirb_term_raw_enable(mirb_term *term)
{
(void)term;
return FALSE;
}
void
mirb_term_raw_disable(mirb_term *term)
{
(void)term;
}
int
mirb_term_read_key(mirb_term *term)
{
(void)term;
return MIRB_KEY_NONE;
}
void
mirb_term_get_size(mirb_term *term)
{
term->cols = 80;
term->rows = 24;
}
#endif /* _WIN32 */
/*
* ANSI escape sequence functions (platform-independent)
*/
void
mirb_term_cursor_up(int n)
{
if (n > 0) printf("\033[%dA", n);
}
void
mirb_term_cursor_down(int n)
{
if (n > 0) printf("\033[%dB", n);
}
void
mirb_term_cursor_right(int n)
{
if (n > 0) printf("\033[%dC", n);
}
void
mirb_term_cursor_left(int n)
{
if (n > 0) printf("\033[%dD", n);
}
void
mirb_term_cursor_col(int col)
{
printf("\033[%dG", col);
}
void
mirb_term_clear_line(void)
{
printf("\033[2K");
}
void
mirb_term_clear_to_end(void)
{
printf("\033[K");
}
void
mirb_term_clear_screen(void)
{
printf("\033[2J\033[H");
}
void
mirb_term_flush(void)
{
fflush(stdout);
}
void
mirb_term_clear_below(void)
{
printf("\033[J");
}
void
mirb_term_save_cursor(void)
{
printf("\033[s");
}
void
mirb_term_restore_cursor(void)
{
printf("\033[u");
}
@@ -0,0 +1,126 @@
/*
** mirb_term.h - Terminal control for mirb multi-line editor
**
** See Copyright Notice in mruby.h
*/
#ifndef MIRB_TERM_H
#define MIRB_TERM_H
#include <mruby.h>
/*
* Key codes for mirb editor
* Values > 255 are used for special keys to avoid collision with ASCII
*/
enum mirb_key {
MIRB_KEY_NONE = 0,
/* Control characters (ASCII values) */
MIRB_KEY_CTRL_A = 1,
MIRB_KEY_CTRL_B = 2,
MIRB_KEY_CTRL_C = 3,
MIRB_KEY_CTRL_D = 4,
MIRB_KEY_CTRL_E = 5,
MIRB_KEY_CTRL_F = 6,
MIRB_KEY_CTRL_K = 11,
MIRB_KEY_CTRL_L = 12,
MIRB_KEY_ENTER = 13,
MIRB_KEY_CTRL_N = 14,
MIRB_KEY_CTRL_P = 16,
MIRB_KEY_CTRL_U = 21,
MIRB_KEY_CTRL_W = 23,
MIRB_KEY_CTRL_Y = 25,
MIRB_KEY_ESC = 27,
MIRB_KEY_BACKSPACE = 127,
/* Special keys (escape sequences mapped to values > 255) */
MIRB_KEY_UP = 256,
MIRB_KEY_DOWN = 257,
MIRB_KEY_RIGHT = 258,
MIRB_KEY_LEFT = 259,
MIRB_KEY_HOME = 260,
MIRB_KEY_END = 261,
MIRB_KEY_DELETE = 262,
/* Alt/Meta key combinations */
MIRB_KEY_ALT_B = 300,
MIRB_KEY_ALT_F = 301,
MIRB_KEY_ALT_D = 302
};
/*
* Terminal state structure
*/
typedef struct mirb_term {
mrb_bool raw_mode; /* TRUE if terminal is in raw mode */
mrb_bool supported; /* TRUE if raw mode is supported */
int cols; /* terminal width in columns */
int rows; /* terminal height in rows */
#if !defined(_WIN32) && !defined(_WIN64)
void *orig_termios; /* original terminal settings (struct termios*) */
#endif
} mirb_term;
/*
* Initialize terminal state
* Returns TRUE if terminal operations are supported
*/
mrb_bool mirb_term_init(mirb_term *term);
/*
* Cleanup terminal state and restore original settings
*/
void mirb_term_cleanup(mirb_term *term);
/*
* Enable raw mode for character-by-character input
* Returns TRUE on success
*/
mrb_bool mirb_term_raw_enable(mirb_term *term);
/*
* Disable raw mode and restore normal terminal operation
*/
void mirb_term_raw_disable(mirb_term *term);
/*
* Read a single key (handles escape sequences)
* Returns key code from mirb_key enum or ASCII value
*/
int mirb_term_read_key(mirb_term *term);
/*
* Cursor movement functions (ANSI escape sequences)
*/
void mirb_term_cursor_up(int n);
void mirb_term_cursor_down(int n);
void mirb_term_cursor_right(int n);
void mirb_term_cursor_left(int n);
void mirb_term_cursor_col(int col); /* move to column (1-based) */
/*
* Line and screen control
*/
void mirb_term_clear_line(void); /* clear entire current line */
void mirb_term_clear_to_end(void); /* clear from cursor to end of line */
void mirb_term_clear_screen(void); /* clear entire screen */
void mirb_term_clear_below(void); /* clear from cursor to end of screen */
/*
* Cursor position save/restore
*/
void mirb_term_save_cursor(void);
void mirb_term_restore_cursor(void);
/*
* Update terminal size information
*/
void mirb_term_get_size(mirb_term *term);
/*
* Flush output buffer
*/
void mirb_term_flush(void);
#endif /* MIRB_TERM_H */