From fc9383213009bf5eefbd064da8ca14fd013c3cd6 Mon Sep 17 00:00:00 2001 From: Arthur Barraux Date: Sun, 11 Jan 2026 19:41:30 +0100 Subject: [PATCH] Syntax highlighting and comment --- include/data.h | 1 + include/lisp.h | 5 +- include/syntax_highlighter.h | 20 ++++ main.c | 2 +- meson.build | 1 + src/builtins.c | 220 ++++++++++++++++++++++++++++++++++- src/file_io.c | 64 +++++++++- src/init.c | 13 +-- src/input.c | 94 +++++++++++++-- src/output.c | 65 ++++++++++- src/syntax_highlighter.c | 163 ++++++++++++++++++++++++++ 11 files changed, 623 insertions(+), 25 deletions(-) create mode 100644 include/syntax_highlighter.h create mode 100644 src/syntax_highlighter.c diff --git a/include/data.h b/include/data.h index 1e1cde9..4e6a4dd 100644 --- a/include/data.h +++ b/include/data.h @@ -67,6 +67,7 @@ struct editorConfig { struct const_t constantes; int quit_times_buffer; + char *init_file_path; FILE *fd_init_file; Lisp env; LispContext ctx; /** Lisp context */ diff --git a/include/lisp.h b/include/lisp.h index 29a6728..d003bed 100644 --- a/include/lisp.h +++ b/include/lisp.h @@ -2075,8 +2075,10 @@ static Lisp parse(Lexer* lex, LispError* out_error, LispContext ctx) result = lisp_cons(get_sym(SYM_BEGIN, ctx), lisp_list_reverse(result), ctx); } - if (out_error) *out_error = error; + if (out_error) + *out_error = error; return result; + } Lisp lisp_read(const char *program, LispError* out_error, LispContext ctx) @@ -2122,6 +2124,7 @@ void* fread_all_(FILE* file, size_t* out_size) { return NULL; } data = new_data; + memset(data + *out_size, 0, cap - *out_size); } size_t read = fread(data + *out_size, 1, BLOCK_SIZE, file); diff --git a/include/syntax_highlighter.h b/include/syntax_highlighter.h new file mode 100644 index 0000000..47d3059 --- /dev/null +++ b/include/syntax_highlighter.h @@ -0,0 +1,20 @@ +#define COLOR_RESET "\033[0m" +#define COLOR_KEYWORD "\033[1;35m" // Bold magenta +#define COLOR_TYPE "\033[1;34m" // Bold blue +#define COLOR_STRING "\033[1;32m" // Bold green +#define COLOR_COMMENT "\033[0;36m" // Cyan +#define COLOR_NUMBER "\033[1;33m" // Bold yellow +#define COLOR_DEFAULT "\033[0;37m" // White + +// Token types +typedef enum { + TOKEN_KEYWORD, + TOKEN_TYPE, + TOKEN_STRING, + TOKEN_COMMENT, + TOKEN_NUMBER, + TOKEN_OPERATOR, + TOKEN_DEFAULT +} TokenType; + +char *highlight_line(const char * line, int *length); diff --git a/main.c b/main.c index 4dd8492..13c10ff 100644 --- a/main.c +++ b/main.c @@ -40,7 +40,7 @@ int main(int argc, char *argv[]) { } free(splash_screen); - editorSetStatusMessage("HELP: Ctrl-S = save | Ctrl-Q = quit"); + editorSetStatusMessage("HELP: Ctrl-x Ctrl-s = save | Ctrl-x Ctrl-q = quit"); while (1) { diff --git a/meson.build b/meson.build index 37ebd43..89781e3 100644 --- a/meson.build +++ b/meson.build @@ -13,6 +13,7 @@ src_files = files( 'main.c', 'src/append_buffer.c', 'src/editor_op.c', + 'src/syntax_highlighter.c', 'src/file_io.c', 'src/init.c', 'src/input.c', diff --git a/src/builtins.c b/src/builtins.c index b755521..5d53853 100644 --- a/src/builtins.c +++ b/src/builtins.c @@ -1,3 +1,10 @@ +/** + * @file builtins.c + * @brief Built-in Lisp functions for editor operations + * @details Provides Lisp bindings for core editor functionality including + * cursor movement, file operations, keybinding management, and text manipulation + */ + #include "../include/builtins.h" #include "../include/data.h" #include "../include/define.h" @@ -11,6 +18,14 @@ #include #include +/** + * @brief Finds a prefix configuration by name + * @details Searches the prefix array for a prefix matching the given name. + * Returns the first prefix (default) if no match is found. + * @param prefix_name Name of the prefix to search for (max 64 chars) + * @return Matching prefix_t structure, or E.prefix[0] if not found + * @note Updates global editor state E + */ struct prefix_t find_prefix(const char prefix_name[64]) { int i = E.number_of_prefix + 1; while (i--) { @@ -21,6 +36,19 @@ struct prefix_t find_prefix(const char prefix_name[64]) { return E.prefix[0]; } +/** + * @brief Lisp function to bind a command to a key sequence + * @details Registers a keybinding with an associated Lisp command and optional + * prefix modifier. Dynamically extends the keybind array. + * @param args Lisp list of 3 arguments: (key-sequence command prefix-name) + * - key_sequence (string): The key or key combination to bind + * - command (Lisp): The Lisp command/function to execute + * - prefix_name (string): Prefix modifier name + * @param e Error pointer for Lisp error handling + * @param ctx Lisp context + * @return lisp_null() + * @note Updates global editor state E (key_binds array) + */ Lisp mapKey(Lisp args, LispError *e, LispContext ctx) { /* * 3 arguments keybind command prefix @@ -49,6 +77,17 @@ Lisp mapKey(Lisp args, LispError *e, LispContext ctx) { return lisp_null(); } +/** + * @brief Lisp function to move cursor in a specified direction + * @details Moves the editor cursor up, down, left, or right based on direction string. + * @param args Lisp list with one argument: direction string + * - "u": up, "d": down, "r": right, "l": left + * @param e Error pointer for Lisp error handling + * @param ctx Lisp context + * @return Lisp boolean indicating whether movement was valid + * @note Updates global editor state E + * @see editorMoveCursor() + */ Lisp moveCursor(Lisp args, LispError *e, LispContext ctx) { const char *direction = lisp_string(lisp_car(args)); int is_in = 0; @@ -70,6 +109,12 @@ Lisp moveCursor(Lisp args, LispError *e, LispContext ctx) { return lisp_make_bool(is_in); } +/** + * @brief Frees all dynamically allocated editor structures + * @details Releases memory for prefix table, keybinds, filename, and all rows. + * Called during shutdown to prevent memory leaks. + * @note Updates global editor state E + */ void free_structs(void) { int i; free(E.prefix); @@ -78,12 +123,28 @@ void free_structs(void) { } free(E.key_binds); free(E.filename); - free(E.row->chars); - free(E.row->render); + for (i = 0; i < E.numrows; ++i) { + free(E.row[i].render); + free(E.row[i].chars); + } free(E.row); + free(E.init_file_path); + fclose(E.fd_init_file); } +/** + * @brief Lisp function to quit the editor + * @details Closes editor with unsaved changes protection. Prompts user to confirm + * quit after multiple attempts if file is dirty. Cleans up resources and restores terminal. + * @param args Lisp arguments (unused) + * @param e Error pointer for Lisp error handling + * @param ctx Lisp context + * @return lisp_null() (never returns on successful exit) + * @note Calls exit(0) to terminate program + * @note Updates quit_times_buffer counter + * @see free_structs() + */ Lisp editorQuit(Lisp args, LispError *e, LispContext ctx) { if (E.dirty && E.quit_times_buffer > 0) { editorSetStatusMessage("WARNING! Changes hasn't been saved. Press Ctrl-Q " @@ -101,6 +162,15 @@ Lisp editorQuit(Lisp args, LispError *e, LispContext ctx) { return lisp_null(); } +/** + * @brief Lisp function to save the current file + * @details Wrapper around editorSave() for use in Lisp keybindings. + * @param args Lisp arguments (unused) + * @param e Error pointer for Lisp error handling + * @param ctx Lisp context + * @return lisp_null() + * @see editorSave() + */ Lisp l_editorSave(Lisp args, LispError *e, LispContext ctx) { editorSave(); @@ -108,6 +178,15 @@ Lisp l_editorSave(Lisp args, LispError *e, LispContext ctx) { return lisp_null(); } +/** + * @brief Lisp function to insert a new line at cursor + * @details Wrapper around editorInsertNewLine() for use in Lisp keybindings. + * @param args Lisp arguments (unused) + * @param e Error pointer for Lisp error handling + * @param ctx Lisp context + * @return lisp_null() + * @see editorInsertNewLine() + */ Lisp l_editorInsertNewLine(Lisp args, LispError *e, LispContext ctx) { editorInsertNewLine(); @@ -115,6 +194,15 @@ Lisp l_editorInsertNewLine(Lisp args, LispError *e, LispContext ctx) { return lisp_null(); } +/** + * @brief Lisp function to insert a tab (spaces) at cursor + * @details Inserts TAB_LENGTH spaces at the current cursor position. + * @param args Lisp arguments (unused) + * @param e Error pointer for Lisp error handling + * @param ctx Lisp context + * @return lisp_null() + * @note Uses E.constantes.TAB_LENGTH for indentation width + */ Lisp l_editorInserTab(Lisp args, LispError *e, LispContext ctx) { for (int i = 0; i E.numrows) { @@ -163,6 +298,17 @@ Lisp editorMoveCursorPageDown(Lisp args, LispError *e, LispContext ctx) { return lisp_null(); } +/** + * @brief Lisp function to open a file + * @details Prompts user for filename and opens the file for editing. + * @param args Lisp arguments (unused) + * @param e Error pointer for Lisp error handling + * @param ctx Lisp context + * @return lisp_null() + * @note Updates global editor state E + * @see editorOpen() + * @see editorPrompt() + */ Lisp editorOpenFile(Lisp args, LispError *e, LispContext ctx) { char *filename = editorPrompt("Open : %s", getenv("PWD"), 1); if (filename){ @@ -174,12 +320,32 @@ Lisp editorOpenFile(Lisp args, LispError *e, LispContext ctx) { return lisp_null(); } +/** + * @brief Lisp function to insert a character + * @details Extracts a character from Lisp string argument and inserts it at cursor. + * @param args Lisp list with one string argument + * @param e Error pointer for Lisp error handling + * @param ctx Lisp context + * @return lisp_null() + * @note Uses first character of the string argument + */ Lisp editorPrintC(Lisp args, LispError *e, LispContext ctx) { char c = lisp_string(lisp_car(args))[0]; editorInsertChar(c); return lisp_null(); } +/** + * @brief Lisp function to load and execute a package + * @details Loads a Lisp package from the user's packages directory + * (~/.beluga/packages//init.lisp) and evaluates it. + * @param args Lisp list with one argument: package name (string) + * @param e Error pointer for Lisp error handling + * @param ctx Lisp context + * @return lisp_null() + * @note Package files must be valid Lisp code + * @note Package directory defaults to ~/.beluga/packages/ + */ Lisp addPackage(Lisp args, LispError *e, LispContext ctx) { const char *package_name = lisp_string(lisp_car(args)); fprintf(stderr, "%s\n", package_name); @@ -199,16 +365,45 @@ Lisp addPackage(Lisp args, LispError *e, LispContext ctx) { return lisp_null(); } +/** + * @brief Lisp function to delete the current row + * @details Removes the line at the current cursor position. + * @param args Lisp arguments (unused) + * @param e Error pointer for Lisp error handling + * @param ctx Lisp context + * @return lisp_null() + * @note Updates global editor state E + * @see editorDelRow() + */ Lisp editorDelRow_L(Lisp args, LispError *e, LispContext ctx) { editorDelRow(E.cursor_y); return lisp_null(); } +/** + * @brief Lisp function to search for text + * @details Wrapper around editorFind() for use in Lisp keybindings. + * @param args Lisp arguments (unused) + * @param e Error pointer for Lisp error handling + * @param ctx Lisp context + * @return lisp_null() + * @see editorFind() + */ Lisp editorFind_L(Lisp args, LispError *e, LispContext ctx) { + fprintf(stderr, "LispFind\n"); editorFind(); return lisp_null(); } +/** + * @brief Lisp function to read character at cursor + * @details Returns the character at the current cursor position as a Lisp character. + * Returns 'a' if at end of line. + * @param args Lisp arguments (unused) + * @param e Error pointer for Lisp error handling + * @param ctx Lisp context + * @return Lisp character object representing the character at cursor + */ Lisp editorReadChar_L(Lisp args, LispError *e, LispContext ctx) { Lisp returned_char; if (E.row[E.cursor_y].render[E.cursor_x] == 0) { @@ -220,6 +415,17 @@ Lisp editorReadChar_L(Lisp args, LispError *e, LispContext ctx) { return returned_char; } +/** + * @brief Lisp function to set the current prefix mode + * @details Changes the editor's prefix state to a named prefix, affecting which + * keybindings are active. Updates status message to show active prefix. + * @param args Lisp list with one argument: prefix name (string) + * @param e Error pointer for Lisp error handling + * @param ctx Lisp context + * @return lisp_null() + * @note Updates global editor state E (prefix_state) + * @see find_prefix() + */ Lisp editorSetPrefix(Lisp args, LispError *e, LispContext ctx) { /* * Set the prefix state of editor to the prefix in argument @@ -233,6 +439,16 @@ Lisp editorSetPrefix(Lisp args, LispError *e, LispContext ctx) { return lisp_null(); } +/** + * @brief Lisp function to define a new prefix modifier + * @details Registers a named prefix modifier that can be used with keybindings + * to create context-aware command sequences (e.g., Ctrl-X as a prefix). + * @param args Lisp list with one argument: prefix name (string, max 64 chars) + * @param e Error pointer for Lisp error handling + * @param ctx Lisp context + * @return lisp_null() + * @note Updates global editor state E (prefix array) + */ Lisp editorPrefix(Lisp args, LispError *e, LispContext ctx) { E.prefix = (struct prefix_t *)realloc(E.prefix, (++(E.number_of_prefix) + 1) * sizeof(struct prefix_t)); diff --git a/src/file_io.c b/src/file_io.c index a0fb3a7..312bfd4 100644 --- a/src/file_io.c +++ b/src/file_io.c @@ -1,3 +1,11 @@ +/** + * @file file_io.c + * @brief File I/O operations module for the Beluga text editor + * @details Handles file loading, saving, searching, and buffer management. + * Provides functionality for opening/closing files, persisting changes to disk, + * and searching for text patterns within the document. + */ + #include "../include/file_io.h" #include "../include/input.h" #include "../include/output.h" @@ -14,6 +22,15 @@ extern ssize_t getline(char **restrict lineptr, size_t *restrict n, extern int ftruncate(int fd, off_t length); extern struct editorConfig E; +/** + * @brief Converts all editor rows to a single string buffer + * @details Concatenates all row content into a single allocated buffer with + * newlines between rows. Useful for file saving and buffer operations. + * @param buffer_len Pointer to integer where total buffer length will be stored + * @return Pointer to dynamically allocated buffer containing all row data. + * Rows are separated by newline characters. + * @note Caller is responsible for freeing the returned buffer + */ char *editorRowsToString(int *buffer_len) { int tot_len = 0; int j; @@ -36,20 +53,42 @@ char *editorRowsToString(int *buffer_len) { return buf; } +/** + * @brief Closes the current file and resets editor state + * @details Clears all rows, resets cursor position, scroll offsets, and file metadata. + * Does not prompt to save unsaved changes. + * @note Updates global editor state E + */ void editorCloseFile(void) { E.cursor_x = 0; E.cursor_y = 0; E.rx = 0; E.row_offset = 0; E.col_offset = 0; + for (int i = 0; i < E.numrows; ++i) { + free(E.row[i].chars); + free(E.row[i].render); + } E.numrows = 0; E.row = NULL; E.dirty = 0; + free(E.filename); E.filename = NULL; E.status_msg[0] = '\0'; E.status_msg_time = 0; } +/** + * @brief Opens a file for editing + * @details Loads file content into editor rows, one line per row. If another file + * is already open, it is closed first (without saving). File is opened in a+ + * (read/append) mode to allow both reading and modification. + * @param filename Path to the file to open (relative or absolute) + * @note Updates global editor state E + * @note Calls die() on file open failure + * @note Newline characters are stripped from loaded lines + * @see editorInsertRow() + */ void editorOpen(char *filename) { FILE *fp; @@ -59,7 +98,6 @@ void editorOpen(char *filename) { E.state = READ_AND_WRITE; } - free(E.filename); E.filename = strdup(filename); fp = fopen(filename, "a+"); @@ -76,12 +114,24 @@ void editorOpen(char *filename) { --line_len; } editorInsertRow(E.numrows, line, line_len); + free(line); + line = NULL; } free(line); fclose(fp); E.dirty = 0; } +/** + * @brief Saves the current file to disk + * @details Prompts for filename if not set, converts all rows to a buffer, + * writes to disk using open/ftruncate/write, and updates dirty flag. + * Displays status messages on success or failure. + * @note Updates global editor state E (dirty flag) + * @note If no filename is set, prompts user via editorPrompt() + * @note Uses O_RDWR | O_CREAT with mode 0644 + * @see editorRowsToString() + */ void editorSave() { int len; char *buf; @@ -111,7 +161,18 @@ void editorSave() { editorSetStatusMessage("Can't save! I/O error: %s", strerror(errno)); } +/** + * @brief Searches for a string in the document + * @details Prompts user for a search query, then searches forward from current + * cursor position. Updates cursor position to the first match found. + * @note Updates global editor state E (cursor position, row_offset) + * @note Search is case-sensitive and operates on rendered line content + * @note Searches begin from the line after current cursor position + * @see editorPrompt() + * @see editorRowRxToCx() + */ void editorFind() { + fprintf(stderr, "searching\n"); char *query = editorPrompt("Search: %s (ESC to cancel)", "", 0); if (query == NULL) return; int i; @@ -127,4 +188,3 @@ void editorFind() { } free(query); } - diff --git a/src/init.c b/src/init.c index 38a27d2..6e95282 100644 --- a/src/init.c +++ b/src/init.c @@ -9,7 +9,7 @@ #include "../include/lisp.h" #include "../include/lisp_lib.h" -extern struct editorConfig; +struct editorConfig; void registerBuiltin(char *key_sequence, LispCFunc f) { lisp_env_define(E.ctx.p->env, lisp_make_symbol(key_sequence, E.ctx), @@ -40,7 +40,7 @@ void initBuiltins() { } void initEditor() { - char *init_file_path = (char *)calloc(256, sizeof(char)); + E.init_file_path = (char *)calloc(256, sizeof(char)); E.cursor_x = 0; E.cursor_y = 0; E.rx = 0; @@ -67,10 +67,10 @@ void initEditor() { E.prefix_state = 0; - strcat(init_file_path, getenv("HOME")); - strcat(init_file_path, "/.beluga/config/init.lisp"); - printf("%s\n", init_file_path); - E.fd_init_file = fopen(init_file_path, "r"); + strcat(E.init_file_path, getenv("HOME")); + strcat(E.init_file_path, "/.beluga/config/init.lisp"); + // printf("%s\n", init_file_path); + E.fd_init_file = fopen(E.init_file_path, "r"); E.ctx = lisp_init(); E.env = lisp_env(E.ctx); lisp_lib_load(E.ctx); @@ -79,7 +79,6 @@ void initEditor() { // Read config file E.ctx_data = lisp_read_file(E.fd_init_file, &E.ctx_error, E.ctx); - free(init_file_path); if (E.ctx_error != LISP_ERROR_NONE) { die("init failed"); } diff --git a/src/input.c b/src/input.c index 7f17c56..e092b8e 100644 --- a/src/input.c +++ b/src/input.c @@ -9,25 +9,46 @@ #include #include #include -#include extern struct editorConfig E; -char *file_completion(const char *path) { +/** + * @file input.c + * @brief Input handling module for the Beluga text editor + * @details Manages user input processing, key bindings, cursor movement, and file path completion + */ + +/** + * @brief Returns the first file completion match for the given path + * @details Searches the directory containing the given path prefix and returns + * the first file or directory entry that matches the filename prefix. + * Appends a trailing slash for directory entries. + * @param path The file path to complete (can be relative or absolute) + * @return Pointer to the completed file path (dynamically allocated), or NULL if: + * - path ends with '/' (already a directory) + * - no matching entries found + * - directory cannot be opened + * @note Caller is responsible for freeing the returned string + * @note Uses static buffer internally; may return stale pointers across calls + */ +const char *file_completion(const char *path) { DIR *dir; struct dirent *entry; char directory[128]; char predict[128]; + const char *last_slash; int predict_len = 0; + size_t dir_len; + // path is a directory if (path[strlen(path) - 1] == '/') { return path; } // Find dir name - char *last_slash = strrchr(path, '/'); + last_slash = strrchr(path, '/'); if (last_slash) { - size_t dir_len = last_slash - path + 1; // length of dir_path + dir_len = last_slash - path + 1; // length of dir_path strncpy(directory, path, dir_len); predict_len = strlen(path) - dir_len - 1; strncpy(predict, last_slash + 1, predict_len); @@ -44,13 +65,16 @@ char *file_completion(const char *path) { while ((entry = readdir(dir)) != NULL) { if (strncmp(entry->d_name, predict, predict_len) == 0) { - static char full_path[128]; + static char full_path[512]; snprintf(full_path, sizeof(full_path), "%s%s", directory, entry->d_name); struct stat st; if (stat(full_path, &st) == 0 && S_ISDIR(st.st_mode)) { strcat(full_path, "/"); // add slash for directories } + closedir(dir); + dir = NULL; + free(entry); return strdup(full_path); } @@ -59,13 +83,24 @@ char *file_completion(const char *path) { // Cleanup when no more entries closedir(dir); dir = NULL; + free(entry); return NULL; } /** - * \fn char * editorPrompt(struct editorConfig *E, char *prompt, char bPathMode) - * \brief Return user input in a prompt when enter is hit. */ - + * @brief Displays an interactive prompt and returns user input + * @details Allows the user to enter text in a prompt with optional path completion + * via Tab key. Supports backspace, delete, and escape key handling. Dynamically + * allocates memory for the input buffer. + * @param prompt The prompt message format string (printf-style) + * @param placeHolder Initial text to display in the input buffer + * @param bPathMode If non-zero, enables Tab key file path completion + * @return Pointer to the user-entered text (dynamically allocated), or NULL if: + * - User pressed ESC to cancel + * - Input buffer is empty when Enter is pressed + * @note Caller is responsible for freeing the returned string + * @note Uses editorReadKey() for input and editorRefreshScreen() for display + */ char *editorPrompt(char *prompt, char *placeHolder, char bPathMode) { size_t buf_size = 128; char *buf = malloc(buf_size); @@ -106,7 +141,9 @@ char *editorPrompt(char *prompt, char *placeHolder, char bPathMode) { } memset(buf, 0, 128); buf_len = 0; - strcpy(buf, file_completion(path)); + char * buf_complete = (char *) file_completion(path); + strcpy(buf, buf_complete); + free(buf_complete); buf_len = strlen(buf); buf[buf_len] = '\0'; @@ -121,6 +158,17 @@ char *editorPrompt(char *prompt, char *placeHolder, char bPathMode) { } } +/** + * @brief Converts a key code to its string representation + * @details Translates raw key codes (including special keys, control keys, + * and regular characters) into human-readable string formats suitable for + * display and keybinding configuration. + * @param key The key code to convert + * @return Pointer to static buffer containing the string representation. + * Examples: "ENTER", "ARROW-UP", "CTRL-a", "TAB", "DELETE", etc. + * @note Returns pointer to static buffer; string is overwritten on next call + * @note Non-printable characters are formatted as "KEY-" + */ char *key_to_string(int key) { static char key_str[32]; @@ -187,6 +235,15 @@ char *key_to_string(int key) { return key_str; } +/** + * @brief Moves the cursor based on arrow key input + * @details Updates cursor position (E.cursor_x, E.cursor_y) based on the given + * key direction. Handles line wrapping and boundary conditions. Prevents cursor + * from exceeding line lengths. + * @param key The arrow key code (ARROW_UP, ARROW_DOWN, ARROW_LEFT, ARROW_RIGHT) + * @return 1 if cursor movement was valid, 0 if cursor was constrained to line boundary + * @note Updates global editor state E + */ int editorMoveCursor(int key) { erow *row = (E.cursor_y >= E.numrows) ? NULL : &E.row[E.cursor_y]; int row_len; @@ -228,6 +285,15 @@ int editorMoveCursor(int key) { return 1; } +/** + * @brief Executes the command bound to a key sequence + * @details Searches the keybinding table for a matching key sequence and + * prefix state, then evaluates the associated Lisp command. + * @param key_sequence The string representation of the key sequence + * @return 1 if a matching keybinding was found and executed, 0 otherwise + * @note Updates global editor state E (prefix_state) + * @note Uses Lisp interpreter to evaluate bound commands + */ int executeKeyBind(char *key_sequence) { int i; int previous_state = 0; @@ -235,7 +301,7 @@ int executeKeyBind(char *key_sequence) { for (i = 0; i < E.number_of_keybinds; ++i) { if (!strcmp(key_sequence, E.key_binds[i].key_sequence)) { if (E.prefix_state != E.key_binds[i].prefix_id) { - return 0; + continue; } previous_state = E.prefix_state; // It's a symbol, create a function call @@ -249,6 +315,14 @@ int executeKeyBind(char *key_sequence) { return 0; } +/** + * @brief Processes a single keypress from the user + * @details Reads a key, checks if it matches any registered keybinding, + * and either executes the bound command or inserts the character. Resets + * the quit buffer counter on successful key processing. + * @note Updates global editor state E + * @note Calls editorReadKey() to get input and editorInsertChar() for unbound keys + */ void editorProcessKeypress() { int c = editorReadKey(); diff --git a/src/output.c b/src/output.c index a8a896a..cbb53a4 100644 --- a/src/output.c +++ b/src/output.c @@ -1,11 +1,30 @@ +/** + * @file output.c + * @brief Screen rendering and output module for the Beluga text editor + * @details Handles all screen updates, cursor positioning, status bar rendering, + * and display synchronization using ANSI escape sequences + */ + #include "../include/output.h" +#include "../include/syntax_highlighter.h" #include #include +#include #include #include extern struct editorConfig E; +/** + * @brief Renders all visible rows to the screen buffer + * @details Draws file content with syntax highlighting, handles line wrapping, + * and displays tilde characters (~) for empty lines. Shows welcome message + * when no file is open. + * @param ab Pointer to append buffer structure for accumulating output + * @note Respects E.row_offset and E.col_offset for scrolling + * @note Clears to end of each line after content + * @see editorRefreshScreen() + */ void editorDrawRows(struct abuf *ab) { int y; char welcome[80]; @@ -41,13 +60,22 @@ void editorDrawRows(struct abuf *ab) { len = 0; if (len > E.screencols) len = E.screencols; - abAppend(ab, &E.row[file_row].render[E.col_offset], len); + char * highlighted = highlight_line(&E.row[file_row].render[E.col_offset], &E.row[file_row].rsize); + abAppend(ab, highlighted, E.row[file_row].rsize); + free(highlighted); } abAppend(ab, ERASE_END_LINE, 3); abAppend(ab, "\r\n", 2); } } +/** + * @brief Updates scroll offsets to keep cursor visible on screen + * @details Adjusts E.row_offset and E.col_offset to ensure the cursor remains + * within the visible viewport. Also updates E.rx (rendered x-coordinate). + * @note Updates global editor state E + * @see editorRowCxToRx() + */ void editorScroll() { E.rx = E.cursor_x; if (E.cursor_y < E.numrows) { @@ -68,6 +96,13 @@ void editorScroll() { } } +/** + * @brief Renders the status bar at the bottom of the screen + * @details Displays filename, line count, dirty flag, and current cursor position + * in an inverted color bar. Right-aligns the cursor position indicator. + * @param ab Pointer to append buffer structure for accumulating output + * @note Uses ANSI escape codes for color inversion + */ void editorDrawStatusBar(struct abuf *ab) { int len, render_len; char status[80], render_status[80]; @@ -95,6 +130,13 @@ void editorDrawStatusBar(struct abuf *ab) { abAppend(ab, "\r\n", 2); } +/** + * @brief Renders the message bar below the status bar + * @details Displays temporary status messages for a limited time (5 seconds). + * Only displays message if within time window and within screen width. + * @param ab Pointer to append buffer structure for accumulating output + * @note Messages are set by editorSetStatusMessage() + */ void editorDrawMessageBar(struct abuf *ab) { int msg_len = strlen(E.status_msg); abAppend(ab, ERASE_END_LINE, 3); @@ -106,6 +148,16 @@ void editorDrawMessageBar(struct abuf *ab) { } } +/** + * @brief Performs complete screen refresh and buffer synchronization + * @details Clears screen, redraws all visible content (rows, status bar, message bar), + * positions cursor, and writes accumulated buffer to stdout. This is the main + * rendering function called each frame. + * @note Updates global editor state E (via editorScroll()) + * @see editorDrawRows() + * @see editorDrawStatusBar() + * @see editorDrawMessageBar() + */ void editorRefreshScreen() { editorScroll(); struct abuf ab = ABUF_INIT; @@ -123,11 +175,20 @@ void editorRefreshScreen() { abAppend(&ab, buf, strlen(buf)); abAppend(&ab, SHOW_CURSOR, 6); - write(STDOUT_FILENO, ab.b, ab.len); abFree(&ab); } + +/** + * @brief Sets a temporary status message for display + * @details Formats and stores a message that will be displayed in the message bar + * for 5 seconds. Uses printf-style variable argument formatting. + * @param fmt Printf-style format string + * @param ... Variable arguments for format string + * @note Updates global editor state E (status_msg, status_msg_time) + * @see editorDrawMessageBar() + */ void editorSetStatusMessage(const char *fmt, ...) { va_list ap; va_start(ap, fmt); diff --git a/src/syntax_highlighter.c b/src/syntax_highlighter.c new file mode 100644 index 0000000..7efdb0a --- /dev/null +++ b/src/syntax_highlighter.c @@ -0,0 +1,163 @@ +#include "../include/syntax_highlighter.h" +#include +#include +#include + +const char *c_keywords[] = { + "if", "else", "while", "for", "do", "switch", "case", "break", + "continue", "return", "goto", "struct", "union", "enum", + "typedef", "static", "extern", "const", "volatile", "sizeof", + "auto", "register", "inline", "restrict", NULL +}; + +// C types +const char *c_types[] = { + "int", "char", "float", "double", "void", "long", "short", + "unsigned", "signed", "bool", NULL +}; + +// Check if character is alphanumeric or underscore +int is_word_char(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '_'; +} + +// Check if string is a keyword +int is_keyword(const char *word) { + for (int i = 0; c_keywords[i] != NULL; i++) { + if (strcmp(word, c_keywords[i]) == 0) return 1; + } + return 0; +} + +// Check if string is a type +int is_type(const char *word) { + for (int i = 0; c_types[i] != NULL; i++) { + if (strcmp(word, c_types[i]) == 0) return 1; + } + return 0; +} + +// Get color code for token type +const char *get_color(TokenType type) { + switch (type) { + case TOKEN_KEYWORD: return COLOR_KEYWORD; + case TOKEN_TYPE: return COLOR_TYPE; + case TOKEN_STRING: return COLOR_STRING; + case TOKEN_COMMENT: return COLOR_COMMENT; + case TOKEN_NUMBER: return COLOR_NUMBER; + default: return COLOR_DEFAULT; + } +} + +// Highlight a line of C code and return the highlighted string +// Returns a newly allocated string that must be freed by the caller +char *highlight_line(const char *line, int *length) { + char *result = malloc(1024); // Allocate space for result + int result_pos = 0; + int i = 0; + + while (line[i] != '\0' && line[i] != '\n') { + // Skip whitespace + if (line[i] == ' ' || line[i] == '\t') { + result[result_pos++] = line[i++]; + continue; + } + + // Handle line comments + if (line[i] == '/' && line[i + 1] == '/') { + result_pos += sprintf(&result[result_pos], "%s", COLOR_COMMENT); + while (line[i] != '\0' && line[i] != '\n') { + result[result_pos++] = line[i++]; + } + result_pos += sprintf(&result[result_pos], "%s", COLOR_RESET); + continue; + } + + // Handle block comments + if (line[i] == '/' && line[i + 1] == '*') { + result_pos += sprintf(&result[result_pos], "%s", COLOR_COMMENT); + result[result_pos++] = line[i++]; + result[result_pos++] = line[i++]; + while (line[i] != '\0') { + if (line[i] == '*' && line[i + 1] == '/') { + result[result_pos++] = line[i++]; + result[result_pos++] = line[i++]; + break; + } + result[result_pos++] = line[i++]; + } + result_pos += sprintf(&result[result_pos], "%s", COLOR_RESET); + continue; + } + + // Handle strings + if (line[i] == '"') { + result_pos += sprintf(&result[result_pos], "%s\"", COLOR_STRING); + i++; + while (line[i] != '\0' && line[i] != '"') { + if (line[i] == '\\') { + result[result_pos++] = line[i++]; + result[result_pos++] = line[i++]; + } else { + result[result_pos++] = line[i++]; + } + } + if (line[i] == '"') result[result_pos++] = line[i++]; + result_pos += sprintf(&result[result_pos], "%s", COLOR_RESET); + continue; + } + + // Handle character literals + if (line[i] == '\'') { + result_pos += sprintf(&result[result_pos], "%s'", COLOR_STRING); + i++; + while (line[i] != '\0' && line[i] != '\'') { + if (line[i] == '\\') { + result[result_pos++] = line[i++]; + result[result_pos++] = line[i++]; + } else { + result[result_pos++] = line[i++]; + } + } + if (line[i] == '\'') result[result_pos++] = line[i++]; + result_pos += sprintf(&result[result_pos], "%s", COLOR_RESET); + continue; + } + + // Handle numbers + if (line[i] >= '0' && line[i] <= '9') { + result_pos += sprintf(&result[result_pos], "%s", COLOR_NUMBER); + while (is_word_char(line[i]) || line[i] == '.') { + result[result_pos++] = line[i++]; + } + result_pos += sprintf(&result[result_pos], "%s", COLOR_RESET); + continue; + } + + // Handle identifiers and keywords + if (is_word_char(line[i])) { + int start = i; + while (is_word_char(line[i])) i++; + + char word[256]; + strncpy(word, &line[start], i - start); + word[i - start] = '\0'; + + TokenType type = TOKEN_DEFAULT; + if (is_keyword(word)) type = TOKEN_KEYWORD; + else if (is_type(word)) type = TOKEN_TYPE; + + result_pos += sprintf(&result[result_pos], "%s%s%s", + get_color(type), word, COLOR_RESET); + continue; + } + + // Handle operators and other characters + result[result_pos++] = line[i++]; + } + + result[result_pos] = '\0'; + *length = result_pos + 1; + return result; +}