From 958bca6e014715a626e698ad160262784642cd98 Mon Sep 17 00:00:00 2001 From: Daniel Hammer Date: Sun, 5 Apr 2026 22:09:39 +0200 Subject: [PATCH] feat: add syntax highlighting to edit command --- programs/include/edit/syntax_ansi.hpp | 497 ++++++++++++++++++++++++++ programs/include/gui/terminal.hpp | 45 +++ programs/src/edit/main.cpp | 65 +++- 3 files changed, 605 insertions(+), 2 deletions(-) create mode 100644 programs/include/edit/syntax_ansi.hpp diff --git a/programs/include/edit/syntax_ansi.hpp b/programs/include/edit/syntax_ansi.hpp new file mode 100644 index 0000000..a2f39d8 --- /dev/null +++ b/programs/include/edit/syntax_ansi.hpp @@ -0,0 +1,497 @@ +/* + * syntax_ansi.hpp + * ANSI terminal syntax highlighting for C and Lua + * Used by the MontaukOS CLI text editor + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once + +#include + +// ============================================================================ +// Token types +// ============================================================================ + +enum SynLanguage : uint8_t { + SYN_LANG_NONE, + SYN_LANG_C, + SYN_LANG_LUA, +}; + +enum SynToken : uint8_t { + SYN_NORMAL, + SYN_KEYWORD, + SYN_TYPE, + SYN_PREPROCESSOR, + SYN_STRING, + SYN_CHAR, + SYN_COMMENT, + SYN_NUMBER, + SYN_OPERATOR, +}; + +struct SynState { + int in_block_comment; // 0 or 1 (bool stored as int for freestanding) + SynToken long_token; + int long_bracket_eqs; +}; + +// ============================================================================ +// Helpers +// ============================================================================ + +inline bool syn_is_alpha(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; +} + +inline bool syn_is_alnum(char c) { + return syn_is_alpha(c) || (c >= '0' && c <= '9'); +} + +inline bool syn_is_digit(char c) { + return c >= '0' && c <= '9'; +} + +inline bool syn_is_hex(char c) { + return syn_is_digit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); +} + +inline void syn_set_token(SynToken* out, int out_len, int idx, SynToken tok) { + if (out && idx >= 0 && idx < out_len) + out[idx] = tok; +} + +inline void syn_fill_tokens(SynToken* out, int out_len, int start, int end, SynToken tok) { + if (!out) return; + if (start < 0) start = 0; + if (end > out_len) end = out_len; + for (int i = start; i < end; i++) + out[i] = tok; +} + +inline bool syn_streq(const char* buf, int len, const char* kw) { + int i = 0; + while (i < len && kw[i]) { + if (buf[i] != kw[i]) return false; + i++; + } + return i == len && kw[i] == '\0'; +} + +// ============================================================================ +// Keyword / type classification +// ============================================================================ + +inline SynToken syn_classify_c_word(const char* buf, int len) { + static const char* keywords[] = { + "auto", "break", "case", "const", "continue", "default", "do", + "else", "enum", "extern", "for", "goto", "if", "inline", + "register", "restrict", "return", "sizeof", "static", "struct", + "switch", "typedef", "union", "volatile", "while", + "NULL", "true", "false", "nullptr", + }; + static const char* types[] = { + "void", "char", "short", "int", "long", "float", "double", + "signed", "unsigned", "bool", "_Bool", + "int8_t", "int16_t", "int32_t", "int64_t", + "uint8_t", "uint16_t", "uint32_t", "uint64_t", + "size_t", "ssize_t", "ptrdiff_t", "intptr_t", "uintptr_t", + "FILE", + }; + for (int i = 0; i < (int)(sizeof(keywords) / sizeof(keywords[0])); i++) { + if (syn_streq(buf, len, keywords[i])) return SYN_KEYWORD; + } + for (int i = 0; i < (int)(sizeof(types) / sizeof(types[0])); i++) { + if (syn_streq(buf, len, types[i])) return SYN_TYPE; + } + return SYN_NORMAL; +} + +inline SynToken syn_classify_lua_word(const char* buf, int len) { + static const char* keywords[] = { + "and", "break", "do", "else", "elseif", "end", "false", + "for", "function", "goto", "if", "in", "local", "nil", + "not", "or", "repeat", "return", "then", "true", + "until", "while", + }; + static const char* builtins[] = { + "_ENV", "_G", "_VERSION", + "assert", "collectgarbage", "coroutine", "debug", "dofile", + "error", "getmetatable", "io", "ipairs", "load", "loadfile", + "math", "next", "os", "package", "pairs", "pcall", + "print", "rawequal", "rawget", "rawlen", "rawset", "require", + "select", "setmetatable", "string", "table", "tonumber", + "tostring", "type", "utf8", "warn", "xpcall", + }; + for (int i = 0; i < (int)(sizeof(keywords) / sizeof(keywords[0])); i++) { + if (syn_streq(buf, len, keywords[i])) return SYN_KEYWORD; + } + for (int i = 0; i < (int)(sizeof(builtins) / sizeof(builtins[0])); i++) { + if (syn_streq(buf, len, builtins[i])) return SYN_TYPE; + } + return SYN_NORMAL; +} + +inline SynToken syn_classify_word(SynLanguage lang, const char* buf, int len) { + switch (lang) { + case SYN_LANG_C: return syn_classify_c_word(buf, len); + case SYN_LANG_LUA: return syn_classify_lua_word(buf, len); + default: return SYN_NORMAL; + } +} + +inline SynState syn_make_state() { + SynState state = {}; + state.in_block_comment = 0; + state.long_token = SYN_NORMAL; + state.long_bracket_eqs = -1; + return state; +} + +// ============================================================================ +// Lua long bracket matching +// ============================================================================ + +inline bool syn_match_lua_long_bracket_open(const char* line, int len, int i, + int& eqs, int& span) { + if (i >= len || line[i] != '[') return false; + int j = i + 1; + while (j < len && line[j] == '=') j++; + if (j < len && line[j] == '[') { + eqs = j - i - 1; + span = j - i + 1; + return true; + } + return false; +} + +inline bool syn_match_lua_long_bracket_close(const char* line, int len, int i, + int eqs, int& span) { + if (i >= len || line[i] != ']') return false; + int j = i + 1; + for (int k = 0; k < eqs; k++) { + if (j >= len || line[j] != '=') return false; + j++; + } + if (j < len && line[j] == ']') { + span = j - i + 1; + return true; + } + return false; +} + +// ============================================================================ +// Number consumption +// ============================================================================ + +inline void syn_consume_number(const char* line, int len, int& i, + SynToken* out, int out_len, int c_style_suffixes) { + int start = i; + if (line[i] == '0' && i + 1 < len && (line[i + 1] == 'x' || line[i + 1] == 'X')) { + i += 2; + while (i < len && syn_is_hex(line[i])) i++; + if (i < len && line[i] == '.' && !(i + 1 < len && line[i + 1] == '.')) { + i++; + while (i < len && syn_is_hex(line[i])) i++; + } + if (i < len && (line[i] == 'p' || line[i] == 'P')) { + int exp = i + 1; + if (exp < len && (line[exp] == '+' || line[exp] == '-')) exp++; + if (exp < len && syn_is_digit(line[exp])) { + i = exp + 1; + while (i < len && syn_is_digit(line[i])) i++; + } + } + } else { + if (line[i] == '.') i++; + while (i < len && syn_is_digit(line[i])) i++; + if (i < len && line[i] == '.' && !(i + 1 < len && line[i + 1] == '.')) { + i++; + while (i < len && syn_is_digit(line[i])) i++; + } + if (i < len && (line[i] == 'e' || line[i] == 'E')) { + int exp = i + 1; + if (exp < len && (line[exp] == '+' || line[exp] == '-')) exp++; + if (exp < len && syn_is_digit(line[exp])) { + i = exp + 1; + while (i < len && syn_is_digit(line[i])) i++; + } + } + } + if (c_style_suffixes) { + while (i < len && (line[i] == 'u' || line[i] == 'U' || + line[i] == 'l' || line[i] == 'L' || + line[i] == 'f' || line[i] == 'F')) + i++; + } + syn_fill_tokens(out, out_len, start, i, SYN_NUMBER); +} + +// ============================================================================ +// C highlighting +// ============================================================================ + +inline void syn_highlight_line_c(const char* line, int len, SynToken* out, int out_len, + SynState& state) { + int i = 0; + while (i < len) { + if (state.in_block_comment) { + while (i < len) { + if (i + 1 < len && line[i] == '*' && line[i + 1] == '/') { + syn_set_token(out, out_len, i, SYN_COMMENT); + syn_set_token(out, out_len, i + 1, SYN_COMMENT); + i += 2; + state.in_block_comment = 0; + break; + } + syn_set_token(out, out_len, i, SYN_COMMENT); + i++; + } + continue; + } + char c = line[i]; + if (c == '/' && i + 1 < len && line[i + 1] == '/') { + syn_fill_tokens(out, out_len, i, len, SYN_COMMENT); + break; + } + if (c == '/' && i + 1 < len && line[i + 1] == '*') { + state.in_block_comment = 1; + syn_set_token(out, out_len, i, SYN_COMMENT); + syn_set_token(out, out_len, i + 1, SYN_COMMENT); + i += 2; + continue; + } + if (c == '#') { + int is_pp = 1; + for (int j = 0; j < i; j++) { + if (line[j] != ' ' && line[j] != '\t') { is_pp = 0; break; } + } + if (is_pp) { + while (i < len) { + if (i + 1 < len && line[i] == '/' && line[i + 1] == '/') { + syn_fill_tokens(out, out_len, i, len, SYN_COMMENT); + break; + } + if (i + 1 < len && line[i] == '/' && line[i + 1] == '*') { + state.in_block_comment = 1; + syn_set_token(out, out_len, i, SYN_COMMENT); + syn_set_token(out, out_len, i + 1, SYN_COMMENT); + i += 2; + while (i < len) { + if (i + 1 < len && line[i] == '*' && line[i + 1] == '/') { + syn_set_token(out, out_len, i, SYN_COMMENT); + syn_set_token(out, out_len, i + 1, SYN_COMMENT); + i += 2; + state.in_block_comment = 0; + break; + } + syn_set_token(out, out_len, i, SYN_COMMENT); + i++; + } + continue; + } + syn_set_token(out, out_len, i, SYN_PREPROCESSOR); + i++; + } + continue; + } + } + if (c == '"') { + syn_set_token(out, out_len, i, SYN_STRING); + i++; + while (i < len) { + if (line[i] == '\\' && i + 1 < len) { + syn_set_token(out, out_len, i, SYN_STRING); + syn_set_token(out, out_len, i + 1, SYN_STRING); + i += 2; + continue; + } + if (line[i] == '"') { + syn_set_token(out, out_len, i, SYN_STRING); + i++; + break; + } + syn_set_token(out, out_len, i, SYN_STRING); + i++; + } + continue; + } + if (c == '\'') { + syn_set_token(out, out_len, i, SYN_CHAR); + i++; + while (i < len) { + if (line[i] == '\\' && i + 1 < len) { + syn_set_token(out, out_len, i, SYN_CHAR); + syn_set_token(out, out_len, i + 1, SYN_CHAR); + i += 2; + continue; + } + if (line[i] == '\'') { + syn_set_token(out, out_len, i, SYN_CHAR); + i++; + break; + } + syn_set_token(out, out_len, i, SYN_CHAR); + i++; + } + continue; + } + if (syn_is_digit(c) || (c == '.' && i + 1 < len && syn_is_digit(line[i + 1]))) { + syn_consume_number(line, len, i, out, out_len, 1); + continue; + } + if (syn_is_alpha(c)) { + int start = i; + while (i < len && syn_is_alnum(line[i])) i++; + SynToken tok = syn_classify_word(SYN_LANG_C, line + start, i - start); + syn_fill_tokens(out, out_len, start, i, tok); + continue; + } + if (c == '+' || c == '-' || c == '*' || c == '/' || c == '%' || + c == '=' || c == '!' || c == '<' || c == '>' || c == '&' || + c == '|' || c == '^' || c == '~' || c == '?' || c == ':') { + syn_set_token(out, out_len, i, SYN_OPERATOR); + i++; + continue; + } + syn_set_token(out, out_len, i, SYN_NORMAL); + i++; + } +} + +// ============================================================================ +// Lua highlighting +// ============================================================================ + +inline void syn_highlight_line_lua(const char* line, int len, SynToken* out, int out_len, + SynState& state) { + int i = 0; + while (i < len) { + if (state.long_token != SYN_NORMAL) { + int span = 0; + if (syn_match_lua_long_bracket_close(line, len, i, state.long_bracket_eqs, span)) { + syn_fill_tokens(out, out_len, i, i + span, state.long_token); + i += span; + state.long_token = SYN_NORMAL; + state.long_bracket_eqs = -1; + continue; + } + syn_set_token(out, out_len, i, state.long_token); + i++; + continue; + } + char c = line[i]; + if (c == '-' && i + 1 < len && line[i + 1] == '-') { + int eqs = 0; + int span = 0; + if (i + 2 < len && syn_match_lua_long_bracket_open(line, len, i + 2, eqs, span)) { + syn_fill_tokens(out, out_len, i, i + 2 + span, SYN_COMMENT); + i += 2 + span; + state.long_token = SYN_COMMENT; + state.long_bracket_eqs = eqs; + continue; + } + syn_fill_tokens(out, out_len, i, len, SYN_COMMENT); + break; + } + if (c == '"' || c == '\'') { + char quote = c; + syn_set_token(out, out_len, i, SYN_STRING); + i++; + while (i < len) { + if (line[i] == '\\' && i + 1 < len) { + syn_set_token(out, out_len, i, SYN_STRING); + syn_set_token(out, out_len, i + 1, SYN_STRING); + i += 2; + continue; + } + syn_set_token(out, out_len, i, SYN_STRING); + if (line[i] == quote) { + i++; + break; + } + i++; + } + continue; + } + int eqs = 0; + int span = 0; + if (syn_match_lua_long_bracket_open(line, len, i, eqs, span)) { + syn_fill_tokens(out, out_len, i, i + span, SYN_STRING); + i += span; + state.long_token = SYN_STRING; + state.long_bracket_eqs = eqs; + continue; + } + if (syn_is_digit(c) || (c == '.' && i + 1 < len && syn_is_digit(line[i + 1]))) { + syn_consume_number(line, len, i, out, out_len, 0); + continue; + } + if (syn_is_alpha(c)) { + int start = i; + while (i < len && syn_is_alnum(line[i])) i++; + SynToken tok = syn_classify_word(SYN_LANG_LUA, line + start, i - start); + syn_fill_tokens(out, out_len, start, i, tok); + continue; + } + if (c == '+' || c == '-' || c == '*' || c == '/' || c == '%' || + c == '^' || c == '#' || c == '=' || c == '<' || c == '>' || + c == '~' || c == ':' || c == '.' || c == ',' || c == ';' || + c == '(' || c == ')' || c == '{' || c == '}' || + c == '[' || c == ']') { + syn_set_token(out, out_len, i, SYN_OPERATOR); + i++; + continue; + } + syn_set_token(out, out_len, i, SYN_NORMAL); + i++; + } +} + +// ============================================================================ +// Per-line highlighter +// ============================================================================ + +inline void syn_highlight_line(const char* line, int len, SynToken* out, int out_len, + SynLanguage lang, SynState& state) { + if (!line || len <= 0) return; + switch (lang) { + case SYN_LANG_C: + syn_highlight_line_c(line, len, out, out_len, state); + break; + case SYN_LANG_LUA: + syn_highlight_line_lua(line, len, out, out_len, state); + break; + default: + syn_fill_tokens(out, out_len, 0, len, SYN_NORMAL); + break; + } +} + +// ============================================================================ +// Language detection +// ============================================================================ + +inline bool syn_path_ends_with(const char* path, const char* suffix) { + if (!path || !suffix) return false; + int path_len = 0; + while (path[path_len]) path_len++; + int suffix_len = 0; + while (suffix[suffix_len]) suffix_len++; + if (path_len < suffix_len) return false; + for (int i = 0; i < suffix_len; i++) { + if (path[path_len - suffix_len + i] != suffix[i]) + return false; + } + return true; +} + +inline SynLanguage syn_detect_language(const char* filepath) { + if (!filepath || filepath[0] == '\0') return SYN_LANG_NONE; + if (syn_path_ends_with(filepath, ".c") || syn_path_ends_with(filepath, ".h")) + return SYN_LANG_C; + if (syn_path_ends_with(filepath, ".lua")) + return SYN_LANG_LUA; + return SYN_LANG_NONE; +} \ No newline at end of file diff --git a/programs/include/gui/terminal.hpp b/programs/include/gui/terminal.hpp index 65c1a54..8b6c52e 100644 --- a/programs/include/gui/terminal.hpp +++ b/programs/include/gui/terminal.hpp @@ -69,6 +69,23 @@ static inline Color term_ansi_color(int idx) { } } +// ANSI 256-color palette (0-15 = standard, 16-231 = RGB cube, 232-255 = grayscale) +static inline Color term_ansi_256_color(int idx) { + if (idx < 0) return colors::TERM_FG; + if (idx <= 15) return term_ansi_color(idx); + if (idx <= 231) { + // 6x6x6 RGB cube: idx 16 = (0,0,0), idx 231 = (5,5,5) + int r = ((idx - 16) / 36) * 51; + int g = (((idx - 16) % 36) / 6) * 51; + int b = ((idx - 16) % 6) * 51; + return Color::from_rgb(r, g, b); + } + // Grayscale: idx 232 = #080808, idx 255 = #eeeeee + int gray = 8 + (idx - 232) * 10; + if (gray > 238) gray = 238; + return Color::from_rgb(gray, gray, gray); +} + // Helper: pointer to start of screen-relative row r in the cells buffer static inline TermCell* term_screen_row(TerminalState* t, int r) { return &t->cells[(t->scrollback_lines + r) * t->cols]; @@ -330,6 +347,34 @@ static inline void terminal_process_csi(TerminalState* t, char cmd) { // SGR - Set Graphics Rendition for (int i = 0; i < t->csi_param_count; i++) { int code = t->csi_params[i]; + + // Handle extended color sequences: 38;5;N or 48;5;N or 38;2;R;G;B or 48;2;R;G;B + if ((code == 38 || code == 48) && i + 2 < t->csi_param_count) { + if (t->csi_params[i + 1] == 5) { + // 256-color: 38;5;N or 48;5;N + int color_idx = t->csi_params[i + 2]; + if (code == 38) { + t->current_fg = term_ansi_256_color(color_idx); + } else { + t->current_bg = term_ansi_256_color(color_idx); + } + i += 2; + continue; + } else if (t->csi_params[i + 1] == 2 && i + 4 < t->csi_param_count) { + // Truecolor: 38;2;R;G;B or 48;2;R;G;B + int r = t->csi_params[i + 2]; + int g = t->csi_params[i + 3]; + int b = t->csi_params[i + 4]; + if (code == 38) { + t->current_fg = Color::from_rgb(r, g, b); + } else { + t->current_bg = Color::from_rgb(r, g, b); + } + i += 4; + continue; + } + } + if (code == 0) { t->current_fg = colors::TERM_FG; t->current_bg = colors::TERM_BG; diff --git a/programs/src/edit/main.cpp b/programs/src/edit/main.cpp index 3256cef..b70930d 100644 --- a/programs/src/edit/main.cpp +++ b/programs/src/edit/main.cpp @@ -12,6 +12,8 @@ using montauk::slen; using montauk::memcpy; using montauk::memmove; +#include + // ---- Integer to string ---- static int itoa(int val, char* buf) { @@ -61,6 +63,33 @@ static void reset_attrs() { esc("0m"); } static void reverse_video() { esc("7m"); } static void dim_text() { esc("2m"); } +// ---- Syntax highlighting state ---- + +static SynState synState = {}; +static SynLanguage synLang = SYN_LANG_NONE; + +// ANSI color for token type (must be called before each colored character) +// Uses ANSI 256-color codes (38;5;N) for rich terminal colors +static void syn_ansi_color(SynToken tok) { + // 256-color foreground: \033[38;5;Nm + putch('\033'); putch('['); + switch (tok) { + case SYN_KEYWORD: print("38;5;13m"); break; // magenta + case SYN_TYPE: print("38;5;14m"); break; // cyan + case SYN_PREPROCESSOR: print("38;5;141m"); break; // purple + case SYN_STRING: print("38;5;71m"); break; // green + case SYN_CHAR: print("38;5;71m"); break; // green + case SYN_COMMENT: print("38;5;8m"); break; // gray + case SYN_NUMBER: print("38;5;214m"); break; // orange + case SYN_OPERATOR: print("38;5;15m"); break; // white + default: print("0m"); break; + } +} + +static void syn_ansi_reset() { + reset_attrs(); +} + // ---- Line buffer ---- struct Line { @@ -207,6 +236,10 @@ static void build_path(const char* fname, char* out, int outMax) { } static void load_file(const char* fname) { + // Detect language before potentially creating a new file + synLang = syn_detect_language(fname); + synState = syn_make_state(); + char path[256]; build_path(fname, path, sizeof(path)); @@ -444,9 +477,35 @@ static void draw_line(int screenRow, int docLine) { Line* ln = &lines[docLine]; int startCol = leftCol; int maxChars = screenCols - gutterWidth; + int visibleLen = 0; + if (startCol < ln->len) { + visibleLen = ln->len - startCol; + if (visibleLen > maxChars) visibleLen = maxChars; + } - for (int c = 0; c < maxChars && startCol + c < ln->len; c++) { - putch(ln->data[startCol + c]); + if (synLang != SYN_LANG_NONE && visibleLen > 0) { + // Tokenize visible portion for syntax highlighting + static SynToken tokens[256]; + int tokCount = visibleLen; + if (tokCount > 256) tokCount = 256; + syn_fill_tokens(tokens, tokCount, 0, tokCount, SYN_NORMAL); + syn_highlight_line(ln->data + startCol, visibleLen, tokens, tokCount, synLang, synState); + + // Output with ANSI colors + SynToken prevTok = SYN_NORMAL; + for (int c = 0; c < tokCount; c++) { + if (c == 0 || tokens[c] != prevTok) { + syn_ansi_color(tokens[c]); + prevTok = tokens[c]; + } + putch(ln->data[startCol + c]); + } + syn_ansi_reset(); + } else { + // No highlighting - plain output + for (int c = 0; c < visibleLen; c++) { + putch(ln->data[startCol + c]); + } } } else { // Past end of file @@ -798,6 +857,8 @@ extern "C" void _start() { // New empty buffer numLines = 1; line_init(&lines[0]); + synLang = SYN_LANG_NONE; + synState = syn_make_state(); } // Enter alternate screen