Files
Yukihiro "Matz" Matsumoto 48c38c822f mirb_editor.c: table-drive block keywords in calc_indent_level()
Replace the strncmp() if-else chain for block-opening and closing
keywords with a data-driven indent_table, matching the existing
dedent_table pattern. Also use mirb_is_word_char() for the word
boundary check.

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-23 15:46:28 +09:00

1067 lines
27 KiB
C

/*
** 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"
/*
* 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;
}
/*
* Dedent keyword table: keywords that reduce indentation level.
* allow_eol: keyword can appear alone at end of line
* delims: valid non-NUL characters that can follow the keyword
*/
static const struct {
const char *word;
const char *delims;
mrb_bool allow_eol;
} dedent_table[] = {
{"else", " \t", TRUE},
{"elsif", " ", FALSE},
{"end", " \t.)", TRUE},
{"ensure", " \t", TRUE},
{"in", " ", FALSE},
{"rescue", " \t", TRUE},
{"when", " ", FALSE},
};
static mrb_bool
is_dedent_keyword(const char *content)
{
size_t i;
if (content[0] == '}') return TRUE;
for (i = 0; i < sizeof(dedent_table)/sizeof(dedent_table[0]); i++) {
size_t len = strlen(dedent_table[i].word);
if (strncmp(content, dedent_table[i].word, len) == 0) {
char c = content[len];
if (c == '\0') return dedent_table[i].allow_eol;
return strchr(dedent_table[i].delims, c) != NULL;
}
}
return FALSE;
}
/*
* Check if a line contains only whitespace (or is empty)
*/
static mrb_bool
is_line_blank(const mirb_line *line)
{
for (size_t i = 0; i < line->len; i++) {
if (line->data[i] != ' ' && line->data[i] != '\t') {
return FALSE;
}
}
return TRUE;
}
/*
* Indent keyword table: keywords that affect indentation level.
* delta: +1 for block-opening, -1 for block-closing
* allow_eol: keyword can appear at end of line/string
* delims: valid non-NUL characters that can follow the keyword
*/
static const struct {
const char *word;
const char *delims;
mrb_bool allow_eol;
int delta;
} indent_table[] = {
{"begin", "\n #", TRUE, +1},
{"case", " ", FALSE, +1},
{"class", " ", FALSE, +1},
{"def", " ", FALSE, +1},
{"do", "\n #|", TRUE, +1},
{"end", "\n #.)", TRUE, -1},
{"for", " ", FALSE, +1},
{"if", " ", FALSE, +1},
{"module", " ", FALSE, +1},
{"unless", " ", FALSE, +1},
{"until", " ", FALSE, +1},
{"while", " ", FALSE, +1},
};
/*
* 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 keywords at word boundary */
if (at_line_start || (p > code && !mirb_is_word_char(p[-1]))) {
size_t ki;
for (ki = 0; ki < sizeof(indent_table)/sizeof(indent_table[0]); ki++) {
size_t len = strlen(indent_table[ki].word);
if (strncmp(p, indent_table[ki].word, len) == 0) {
char c = p[len];
if ((c == '\0' && indent_table[ki].allow_eol) ||
(c != '\0' && strchr(indent_table[ki].delims, c))) {
level += indent_table[ki].delta;
if (level < 0) level = 0;
}
break;
}
}
}
/* Check for block opening/closing with braces */
if (*p == '{') {
level++;
}
else if (*p == '}') {
if (level > 0) level--;
}
at_line_start = 0;
p++;
}
return level;
}
/*
* Calculate expected indent level for the given line index.
* Uses code up to line_idx-1 to determine nesting depth.
*/
static int
calc_expected_indent(mirb_buffer *buf, size_t line_idx)
{
int indent = 0;
char *partial;
if (line_idx == 0) return 0;
partial = mirb_buffer_to_string_upto_line(buf, line_idx - 1);
if (partial) {
indent = calc_indent_level(partial);
free(partial);
}
return indent;
}
/*
* Adjust current line's leading whitespace to target_spaces.
* Preserves cursor position relative to line content.
*/
static void
adjust_line_indent(mirb_buffer *buf, size_t target_spaces)
{
size_t current_spaces = leading_spaces(mirb_buffer_current_line(buf));
size_t saved_col = buf->cursor_col;
if (target_spaces == current_spaces) return;
if (target_spaces > current_spaces) {
size_t add = target_spaces - current_spaces;
buf->cursor_col = 0;
for (size_t i = 0; i < add; i++) {
mirb_buffer_insert_char(buf, ' ');
}
buf->cursor_col = saved_col + add;
}
else {
size_t to_remove = current_spaces - target_spaces;
buf->cursor_col = 0;
for (size_t i = 0; i < to_remove; i++) {
mirb_buffer_delete_forward(buf);
}
buf->cursor_col = (saved_col > to_remove) ? (saved_col - to_remove) : 0;
}
}
/*
* Insert indent spaces at cursor position
*/
static void
insert_indent_spaces(mirb_buffer *buf, int indent_level)
{
for (int i = 0; i < indent_level * 2; i++) {
mirb_buffer_insert_char(buf, ' ');
}
}
/*
* 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;
size_t line_len = buf->lines[buf->cursor_line].len;
/* 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;
}
}
/* Helper macro: check keyword completion */
#define CHECK_KEYWORD(keyword, len, trigger_char) \
if (last_char == trigger_char && col >= len) { \
if (strncmp(line + col - len, keyword, len) == 0) { \
if (col == len || line_is_blank_before(line, col - len)) { \
if (col == line_len || \
line[col] == ' ' || line[col] == '\t' || line[col] == '\0' || \
line[col] == '\n' || line[col] == '.' || line[col] == ')') { \
return TRUE; \
} \
} \
} \
}
/* Check for 'end' - dedent when 'd' completes "end" */
CHECK_KEYWORD("end", 3, 'd');
/* Check for 'else' - dedent when 'e' completes "else" */
CHECK_KEYWORD("else", 4, 'e');
/* Check for 'elsif' - dedent when 'f' completes "elsif" */
CHECK_KEYWORD("elsif", 5, 'f');
/* Check for 'when' - dedent when 'n' completes "when" */
CHECK_KEYWORD("when", 4, 'n');
/* Check for 'in' - dedent when 'n' completes "in" (pattern matching) */
CHECK_KEYWORD("in", 2, 'n');
/* Check for 'rescue' - dedent when 'e' completes "rescue" */
CHECK_KEYWORD("rescue", 6, 'e');
/* Check for 'ensure' - dedent when 'e' completes "ensure" */
CHECK_KEYWORD("ensure", 6, 'e');
#undef CHECK_KEYWORD
return FALSE;
}
/*
* Perform dedentation - adjust indent for dedent keyword
*/
static void
perform_dedent(mirb_buffer *buf)
{
int indent = calc_expected_indent(buf, buf->cursor_line);
if (indent > 0) indent--;
adjust_line_indent(buf, (size_t)(indent * 2));
}
/*
* Re-indent current line to match expected indent level
* Used before inserting newline to fix any misaligned indentation
*/
static void
reindent_line(mirb_buffer *buf)
{
mirb_line *line = &buf->lines[buf->cursor_line];
int indent = calc_expected_indent(buf, buf->cursor_line);
const char *content = line->data + leading_spaces(line->data);
if (is_dedent_keyword(content)) {
if (indent > 0) indent--;
}
adjust_line_indent(buf, (size_t)(indent * 2));
}
/*
* 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;
}
if (!mirb_history_init(&ed->hist, MIRB_HISTORY_SIZE)) {
mirb_buffer_free(&ed->buf);
mirb_term_cleanup(&ed->term);
return FALSE;
}
ed->prompt = "> ";
ed->prompt_cont = "* ";
ed->prompt_len = 2;
ed->prompt_cont_len = 2;
ed->prompt_fmt = NULL;
ed->prompt_cont_fmt = NULL;
ed->line_num_base = 1;
ed->use_color = FALSE;
mirb_highlight_init(&ed->highlight, FALSE);
ed->initialized = TRUE;
return TRUE;
}
/*
* Cleanup editor
*/
void
mirb_editor_cleanup(mirb_editor *ed)
{
if (!ed->initialized) return;
mirb_history_free(&ed->hist);
mirb_buffer_free(&ed->buf);
mirb_term_cleanup(&ed->term);
ed->initialized = FALSE;
}
/*
* Set prompts (fixed strings)
*/
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);
ed->prompt_fmt = NULL;
ed->prompt_cont_fmt = NULL;
}
/*
* Set prompt format strings for line-numbered prompts
*/
void
mirb_editor_set_prompt_format(mirb_editor *ed, const char *prompt_fmt,
const char *prompt_cont_fmt, int line_num)
{
ed->prompt_fmt = prompt_fmt;
ed->prompt_cont_fmt = prompt_cont_fmt;
ed->line_num_base = line_num;
/* Estimate prompt length (assuming line numbers up to 999) */
ed->prompt_len = strlen(prompt_fmt) + 2; /* %d -> up to 3 digits, minus 2 for %d */
ed->prompt_cont_len = strlen(prompt_cont_fmt) + 2;
}
/*
* 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;
}
/*
* Set tab completion callbacks
*/
void
mirb_editor_set_tab_complete(mirb_editor *ed,
mirb_tab_complete_fn *complete_fn,
mirb_tab_complete_free_fn *free_fn,
void *user_data)
{
ed->tab_complete = complete_fn;
ed->tab_complete_free = free_fn;
ed->tab_complete_data = user_data;
}
/*
* Handle tab completion
* Returns TRUE if completion was performed
*/
/*
* Handle tab auto-indent
* Adjusts current line's indentation to match the expected level
*/
static void
handle_tab_indent(mirb_editor *ed)
{
reindent_line(&ed->buf);
}
static mrb_bool
handle_tab_completion(mirb_editor *ed)
{
char **completions = NULL;
int count, prefix_len;
const char *current_line;
int cursor_col;
if (!ed->tab_complete) return FALSE;
/* Get current line and cursor position */
current_line = ed->buf.lines[ed->buf.cursor_line].data;
cursor_col = (int)ed->buf.cursor_col;
/* Get completions */
count = ed->tab_complete(current_line, cursor_col, &completions, &prefix_len,
ed->tab_complete_data);
if (count == 0 || !completions) {
return FALSE;
}
if (count == 1) {
/* Single completion - insert it */
const char *completion = completions[0];
int i;
/* Delete the prefix we're replacing */
for (i = 0; i < prefix_len; i++) {
mirb_buffer_delete_back(&ed->buf);
}
/* Insert completion */
mirb_buffer_insert_string(&ed->buf, completion, strlen(completion));
}
else {
/* Multiple completions - find common prefix and show options */
int common_len = (int)strlen(completions[0]);
int i, j;
/* Find longest common prefix */
for (i = 1; i < count; i++) {
for (j = 0; j < common_len && completions[i][j]; j++) {
if (completions[0][j] != completions[i][j]) {
common_len = j;
break;
}
}
if (j < common_len) common_len = j;
}
if (common_len > prefix_len) {
/* Extend with common prefix */
for (i = 0; i < prefix_len; i++) {
mirb_buffer_delete_back(&ed->buf);
}
mirb_buffer_insert_string(&ed->buf, completions[0], common_len);
}
else {
/* Show all completions */
printf("\r\n");
for (i = 0; i < count; i++) {
printf("%s ", completions[i]);
if ((i + 1) % 4 == 0 && i + 1 < count) printf("\r\n");
}
printf("\r\n");
/* Force full redraw */
ed->prev_line_count = 0;
}
}
/* Free completions */
if (ed->tab_complete_free) {
ed->tab_complete_free(completions, count, ed->tab_complete_data);
}
return TRUE;
}
/*
* Enable/disable color
*/
void
mirb_editor_set_color(mirb_editor *ed, mrb_bool enable)
{
ed->use_color = enable;
mirb_highlight_init(&ed->highlight, enable);
}
/*
* Check if multi-line editing is supported
*/
mrb_bool
mirb_editor_supported(mirb_editor *ed)
{
return ed->term.supported;
}
/*
* Calculate prompt length for given line
*/
static size_t
calc_prompt_len(mirb_editor *ed, size_t line_idx)
{
if (ed->prompt_fmt != NULL) {
/* Format string: calculate actual length */
int line_num = ed->line_num_base + (int)line_idx;
const char *fmt = (line_idx == 0) ? ed->prompt_fmt : ed->prompt_cont_fmt;
return (size_t)snprintf(NULL, 0, fmt, line_num);
}
else {
/* Fixed prompt string */
return (line_idx == 0) ? ed->prompt_len : ed->prompt_cont_len;
}
}
/*
* Print prompt for given line
*/
static void
print_prompt(mirb_editor *ed, size_t line_idx)
{
int line_num = ed->line_num_base + (int)line_idx;
if (ed->use_color) {
printf("%s", COLOR_GREEN);
}
if (ed->prompt_fmt != NULL) {
/* Use format string with line number */
const char *fmt = (line_idx == 0) ? ed->prompt_fmt : ed->prompt_cont_fmt;
printf(fmt, line_num);
}
else {
/* Use fixed prompt string */
const char *p = (line_idx == 0) ? ed->prompt : ed->prompt_cont;
printf("%s", p);
}
if (ed->use_color) {
printf("%s", COLOR_RESET);
}
}
/*
* 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 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();
/* Reset highlight state for fresh scan */
mirb_highlight_reset(&ed->highlight);
/* Redraw all lines */
for (size_t i = 0; i < ed->buf.line_count; i++) {
print_prompt(ed, i);
mirb_highlight_print_line(&ed->highlight, 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 (calculate actual prompt length) */
size_t prompt_len = calc_prompt_len(ed, ed->buf.cursor_line);
size_t display_col = mirb_buffer_cursor_display_col(&ed->buf);
mirb_term_cursor_col((int)(prompt_len + display_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:
/* Stop history browsing */
mirb_history_browse_stop(&ed->hist);
/* Re-indent current line before inserting newline */
reindent_line(&ed->buf);
{
/*
* Smart Enter behavior:
* - Only evaluate when cursor is at end of last line and code is complete
* - If cursor is not at end of last line, always insert/split (no evaluation)
* - If next line is blank last line and cursor at end, move to it
*/
mrb_bool at_last_line = (ed->buf.cursor_line == ed->buf.line_count - 1);
mirb_line *current_line = &ed->buf.lines[ed->buf.cursor_line];
mrb_bool at_end_of_line = (ed->buf.cursor_col == current_line->len);
mrb_bool can_evaluate = at_last_line && at_end_of_line;
/* Check for smart navigation to existing blank last line */
if (!at_last_line && at_end_of_line) {
size_t next_line_idx = ed->buf.cursor_line + 1;
mrb_bool next_is_last = (next_line_idx == ed->buf.line_count - 1);
if (next_is_last) {
mirb_line *next_line = &ed->buf.lines[next_line_idx];
if (is_line_blank(next_line)) {
/* Move to existing blank last line with proper indentation */
int indent = calc_expected_indent(&ed->buf, ed->buf.line_count);
mirb_buffer_cursor_down(&ed->buf);
/* Clear existing whitespace and set correct indent */
mirb_line *line = &ed->buf.lines[ed->buf.cursor_line];
line->len = 0;
line->data[0] = '\0';
ed->buf.cursor_col = 0;
insert_indent_spaces(&ed->buf, indent);
return TRUE;
}
}
}
/* If cursor in middle of line and next is blank last, remove it before split */
if (!at_last_line && !at_end_of_line) {
size_t next_line_idx = ed->buf.cursor_line + 1;
if (next_line_idx == ed->buf.line_count - 1) {
mirb_line *next_line = &ed->buf.lines[next_line_idx];
if (is_line_blank(next_line)) {
mirb_buffer_delete_line(&ed->buf, next_line_idx);
}
}
}
/* Check if input is complete - only when cursor at end of last line */
if (can_evaluate && 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) {
free(code);
*result = MIRB_EDIT_OK;
return FALSE;
}
/* Code not complete - add new line with indentation */
int indent = calc_indent_level(code);
free(code);
mirb_buffer_newline(&ed->buf);
insert_indent_spaces(&ed->buf, indent);
return TRUE;
}
}
/* Not at end of last line - just insert/split with appropriate indent */
{
int indent = calc_expected_indent(&ed->buf, ed->buf.cursor_line + 1);
mirb_buffer_newline(&ed->buf);
/* Check if new line starts with dedenting keyword */
mirb_line *new_line = &ed->buf.lines[ed->buf.cursor_line];
if (is_dedent_keyword(new_line->data)) {
if (indent > 0) indent--;
}
insert_indent_spaces(&ed->buf, indent);
return TRUE;
}
}
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:
/* If on first line, navigate history; otherwise move cursor up */
if (ed->buf.cursor_line == 0) {
/* Start history browsing if not already */
if (!ed->hist.browsing) {
char *current = mirb_buffer_to_string(&ed->buf);
mirb_history_browse_start(&ed->hist, current);
free(current);
}
const char *prev = mirb_history_prev(&ed->hist);
if (prev) {
mirb_buffer_set_string(&ed->buf, prev);
mirb_buffer_cursor_finish(&ed->buf);
}
}
else {
mirb_buffer_cursor_up(&ed->buf);
}
return TRUE;
case MIRB_KEY_DOWN:
case MIRB_KEY_CTRL_N:
/* If on last line, navigate history; otherwise move cursor down */
if (ed->buf.cursor_line == ed->buf.line_count - 1) {
if (ed->hist.browsing) {
const char *next = mirb_history_next(&ed->hist);
if (next) {
mirb_buffer_set_string(&ed->buf, next);
mirb_buffer_cursor_finish(&ed->buf);
}
}
}
else {
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;
case MIRB_KEY_TAB:
/* Auto-indent if at start/end of line or preceded by whitespace */
{
mirb_line *line = &ed->buf.lines[ed->buf.cursor_line];
mrb_bool do_indent = FALSE;
if (ed->buf.cursor_col == 0) {
do_indent = TRUE;
}
else if (ed->buf.cursor_col == line->len) {
/* At end of line */
do_indent = TRUE;
}
else {
char prev_char = line->data[ed->buf.cursor_col - 1];
if (prev_char == ' ' || prev_char == '\t') {
do_indent = TRUE;
}
}
if (do_indent) {
handle_tab_indent(ed);
}
else {
handle_tab_completion(ed);
}
}
return TRUE;
default:
/* Insert printable characters */
if (key >= 32 && key < 127) {
/* Stop history browsing when user types */
mirb_history_browse_stop(&ed->hist);
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);
}
}
#ifdef MRB_UTF8_STRING
/* Handle UTF-8 multibyte characters (bytes >= 0x80) */
else if (key >= 128 && key <= 255) {
mirb_history_browse_stop(&ed->hist);
mirb_buffer_insert_char(&ed->buf, (char)key);
}
#endif
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;
}
/*
* Add entry to history
*/
void
mirb_editor_history_add(mirb_editor *ed, const char *entry)
{
mirb_history_add(&ed->hist, entry);
}