mirror of
https://github.com/mruby/mruby
synced 2026-06-08 16:11:16 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
/*
|
||||
|
||||
@@ -0,0 +1,425 @@
|
||||
/*
|
||||
** mirb_highlight.c - Syntax highlighting for mirb
|
||||
**
|
||||
** See Copyright Notice in mruby.h
|
||||
*/
|
||||
|
||||
#include "mirb_highlight.h"
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <ctype.h>
|
||||
|
||||
/* 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++);
|
||||
}
|
||||
}
|
||||
@@ -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 <mruby.h>
|
||||
#include <stdio.h>
|
||||
|
||||
/*
|
||||
* 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 */
|
||||
Reference in New Issue
Block a user