mruby-bin-mirb: add auto-indent for continuation lines

- automatically indent continuation lines based on block depth
- detect block-opening keywords (def, class, if, do, etc.) and braces
- use ANSI escape sequences to fix indentation for:
  - block-closing keywords (end, })
  - mid-block keywords (else, elsif, rescue, ensure, when)
- only active for interactive TTY input with ANSI support
- requires GNU readline (not available with linenoise)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Yukihiro "Matz" Matsumoto
2025-12-09 16:49:39 +09:00
parent 9ad72e3a96
commit d52f3189d8
+178 -1
View File
@@ -29,6 +29,13 @@
#include <signal.h>
#include <setjmp.h>
#ifdef _WIN32
#include <io.h>
#define isatty(fd) _isatty(fd)
#else
#include <unistd.h>
#endif
#include "mirb_completion.h"
/* obsolete configuration */
@@ -81,6 +88,151 @@
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;
}
/* 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 */
printf("%d* ", line_num);
/* 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)
{
@@ -596,10 +748,17 @@ main(int argc, char **argv)
mrb_ccontext_filename(mrb, cxt, "(mirb)");
if (args.verbose) cxt->dump_result = TRUE;
/* Setup tab completion */
/* 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();
}
#endif
#else
mirb_setup_linenoise_completion(mrb, cxt);
#endif
@@ -661,6 +820,10 @@ main(int argc, char **argv)
{
char prompt[32];
snprintf(prompt, sizeof(prompt), "%d%c ", line_num, code_block_open ? '*' : '>');
#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;
#endif
line = MIRB_READLINE(prompt);
}
signal(SIGINT, SIG_DFL);
@@ -689,6 +852,20 @@ 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")) {