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