From 624272b15d651228e5998171a85af7b1a273655a Mon Sep 17 00:00:00 2001 From: "Yukihiro \"Matz\" Matsumoto" Date: Mon, 29 Dec 2025 08:06:59 +0900 Subject: [PATCH] mruby-bin-mirb: add syntax highlighting for keywords and strings Add syntax highlighting to mirb's multi-line editor with support for: - keywords (def, if, class, end, etc.) in magenta - strings ("...", '...', %q{...}) in green - comments (#...) in gray - numbers (42, 3.14, 0xff) in cyan - symbols (:foo) in yellow - constants (Array, Foo) in bold yellow - instance variables (@var) in blue - global variables ($var) in bold blue Features: - auto-detects light/dark theme via COLORFGBG env var - MIRB_THEME=light/dark for explicit override - method calls like obj.class correctly not highlighted as keywords - enabled automatically when terminal supports color Co-authored-by: Claude --- .../mruby-bin-mirb/tools/mirb/mirb_editor.c | 7 +- .../mruby-bin-mirb/tools/mirb/mirb_editor.h | 3 + .../tools/mirb/mirb_highlight.c | 425 ++++++++++++++++++ .../tools/mirb/mirb_highlight.h | 78 ++++ 4 files changed, 512 insertions(+), 1 deletion(-) create mode 100644 mrbgems/mruby-bin-mirb/tools/mirb/mirb_highlight.c create mode 100644 mrbgems/mruby-bin-mirb/tools/mirb/mirb_highlight.h diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_editor.c b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_editor.c index 36b8ec6fe..406927168 100644 --- a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_editor.c +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_editor.c @@ -340,6 +340,7 @@ mirb_editor_init(mirb_editor *ed) 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; @@ -573,6 +574,7 @@ void mirb_editor_set_color(mirb_editor *ed, mrb_bool enable) { ed->use_color = enable; + mirb_highlight_init(&ed->highlight, enable); } /* @@ -657,10 +659,13 @@ refresh_display(mirb_editor *ed) 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); - printf("%s", mirb_buffer_line_at(&ed->buf, i)); + mirb_highlight_print_line(&ed->highlight, mirb_buffer_line_at(&ed->buf, i)); if (i < ed->buf.line_count - 1) { printf("\r\n"); diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_editor.h b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_editor.h index 255bc8571..fe50da325 100644 --- a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_editor.h +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_editor.h @@ -11,6 +11,7 @@ #include "mirb_term.h" #include "mirb_buffer.h" #include "mirb_history.h" +#include "mirb_highlight.h" /* * Editor result codes @@ -72,6 +73,8 @@ typedef struct mirb_editor { mrb_bool initialized; /* editor is initialized */ mrb_bool use_color; /* use colored output */ + + mirb_highlighter highlight; /* syntax highlighting state */ } mirb_editor; /* diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_highlight.c b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_highlight.c new file mode 100644 index 000000000..3d1c4f1f1 --- /dev/null +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_highlight.c @@ -0,0 +1,425 @@ +/* +** mirb_highlight.c - Syntax highlighting for mirb +** +** See Copyright Notice in mruby.h +*/ + +#include "mirb_highlight.h" +#include +#include +#include + +/* ANSI color codes - using standard 16-color palette for compatibility */ + +/* Dark theme colors (bright/light colors on dark background) */ +#define DARK_KEYWORD "\033[1;35m" /* bold magenta */ +#define DARK_STRING "\033[32m" /* green */ +#define DARK_COMMENT "\033[90m" /* bright black (gray) */ +#define DARK_NUMBER "\033[36m" /* cyan */ +#define DARK_SYMBOL "\033[33m" /* yellow */ +#define DARK_CONSTANT "\033[1;33m" /* bold yellow */ +#define DARK_IVAR "\033[34m" /* blue */ +#define DARK_GVAR "\033[1;34m" /* bold blue */ +#define DARK_REGEXP "\033[31m" /* red */ + +/* Light theme colors (dark colors on light background) */ +#define LIGHT_KEYWORD "\033[35m" /* magenta */ +#define LIGHT_STRING "\033[32m" /* green */ +#define LIGHT_COMMENT "\033[37m" /* white (light gray) */ +#define LIGHT_NUMBER "\033[36m" /* cyan */ +#define LIGHT_SYMBOL "\033[33m" /* yellow */ +#define LIGHT_CONSTANT "\033[33m" /* yellow */ +#define LIGHT_IVAR "\033[34m" /* blue */ +#define LIGHT_GVAR "\033[34m" /* blue */ +#define LIGHT_REGEXP "\033[31m" /* red */ + +#define COLOR_RESET "\033[0m" + +/* Keyword list - must be sorted alphabetically for bsearch */ +static const char *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" +}; +#define NUM_KEYWORDS (sizeof(keywords) / sizeof(keywords[0])) + +static int +keyword_cmp(const void *a, const void *b) +{ + return strcmp((const char *)a, *(const char **)b); +} + +static mrb_bool +is_keyword(const char *word, size_t len) +{ + char buf[32]; + + if (len >= sizeof(buf)) return FALSE; + memcpy(buf, word, len); + buf[len] = '\0'; + return bsearch(buf, keywords, NUM_KEYWORDS, sizeof(keywords[0]), keyword_cmp) != NULL; +} + +static mrb_bool +is_word_char(char c) +{ + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '_'; +} + +static mrb_bool +is_word_start(char c) +{ + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; +} + +static mrb_bool +is_upper(char c) +{ + return c >= 'A' && c <= 'Z'; +} + +/* + * Get color code for token type based on theme + */ +static const char * +get_color(mirb_highlighter *hl, mirb_token_type type) +{ + if (!hl->enabled) return ""; + + if (hl->theme == MIRB_THEME_DARK) { + switch (type) { + case MIRB_TOK_KEYWORD: return DARK_KEYWORD; + case MIRB_TOK_STRING: return DARK_STRING; + case MIRB_TOK_COMMENT: return DARK_COMMENT; + case MIRB_TOK_NUMBER: return DARK_NUMBER; + case MIRB_TOK_SYMBOL: return DARK_SYMBOL; + case MIRB_TOK_CONSTANT: return DARK_CONSTANT; + case MIRB_TOK_IVAR: return DARK_IVAR; + case MIRB_TOK_GVAR: return DARK_GVAR; + case MIRB_TOK_REGEXP: return DARK_REGEXP; + default: return ""; + } + } + else { + switch (type) { + case MIRB_TOK_KEYWORD: return LIGHT_KEYWORD; + case MIRB_TOK_STRING: return LIGHT_STRING; + case MIRB_TOK_COMMENT: return LIGHT_COMMENT; + case MIRB_TOK_NUMBER: return LIGHT_NUMBER; + case MIRB_TOK_SYMBOL: return LIGHT_SYMBOL; + case MIRB_TOK_CONSTANT: return LIGHT_CONSTANT; + case MIRB_TOK_IVAR: return LIGHT_IVAR; + case MIRB_TOK_GVAR: return LIGHT_GVAR; + case MIRB_TOK_REGEXP: return LIGHT_REGEXP; + default: return ""; + } + } +} + +static const char * +get_reset(mirb_highlighter *hl) +{ + return hl->enabled ? COLOR_RESET : ""; +} + +/* + * Print n characters with specified color + */ +static void +print_colored(mirb_highlighter *hl, const char *start, size_t len, mirb_token_type type) +{ + const char *color = get_color(hl, type); + const char *reset = get_reset(hl); + + if (*color) printf("%s", color); + fwrite(start, 1, len, stdout); + if (*color) printf("%s", reset); +} + +/* + * Detect theme from environment + */ +mirb_theme +mirb_highlight_detect_theme(void) +{ + const char *env; + + /* Check explicit MIRB_THEME first */ + env = getenv("MIRB_THEME"); + if (env) { + if (strcmp(env, "light") == 0) return MIRB_THEME_LIGHT; + if (strcmp(env, "dark") == 0) return MIRB_THEME_DARK; + } + + /* Check COLORFGBG (format: "fg;bg" where bg > 6 usually means light) */ + env = getenv("COLORFGBG"); + if (env) { + const char *semi = strchr(env, ';'); + if (semi) { + int bg = atoi(semi + 1); + /* Background colors 7, 15, or high values typically mean light theme */ + if (bg == 7 || bg == 15 || (bg >= 230 && bg <= 255)) { + return MIRB_THEME_LIGHT; + } + } + } + + /* Default to dark theme (more common in terminals) */ + return MIRB_THEME_DARK; +} + +void +mirb_highlight_init(mirb_highlighter *hl, mrb_bool enabled) +{ + memset(hl, 0, sizeof(*hl)); + hl->enabled = enabled; + if (enabled) { + hl->theme = mirb_highlight_detect_theme(); + } +} + +void +mirb_highlight_set_theme(mirb_highlighter *hl, mirb_theme theme) +{ + hl->theme = theme; +} + +void +mirb_highlight_reset(mirb_highlighter *hl) +{ + hl->in_string = FALSE; + hl->string_quote = 0; + hl->in_heredoc = FALSE; + hl->in_regexp = FALSE; +} + +/* + * Print a line with syntax highlighting + */ +void +mirb_highlight_print_line(mirb_highlighter *hl, const char *line) +{ + const char *p = line; + const char *token_start; + + if (!hl->enabled) { + printf("%s", line); + return; + } + + /* Handle continuation of multi-line string */ + if (hl->in_string) { + token_start = p; + while (*p) { + if (*p == '\\' && p[1]) { + p += 2; + continue; + } + if (*p == hl->string_quote) { + p++; + print_colored(hl, token_start, (size_t)(p - token_start), MIRB_TOK_STRING); + hl->in_string = FALSE; + break; + } + p++; + } + if (hl->in_string) { + /* String continues to next line */ + print_colored(hl, token_start, (size_t)(p - token_start), MIRB_TOK_STRING); + return; + } + } + + while (*p) { + /* Comment - rest of line */ + if (*p == '#') { + print_colored(hl, p, strlen(p), MIRB_TOK_COMMENT); + return; + } + + /* Strings */ + if (*p == '"' || *p == '\'') { + char quote = *p; + token_start = p++; + while (*p) { + if (*p == '\\' && p[1]) { + p += 2; + continue; + } + if (*p == quote) { + p++; + break; + } + p++; + } + if (p[-1] == quote) { + print_colored(hl, token_start, (size_t)(p - token_start), MIRB_TOK_STRING); + } + else { + /* Unterminated string - continues to next line */ + print_colored(hl, token_start, (size_t)(p - token_start), MIRB_TOK_STRING); + hl->in_string = TRUE; + hl->string_quote = quote; + return; + } + continue; + } + + /* Percent strings: %q{...}, %Q{...}, %w{...}, etc. */ + if (*p == '%' && p[1] && strchr("qQwWiIxsr", p[1])) { + char open = p[2]; + char close = 0; + int depth = 1; + + token_start = p; + if (open == '(' || open == '{' || open == '[' || open == '<') { + close = (open == '(') ? ')' : (open == '{') ? '}' : (open == '[') ? ']' : '>'; + p += 3; + while (*p && depth > 0) { + if (*p == '\\' && p[1]) { + p += 2; + continue; + } + if (*p == open) depth++; + else if (*p == close) depth--; + p++; + } + print_colored(hl, token_start, (size_t)(p - token_start), MIRB_TOK_STRING); + continue; + } + else if (open) { + /* Non-paired delimiter like %q!...! */ + p += 3; + while (*p && *p != open) { + if (*p == '\\' && p[1]) { + p += 2; + continue; + } + p++; + } + if (*p == open) p++; + print_colored(hl, token_start, (size_t)(p - token_start), MIRB_TOK_STRING); + continue; + } + /* Not a percent string, fall through */ + } + + /* Symbols: :symbol or :"string" */ + if (*p == ':' && p[1] && (is_word_start(p[1]) || p[1] == '"' || p[1] == '\'')) { + token_start = p++; + if (*p == '"' || *p == '\'') { + /* Quoted symbol */ + char quote = *p++; + while (*p && *p != quote) { + if (*p == '\\' && p[1]) { + p += 2; + continue; + } + p++; + } + if (*p == quote) p++; + } + else { + /* Regular symbol */ + while (*p && (is_word_char(*p) || *p == '?' || *p == '!')) p++; + } + print_colored(hl, token_start, (size_t)(p - token_start), MIRB_TOK_SYMBOL); + continue; + } + + /* Instance variables: @var */ + if (*p == '@') { + token_start = p++; + if (*p == '@') p++; /* @@class_var */ + while (*p && is_word_char(*p)) p++; + print_colored(hl, token_start, (size_t)(p - token_start), MIRB_TOK_IVAR); + continue; + } + + /* Global variables: $var */ + if (*p == '$') { + token_start = p++; + /* Special globals like $!, $?, $1, etc. */ + if (*p && !is_word_char(*p) && *p != ' ') { + p++; + } + else { + while (*p && is_word_char(*p)) p++; + } + print_colored(hl, token_start, (size_t)(p - token_start), MIRB_TOK_GVAR); + continue; + } + + /* Numbers */ + if ((*p >= '0' && *p <= '9') || + (*p == '-' && p[1] >= '0' && p[1] <= '9' && (p == line || !is_word_char(p[-1])))) { + token_start = p; + if (*p == '-') p++; + if (*p == '0' && (p[1] == 'x' || p[1] == 'X')) { + /* Hex */ + p += 2; + while ((*p >= '0' && *p <= '9') || (*p >= 'a' && *p <= 'f') || + (*p >= 'A' && *p <= 'F') || *p == '_') p++; + } + else if (*p == '0' && (p[1] == 'b' || p[1] == 'B')) { + /* Binary */ + p += 2; + while (*p == '0' || *p == '1' || *p == '_') p++; + } + else if (*p == '0' && (p[1] == 'o' || p[1] == 'O')) { + /* Octal */ + p += 2; + while ((*p >= '0' && *p <= '7') || *p == '_') p++; + } + else { + /* Decimal or float */ + while ((*p >= '0' && *p <= '9') || *p == '_') p++; + if (*p == '.' && p[1] >= '0' && p[1] <= '9') { + p++; + while ((*p >= '0' && *p <= '9') || *p == '_') p++; + } + if (*p == 'e' || *p == 'E') { + p++; + if (*p == '+' || *p == '-') p++; + while ((*p >= '0' && *p <= '9') || *p == '_') p++; + } + } + /* Suffix like 'i' for complex or 'r' for rational */ + if (*p == 'i' || *p == 'r') p++; + print_colored(hl, token_start, (size_t)(p - token_start), MIRB_TOK_NUMBER); + continue; + } + + /* Identifiers and keywords */ + if (is_word_start(*p)) { + token_start = p; + mrb_bool is_const = is_upper(*p); + /* Check if preceded by dot (method call like obj.class) */ + mrb_bool after_dot = (token_start > line && token_start[-1] == '.'); + while (*p && (is_word_char(*p) || *p == '?' || *p == '!')) p++; + + size_t len = (size_t)(p - token_start); + + if (is_const) { + print_colored(hl, token_start, len, MIRB_TOK_CONSTANT); + } + else if (!after_dot && is_keyword(token_start, len)) { + print_colored(hl, token_start, len, MIRB_TOK_KEYWORD); + } + else { + fwrite(token_start, 1, len, stdout); + } + continue; + } + + /* Regular expression (simple heuristic: after =~, !~ or at line start after if/unless/when) */ + /* This is tricky - for now just output as-is */ + + /* Default: just output character */ + putchar(*p++); + } +} diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_highlight.h b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_highlight.h new file mode 100644 index 000000000..b81a23845 --- /dev/null +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_highlight.h @@ -0,0 +1,78 @@ +/* +** mirb_highlight.h - Syntax highlighting for mirb +** +** See Copyright Notice in mruby.h +*/ + +#ifndef MIRB_HIGHLIGHT_H +#define MIRB_HIGHLIGHT_H + +#include +#include + +/* + * Token types for syntax highlighting + */ +typedef enum mirb_token_type { + MIRB_TOK_DEFAULT, /* default text */ + MIRB_TOK_KEYWORD, /* if, else, def, class, end, etc. */ + MIRB_TOK_STRING, /* "..." or '...' */ + MIRB_TOK_COMMENT, /* # to end of line */ + MIRB_TOK_NUMBER, /* integers, floats */ + MIRB_TOK_SYMBOL, /* :symbol */ + MIRB_TOK_CONSTANT, /* Uppercase identifiers */ + MIRB_TOK_IVAR, /* @instance_var */ + MIRB_TOK_GVAR, /* $global_var */ + MIRB_TOK_REGEXP, /* /regexp/ */ + MIRB_TOK_MAX +} mirb_token_type; + +/* + * Color theme + */ +typedef enum mirb_theme { + MIRB_THEME_DARK, /* light text on dark background (default) */ + MIRB_THEME_LIGHT /* dark text on light background */ +} mirb_theme; + +/* + * Highlighter state + */ +typedef struct mirb_highlighter { + mirb_theme theme; + mrb_bool enabled; + /* Multi-line state tracking */ + mrb_bool in_string; + char string_quote; /* '"' or '\'' */ + mrb_bool in_heredoc; + mrb_bool in_regexp; +} mirb_highlighter; + +/* + * Initialize highlighter with auto-detected or specified theme + */ +void mirb_highlight_init(mirb_highlighter *hl, mrb_bool enabled); + +/* + * Set theme explicitly + */ +void mirb_highlight_set_theme(mirb_highlighter *hl, mirb_theme theme); + +/* + * Detect theme from environment variables + * Returns MIRB_THEME_DARK if cannot detect + */ +mirb_theme mirb_highlight_detect_theme(void); + +/* + * Print a line with syntax highlighting + * Handles multi-line strings/comments by tracking state + */ +void mirb_highlight_print_line(mirb_highlighter *hl, const char *line); + +/* + * Reset multi-line state (call when starting new input) + */ +void mirb_highlight_reset(mirb_highlighter *hl); + +#endif /* MIRB_HIGHLIGHT_H */