From 527018cf07a80c83d2862a801b4dae6bea79644e Mon Sep 17 00:00:00 2001 From: "Yukihiro \"Matz\" Matsumoto" Date: Wed, 10 Dec 2025 16:30:18 +0900 Subject: [PATCH] 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 --- mrbgems/mruby-bin-mirb/mrbgem.rake | 133 --- mrbgems/mruby-bin-mirb/tools/mirb/mirb.c | 526 +++--------- .../mruby-bin-mirb/tools/mirb/mirb_buffer.c | 780 ++++++++++++++++++ .../mruby-bin-mirb/tools/mirb/mirb_buffer.h | 168 ++++ .../tools/mirb/mirb_completion.c | 650 --------------- .../tools/mirb/mirb_completion.h | 121 --- .../mruby-bin-mirb/tools/mirb/mirb_editor.c | 607 ++++++++++++++ .../mruby-bin-mirb/tools/mirb/mirb_editor.h | 102 +++ mrbgems/mruby-bin-mirb/tools/mirb/mirb_term.c | 361 ++++++++ mrbgems/mruby-bin-mirb/tools/mirb/mirb_term.h | 126 +++ 10 files changed, 2271 insertions(+), 1303 deletions(-) create mode 100644 mrbgems/mruby-bin-mirb/tools/mirb/mirb_buffer.c create mode 100644 mrbgems/mruby-bin-mirb/tools/mirb/mirb_buffer.h delete mode 100644 mrbgems/mruby-bin-mirb/tools/mirb/mirb_completion.c delete mode 100644 mrbgems/mruby-bin-mirb/tools/mirb/mirb_completion.h create mode 100644 mrbgems/mruby-bin-mirb/tools/mirb/mirb_editor.c create mode 100644 mrbgems/mruby-bin-mirb/tools/mirb/mirb_editor.h create mode 100644 mrbgems/mruby-bin-mirb/tools/mirb/mirb_term.c create mode 100644 mrbgems/mruby-bin-mirb/tools/mirb/mirb_term.h diff --git a/mrbgems/mruby-bin-mirb/mrbgem.rake b/mrbgems/mruby-bin-mirb/mrbgem.rake index 49717d9ae..6842e6f29 100644 --- a/mrbgems/mruby-bin-mirb/mrbgem.rake +++ b/mrbgems/mruby-bin-mirb/mrbgem.rake @@ -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=''" - spec.cc.defines << "MRB_READLINE_HISTORY=''" - 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=''" - spec.cc.defines << "MRB_READLINE_HISTORY=''" - 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=''" - spec.cc.defines << "MRB_READLINE_HISTORY=''" - 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=''" - spec.cc.defines << "MRB_READLINE_HISTORY=''" - 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 diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb.c b/mrbgems/mruby-bin-mirb/tools/mirb/mirb.c index 2ff45a011..ffa102a29 100644 --- a/mrbgems/mruby-bin-mirb/tools/mirb/mirb.c +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb.c @@ -27,7 +27,6 @@ #include #include -#include #ifdef _WIN32 #include @@ -36,256 +35,19 @@ #include #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 -#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); diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_buffer.c b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_buffer.c new file mode 100644 index 000000000..b7ab38cc9 --- /dev/null +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_buffer.c @@ -0,0 +1,780 @@ +/* +** mirb_buffer.c - Multi-line buffer for mirb editor +** +** See Copyright Notice in mruby.h +*/ + +#include "mirb_buffer.h" +#include +#include +#include +#include + +/* + * 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; +} diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_buffer.h b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_buffer.h new file mode 100644 index 000000000..53a6b32ad --- /dev/null +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_buffer.h @@ -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 + +/* + * 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 */ diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_completion.c b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_completion.c deleted file mode 100644 index 5fbcd3ed6..000000000 --- a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_completion.c +++ /dev/null @@ -1,650 +0,0 @@ -/* -** mirb_completion.c - Tab completion support for mirb -** -** See Copyright Notice in mruby.h -*/ - -#include "mirb_completion.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#ifdef MRB_USE_READLINE -#ifndef MRB_USE_LINENOISE -#include MRB_READLINE_HEADER -#endif -#endif - -#ifdef MRB_USE_LINENOISE -#include -#endif - -/* Helper macros */ -#ifndef ISSPACE -#define ISSPACE(c) isspace((unsigned char)(c)) -#endif -#ifndef ISALNUM -#define ISALNUM(c) isalnum((unsigned char)(c)) -#endif - -/* Ruby keywords */ -static const char *ruby_keywords[] = { - "BEGIN", "END", "__ENCODING__", "__FILE__", "__LINE__", - "alias", "and", "begin", "break", "case", "class", "def", - "defined?", "do", "else", "elsif", "end", "ensure", "false", - "for", "if", "in", "module", "next", "nil", "not", "or", - "redo", "rescue", "retry", "return", "self", "super", "then", - "true", "undef", "unless", "until", "when", "while", "yield", - NULL -}; - -/* ============================================================ - * Core Completion Engine - * ============================================================ */ - -void -mirb_completion_init(mirb_completion_ctx *ctx, mrb_state *mrb, mrb_ccontext *cxt) -{ - memset(ctx, 0, sizeof(*ctx)); - ctx->mrb = mrb; - ctx->cxt = cxt; -} - -void -mirb_completion_free(mirb_completion_ctx *ctx) -{ - int i; - - /* Free match prefix */ - if (ctx->match_prefix) { - free(ctx->match_prefix); - ctx->match_prefix = NULL; - } - - /* Free completions */ - if (ctx->completions) { - for (i = 0; i < ctx->completion_count; i++) { - free(ctx->completions[i]); - } - free(ctx->completions); - ctx->completions = NULL; - } - - ctx->completion_count = 0; - ctx->completion_alloc = 0; - ctx->current_index = 0; -} - -/* ============================================================ - * Context Analysis - * ============================================================ */ - -mirb_completion_type -mirb_detect_completion_type(const char *line, int cursor_pos) -{ - int i; - - /* 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 diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_completion.h b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_completion.h deleted file mode 100644 index 63ecbb3a4..000000000 --- a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_completion.h +++ /dev/null @@ -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 -#include - -/** - * @file mirb_completion.h - * - * Tab completion support for mirb. - * - * Architecture: - * - Core engine is library-agnostic - * - Adapters for readline/libedit and linenoise - * - Context detection based on input line analysis - * - Safe evaluation of receiver expressions - * - * Completion Types: - * - COMPLETION_METHOD: After dot operator - * - COMPLETION_KEYWORD: Ruby keywords - * - COMPLETION_LOCAL_VAR: Variables in scope - * - COMPLETION_GLOBAL_VAR: $variables - * - COMPLETION_CONSTANT: Constants and classes - * - COMPLETION_FILE: File paths (optional) - * - * Performance: - * - Completions generated on-demand - * - Results cached per tab press - * - Safe evaluation with exception handling - */ - -/* Completion types */ -typedef enum { - COMPLETION_METHOD, /* Object methods */ - COMPLETION_KEYWORD, /* Ruby keywords */ - COMPLETION_GLOBAL_VAR, /* $global */ - COMPLETION_LOCAL_VAR, /* local_var */ - COMPLETION_CONSTANT, /* CONSTANT or Class */ - COMPLETION_FILE, /* File paths */ -} mirb_completion_type; - -/* Completion context - shared state */ -typedef struct mirb_completion_ctx { - mrb_state *mrb; /* mruby VM state */ - mrb_ccontext *cxt; /* Compiler context for locals */ - const char *line_buf; /* Current input line */ - int cursor_pos; /* Cursor position in line */ - char *match_prefix; /* Text to match against */ - int prefix_len; /* Length of prefix */ - - /* Completion results */ - char **completions; /* Array of completion strings */ - int completion_count; /* Number of completions */ - int completion_alloc; /* Allocated size */ - int current_index; /* For generator pattern (readline) */ -} mirb_completion_ctx; - -/* Core Completion Engine Interface */ - -/* Initialize completion context */ -void mirb_completion_init(mirb_completion_ctx *ctx, mrb_state *mrb, - mrb_ccontext *cxt); - -/* Free completion context */ -void mirb_completion_free(mirb_completion_ctx *ctx); - -/* Analyze line and generate completions */ -void mirb_generate_completions(mirb_completion_ctx *ctx, - const char *line, int cursor_pos); - -/* Get completion type from context */ -mirb_completion_type mirb_detect_completion_type(const char *line, - int cursor_pos); - -/* Individual completion generators */ -void mirb_complete_methods(mirb_completion_ctx *ctx, mrb_value receiver); -void mirb_complete_keywords(mirb_completion_ctx *ctx); -void mirb_complete_local_vars(mirb_completion_ctx *ctx); -void mirb_complete_global_vars(mirb_completion_ctx *ctx); -void mirb_complete_constants(mirb_completion_ctx *ctx, struct RClass *scope); -void mirb_complete_files(mirb_completion_ctx *ctx, const char *partial_path); - -/* Helper functions */ - -/* Add completion if matches prefix */ -void mirb_add_completion(mirb_completion_ctx *ctx, const char *text); - -/* Extract receiver expression from line */ -char *mirb_extract_receiver(const char *line, int cursor_pos, int *recv_end); - -/* Evaluate receiver expression to get object */ -mrb_value mirb_eval_receiver(mrb_state *mrb, const char *receiver_expr, mrb_ccontext *cxt);; - -/* Check if in file completion context */ -mrb_bool mirb_in_file_context(const char *line, int quote_pos); - -/* Readline/Libedit adapter setup */ -#ifdef MRB_USE_READLINE -#ifndef MRB_USE_LINENOISE - -void mirb_setup_readline_completion(mrb_state *mrb, mrb_ccontext *cxt); -void mirb_cleanup_readline_completion(void); - -#endif -#endif - -/* Linenoise adapter setup */ -#ifdef MRB_USE_LINENOISE - -void mirb_setup_linenoise_completion(mrb_state *mrb, mrb_ccontext *cxt); -void mirb_cleanup_linenoise_completion(void); - -#endif - -#endif /* MIRB_COMPLETION_H */ diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_editor.c b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_editor.c new file mode 100644 index 000000000..47e565e67 --- /dev/null +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_editor.c @@ -0,0 +1,607 @@ +/* +** mirb_editor.c - Multi-line editor for mirb +** +** See Copyright Notice in mruby.h +*/ + +#include "mirb_editor.h" +#include +#include +#include + +/* 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; +} diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_editor.h b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_editor.h new file mode 100644 index 000000000..b0c3ee00d --- /dev/null +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_editor.h @@ -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 +#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 */ diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_term.c b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_term.c new file mode 100644 index 000000000..19445219f --- /dev/null +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_term.c @@ -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 +#include +#include + +#if !defined(_WIN32) && !defined(_WIN64) +#include +#include +#include +#include + +/* + * 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"); +} diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_term.h b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_term.h new file mode 100644 index 000000000..edea88088 --- /dev/null +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_term.h @@ -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 + +/* + * 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 */