#include "ecex.h" #include "media.h" #include "ccdjit.h" #include "common.h" #include "config.h" #include "util.h" #include "path.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include extern FILE *popen(const char *command, const char *type); extern int pclose(FILE *stream); extern int kill(pid_t pid, int sig); #define ECEX_INITIAL_BUFFER_CAP 8 #define ECEX_INITIAL_COMMAND_CAP 32 #define ECEX_INITIAL_KEYBIND_CAP 32 #define ECEX_INITIAL_WINDOW_CAP 4 #define ECEX_INITIAL_MODE_CAP 8 #define ECEX_INITIAL_MODE_KEYBIND_CAP 16 #define ECEX_INITIAL_HOOK_CAP 8 ecex_window_t *ecex_current_window(ecex_t *ed); static void ecex_clear_command_hooks(ecex_t *ed); static void ecex_clear_prefix_hooks(ecex_t *ed); static void ecex_clear_buffer_hooks(ecex_t *ed); static void ecex_clear_completion_providers(ecex_t *ed); static int ecex_complete_at_point_direction(ecex_t *ed, int direction); static int ecex_indent_for_tab(ecex_t *ed); void *ecex_config_alloc(size_t size) { return malloc(size ? size : 1); } void *ecex_config_calloc(size_t count, size_t size) { if (count == 0) count = 1; if (size == 0) size = 1; return calloc(count, size); } void ecex_config_free(void *ptr) { free(ptr); } double ecex_time_seconds(void) { struct timeval tv; gettimeofday(&tv, NULL); return (double)tv.tv_sec + (double)tv.tv_usec / 1000000.0; } void ecex_mem_zero(void *ptr, size_t size) { if (!ptr || size == 0) return; memset(ptr, 0, size); } int ecex_i32_get(const int *items, size_t index) { if (!items) return 0; return items[index]; } void ecex_i32_set(int *items, size_t index, int value) { if (!items) return; items[index] = value; } int ecex_run_plugin_file_handlers(ecex_t *ed, buffer_t *buffer) { return ecex_plugin_file_handlers_run(ed, buffer); } int ecex_prng_next_bounded(unsigned int *state, int bound) { unsigned int x; unsigned int limit; if (!state || bound <= 0) return 0; x = *state; if (x == 0u) x = 0x6d2b79f5u; /* Xorshift32. Keep this host-side so CCDJIT plugins do not depend on * unsigned multiply/divide lowering details for game logic randomness. */ x ^= x << 13; x ^= x >> 17; x ^= x << 5; if (x == 0u) x = 0xa5a5a5a5u; *state = x; /* Avoid modulo bias enough for tiny bounds without libc. */ limit = 0xffffffffu - (0xffffffffu % (unsigned int)bound); while (x >= limit) { x ^= x << 13; x ^= x >> 17; x ^= x << 5; if (x == 0u) x = 0x9e3779b9u; *state = x; } return (int)(x % (unsigned int)bound); } int ecex_random_bounded(int bound) { static unsigned int state = 0x7f4a7c15u; unsigned int mix; if (bound <= 0) return 0; /* Host-owned PRNG for plugins that should not pass writable state pointers * through the JIT ABI. Stir in a stack address so separate launches differ * enough for games/demos without requiring libc time calls from plugins. */ mix = (unsigned int)(size_t)&bound; state ^= mix + 0x9e3779b9u + (state << 6) + (state >> 2); return ecex_prng_next_bounded(&state, bound); } static int ecex_file_browser_preview_current(ecex_t *ed); static int ecex_file_browser_update_preview_if_enabled(ecex_t *ed); static ecex_color_t ecex_color(float r, float g, float b) { ecex_color_t c = {r, g, b}; return c; } static void ecex_mark_ui_changed(ecex_t *ed) { if (!ed) return; ed->ui_revision++; } static void ecex_mark_font_changed(ecex_t *ed) { if (!ed) return; ed->font_revision++; ecex_mark_ui_changed(ed); } static void ecex_theme_set_defaults(ecex_t *ed) { if (!ed) return; ed->theme.font_path = NULL; ed->theme.font_size = 22.0f; ed->theme.bg = ecex_color(0.08f, 0.09f, 0.11f); ed->theme.fg = ecex_color(0.88f, 0.88f, 0.86f); ed->theme.status_bg = ecex_color(0.15f, 0.15f, 0.18f); ed->theme.status_fg = ecex_color(0.86f, 0.86f, 0.84f); ed->theme.status_border = ecex_color(0.28f, 0.28f, 0.32f); ed->theme.cursor = ecex_color(1.0f, 1.0f, 1.0f); ed->theme.region_bg = ecex_color(0.22f, 0.34f, 0.48f); ed->theme.minibuffer_bg = ecex_color(0.10f, 0.11f, 0.14f); ed->theme.minibuffer_fg = ecex_color(0.95f, 0.95f, 0.90f); ed->theme.completion_fg = ecex_color(0.45f, 0.45f, 0.45f); ed->theme.completion_enabled = 1; ed->theme.interactive_highlight_bg = ecex_color(0.18f, 0.20f, 0.24f); ed->theme.interactive_highlight_fg = ecex_color(1.0f, 1.0f, 1.0f); ed->theme.current_line_bg = ecex_color(0.12f, 0.13f, 0.16f); ed->theme.search_bg = ecex_color(0.55f, 0.42f, 0.12f); ed->theme.line_numbers_enabled = 1; ed->theme.current_line_enabled = 1; } #define ECEX_COMMAND(name, fn) \ do { \ if (ecex_register_command(ed, (name), (fn)) != ECEX_OK) return ECEX_ERR; \ } while (0) #define ECEX_BIND(key, command) \ do { \ if (ecex_bind_key(ed, (key), (command)) != ECEX_OK) return ECEX_ERR; \ } while (0) #define CURRENT_BUFFER_OR_ERR(ed) \ buffer_t *buf = ecex_current_buffer(ed); \ if (!buf) return ECEX_ERR static int cmd_quit(ecex_t *ed) { if (!ed) return ECEX_ERR; if (ecex_has_modified_buffers(ed)) { fprintf(stderr, "ecex: refusing to quit; modified buffers exist. Save or use force-quit.\n"); return ECEX_ERR; } ed->should_quit = 1; return ECEX_OK; } static int cmd_force_quit(ecex_t *ed) { if (!ed) return ECEX_ERR; ed->should_quit = 1; return ECEX_OK; } static int cmd_next_buffer(ecex_t *ed) { return ecex_next_buffer(ed); } static int cmd_previous_buffer(ecex_t *ed) { return ecex_previous_buffer(ed); } static int cmd_split_window_right(ecex_t *ed) { return ecex_split_window_vertically(ed); } static int cmd_split_window_below(ecex_t *ed) { return ecex_split_window_horizontally(ed); } static int cmd_other_window(ecex_t *ed) { return ecex_other_window(ed); } static int cmd_previous_window(ecex_t *ed) { return ecex_previous_window(ed); } static int cmd_delete_window(ecex_t *ed) { return ecex_delete_window(ed); } static int cmd_delete_other_windows(ecex_t *ed) { return ecex_delete_other_windows(ed); } static int cmd_balance_windows(ecex_t *ed) { return ecex_balance_windows(ed); } static int cmd_move_left(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_left(buf); return ECEX_OK; } static int cmd_move_right(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_right(buf); return ECEX_OK; } static int cmd_move_up(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_up(buf); ecex_file_browser_update_preview_if_enabled(ed); return ECEX_OK; } static int cmd_move_down(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_down(buf); ecex_file_browser_update_preview_if_enabled(ed); return ECEX_OK; } static int cmd_move_word_left(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_word_left(buf); return ECEX_OK; } static int cmd_move_word_right(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_word_right(buf); return ECEX_OK; } static int cmd_beginning_of_line(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_beginning_of_line(buf); return ECEX_OK; } static int cmd_end_of_line(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_end_of_line(buf); return ECEX_OK; } static int cmd_beginning_of_buffer(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_beginning_of_buffer(buf); return ECEX_OK; } static int cmd_end_of_buffer(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_move_end_of_buffer(buf); return ECEX_OK; } static int cmd_undo(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); return buffer_undo(buf); } static int cmd_redo(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); return buffer_redo(buf); } static int cmd_backspace(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); return buffer_backspace(buf); } static int cmd_delete_forward(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); return buffer_delete_forward(buf); } static int cmd_newline(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); if (buffer_is_interactive(buf)) { return ecex_interactive_activate_current_line(ed); } return buffer_insert_char(buf, '\n'); } static int cmd_kill_line(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); return buffer_kill_line(buf); } static int cmd_clear_buffer(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); return buffer_clear(buf); } static int cmd_set_mark(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_set_mark(buf, buf->point); return ECEX_OK; } static int cmd_clear_mark(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); buffer_clear_mark(buf); return ECEX_OK; } static int cmd_yank(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); const char *text = ecex_clipboard_get(ed); if (!text || !text[0]) return ECEX_OK; return buffer_replace_selection(buf, text); } static int cmd_copy_region(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); if (!buffer_has_selection(buf)) return ECEX_OK; size_t start = 0; size_t end = 0; buffer_selection_range(buf, &start, &end); char *text = buffer_substring(buf, start, end); if (!text) return ECEX_ERR; int result = ecex_clipboard_set(ed, text); free(text); return result; } static int cmd_kill_region(ecex_t *ed) { CURRENT_BUFFER_OR_ERR(ed); if (!buffer_has_selection(buf)) return ECEX_OK; size_t start = 0; size_t end = 0; buffer_selection_range(buf, &start, &end); char *text = buffer_substring(buf, start, end); if (!text) return ECEX_ERR; int result = ecex_clipboard_set(ed, text); free(text); if (result != ECEX_OK) return result; return buffer_delete_selection(buf); } static int cmd_list_commands(ecex_t *ed) { return ecex_list_commands(ed); } static int cmd_list_buffers(ecex_t *ed) { return ecex_list_buffers(ed); } static int cmd_switch_buffer(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_SWITCH_BUFFER, "Switch buffer: "); return ECEX_OK; } static int cmd_kill_buffer_command(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_KILL_BUFFER, "Kill buffer: "); return ECEX_OK; } static int cmd_force_kill_buffer_command(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_FORCE_KILL_BUFFER, "Force kill buffer: "); return ECEX_OK; } static int cmd_compile(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_COMPILE, "Compile: "); return ECEX_OK; } static int cmd_grep(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_GREP, "Grep: "); return ECEX_OK; } static int cmd_recompile(ecex_t *ed) { return ecex_rerun_compile(ed); } static int cmd_regrep(ecex_t *ed) { return ecex_rerun_grep(ed); } static int cmd_next_error(ecex_t *ed) { return ecex_next_interactive_action(ed); } static int cmd_previous_error(ecex_t *ed) { return ecex_previous_interactive_action(ed); } static int cmd_comment_region(ecex_t *ed) { return ecex_comment_region(ed); } static int cmd_uncomment_region(ecex_t *ed) { return ecex_uncomment_region(ed); } static int cmd_toggle_line_numbers(ecex_t *ed) { if (!ed) return ECEX_ERR; ecex_set_line_numbers_enabled(ed, !ed->theme.line_numbers_enabled); return ECEX_OK; } static int cmd_toggle_current_line(ecex_t *ed) { if (!ed) return ECEX_ERR; ecex_set_current_line_enabled(ed, !ed->theme.current_line_enabled); return ECEX_OK; } static int cmd_isearch_forward(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_ISEARCH_FORWARD, "I-search: "); return ECEX_OK; } static int cmd_isearch_backward(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_ISEARCH_BACKWARD, "I-search backward: "); return ECEX_OK; } static int cmd_find_file(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_FIND_FILE, "Find file: "); return ECEX_OK; } static int cmd_write_file(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_WRITE_FILE, "Write file: "); return ECEX_OK; } static int cmd_save_buffer(ecex_t *ed) { if (!ed) return ECEX_ERR; buffer_t *buf = ecex_current_buffer(ed); if (!buf) return ECEX_ERR; if (!buf->path) { ecex_request_prompt(ed, ECEX_PROMPT_WRITE_FILE, "Write file: "); return ECEX_OK; } return ecex_save_current_buffer(ed); } static int cmd_eval_buffer(ecex_t *ed) { return ecex_eval_current_buffer(ed); } static int cmd_eval_line(ecex_t *ed) { return ecex_eval_current_line(ed); } static int cmd_eval_region(ecex_t *ed) { return ecex_eval_current_region(ed); } static int cmd_eval_file(ecex_t *ed) { ecex_request_prompt(ed, ECEX_PROMPT_EVAL_FILE, "Eval file: "); return ECEX_OK; } static int cmd_eval_rerun_last(ecex_t *ed) { return ecex_eval_rerun_last(ed); } static int cmd_quit_window(ecex_t *ed) { if (!ed) return ECEX_ERR; if (ecex_window_count(ed) > 1) return ecex_delete_window(ed); return ecex_previous_buffer(ed); } static int cmd_reload_config(ecex_t *ed) { return ecex_reload_config(ed); } static int cmd_indent_for_tab(ecex_t *ed) { return ecex_indent_for_tab(ed); } static int cmd_complete_at_point(ecex_t *ed) { return ecex_complete_at_point(ed); } static int cmd_complete_at_point_previous(ecex_t *ed) { return ecex_complete_at_point_direction(ed, -1); } /* Built-in file browser -------------------------------------------------- */ typedef struct ecex_file_entry { char *name; char *path; int is_dir; int is_image; long long size; } ecex_file_entry_t; static char ecex_fb_cwd[4096] = "."; static char ecex_fb_history[64][4096]; static size_t ecex_fb_history_count = 0; static size_t ecex_fb_history_index = 0; static int ecex_fb_preview_expanded = 0; static void ecex_file_entry_free(ecex_file_entry_t *entries, size_t count) { if (!entries) return; for (size_t i = 0; i < count; i++) { free(entries[i].name); free(entries[i].path); } free(entries); } static int ecex_file_entry_compare(const void *a, const void *b) { const ecex_file_entry_t *ea = (const ecex_file_entry_t *)a; const ecex_file_entry_t *eb = (const ecex_file_entry_t *)b; if (ea->is_dir != eb->is_dir) return eb->is_dir - ea->is_dir; return strcmp(ea->name ? ea->name : "", eb->name ? eb->name : ""); } static int ecex_file_browser_collect(const char *dir, ecex_file_entry_t **out_entries, size_t *out_count) { if (out_entries) *out_entries = NULL; if (out_count) *out_count = 0; if (!dir || !out_entries || !out_count) return ECEX_ERR; DIR *d = opendir(dir); if (!d) return ECEX_ERR; size_t cap = 64; size_t count = 0; ecex_file_entry_t *entries = calloc(cap, sizeof(*entries)); if (!entries) { closedir(d); return ECEX_ERR; } struct dirent *entry; while ((entry = readdir(d)) != NULL) { const char *name = entry->d_name; if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue; if (count == cap) { size_t new_cap = cap * 2; ecex_file_entry_t *grown = realloc(entries, new_cap * sizeof(*entries)); if (!grown) break; memset(grown + cap, 0, (new_cap - cap) * sizeof(*grown)); entries = grown; cap = new_cap; } char *path = ecex_path_join(dir, name); if (!path) continue; char *name_copy = ecex_strdup(name); if (!name_copy) { free(path); continue; } entries[count].name = name_copy; entries[count].path = path; entries[count].is_dir = ecex_path_is_dir(path); entries[count].is_image = ecex_path_is_image(path); entries[count].size = ecex_path_file_size(path); count++; } closedir(d); qsort(entries, count, sizeof(*entries), ecex_file_entry_compare); *out_entries = entries; *out_count = count; return ECEX_OK; } static void ecex_file_browser_push_history(const char *dir) { if (!dir || !dir[0]) return; if (ecex_fb_history_count > 0 && strcmp(ecex_fb_history[ecex_fb_history_index], dir) == 0) return; if (ecex_fb_history_index + 1 < ecex_fb_history_count) { ecex_fb_history_count = ecex_fb_history_index + 1; } if (ecex_fb_history_count == 64) { memmove(ecex_fb_history, ecex_fb_history + 1, sizeof(ecex_fb_history[0]) * 63); ecex_fb_history_count = 63; if (ecex_fb_history_index > 0) ecex_fb_history_index--; } ecex_path_copy(ecex_fb_history[ecex_fb_history_count], sizeof(ecex_fb_history[0]), dir); ecex_fb_history_index = ecex_fb_history_count; ecex_fb_history_count++; } static int ecex_file_browser_populate(ecex_t *ed, const char *dir, int push_history); static int ecex_file_browser_move_to_action(buffer_t *buf, size_t action_index); static int ecex_file_browser_open_action(ecex_t *ed, buffer_t *buffer, size_t line, const char *payload, void *userdata) { (void)buffer; (void)line; (void)userdata; if (!ed || !payload) return ECEX_ERR; char selected[4096]; ecex_path_copy(selected, sizeof(selected), payload); if (ecex_path_is_dir(selected)) return ecex_file_browser_populate(ed, selected, 1); if (ecex_path_is_file(selected) && ecex_path_is_media(selected)) return ecex_media_open(ed, selected); if (ecex_path_is_file(selected)) return ecex_find_file(ed, selected); return ECEX_ERR; } static int ecex_file_browser_populate(ecex_t *ed, const char *dir, int push_history) { if (!ed) return ECEX_ERR; char *normalized = ecex_path_normalize((dir && dir[0]) ? dir : ecex_fb_cwd); if (!normalized) return ECEX_ERR; if (!ecex_path_is_dir(normalized)) { char *parent = ecex_path_dirname(normalized); free(normalized); normalized = parent; if (!normalized) return ECEX_ERR; } ecex_path_copy(ecex_fb_cwd, sizeof(ecex_fb_cwd), normalized); if (push_history) ecex_file_browser_push_history(ecex_fb_cwd); buffer_t *buf = ecex_find_buffer(ed, "*file-browser*"); if (!buf) buf = ecex_create_interactive_buffer(ed, "*file-browser*"); if (!buf) { free(normalized); return ECEX_ERR; } buffer_clear(buf); buffer_set_interactive(buf, 1); ecex_buffer_set_major_mode_by_name(ed, buf, "file-browser-mode"); char line[8192]; snprintf(line, sizeof(line), "File browser: %s\n\nKeys: RET/l open, h parent, g refresh, b/f history, v preview, m toggle preview, q quit.\n\n", ecex_fb_cwd); buffer_append(buf, line); char *parent = ecex_path_dirname(ecex_fb_cwd); if (parent) { ecex_interactive_append_line(ed, buf, "[..] parent", ecex_file_browser_open_action, parent, NULL); free(parent); } ecex_file_entry_t *entries = NULL; size_t count = 0; if (ecex_file_browser_collect(ecex_fb_cwd, &entries, &count) != ECEX_OK) { buffer_append(buf, "\nCould not read directory.\n"); } else { for (size_t i = 0; i < count; i++) { const char *tag = entries[i].is_dir ? "[D]" : (entries[i].is_image ? "[I]" : (ecex_path_is_video(entries[i].path) ? "[V]" : " ")); if (entries[i].is_dir) { snprintf(line, sizeof(line), "%s %s/", tag, entries[i].name); } else if (entries[i].size >= 0) { snprintf(line, sizeof(line), "%s %s (%lld bytes)", tag, entries[i].name, entries[i].size); } else { snprintf(line, sizeof(line), "%s %s", tag, entries[i].name); } ecex_interactive_append_line(ed, buf, line, ecex_file_browser_open_action, entries[i].path, NULL); } } ecex_file_entry_free(entries, count); if (ecex_fb_preview_expanded) { buffer_append(buf, "\nPreview pane is active: move up/down to update it, v refreshes, m closes it.\n"); } buf->point = 0; buf->scroll_line = 0; buf->scroll_col = 0; if (buf->interactive_action_count > 0) { /* Start on the first actual directory entry instead of the [..] parent row. * That makes pressing m/v immediately show a useful preview. */ ecex_file_browser_move_to_action(buf, buf->interactive_action_count > 1 ? 1 : 0); } buf->modified = 0; free(normalized); int result = ecex_switch_buffer(ed, "*file-browser*"); if (result == ECEX_OK && ecex_fb_preview_expanded) { ecex_file_browser_update_preview_if_enabled(ed); } return result; } static int ecex_file_browser_current_payload(ecex_t *ed, char *out, size_t out_size) { if (!ed || !out || out_size == 0) return ECEX_ERR; buffer_t *buf = ecex_current_buffer(ed); if (!buf || !buffer_is_interactive(buf)) return ECEX_ERR; size_t line = buffer_current_line_number(buf); if (line > 0) line--; ecex_interactive_line_action_t *action = buffer_interactive_action_at_line(buf, line); if ((!action || !action->payload) && buf->interactive_action_count > 0) { action = &buf->interactive_actions[0]; } if (!action || !action->payload) return ECEX_ERR; return ecex_path_copy(out, out_size, action->payload) == 0 ? ECEX_OK : ECEX_ERR; } static int ecex_file_browser_move_to_action(buffer_t *buf, size_t action_index) { if (!buf || !buffer_is_interactive(buf) || buf->interactive_action_count == 0) return ECEX_ERR; if (action_index >= buf->interactive_action_count) action_index = buf->interactive_action_count - 1; size_t target_line = buf->interactive_actions[action_index].line; size_t pos = 0; for (size_t line = 0; line < target_line && pos < buf->len; pos++) { if (buf->data[pos] == '\n') line++; } buffer_set_point(buf, pos); return ECEX_OK; } static buffer_t *ecex_file_preview_buffer(ecex_t *ed) { if (!ed) return NULL; buffer_t *preview = ecex_find_buffer(ed, "*file-preview*"); if (!preview) preview = ecex_create_buffer(ed, "*file-preview*", NULL, 0); return preview; } static int ecex_file_browser_show_preview_pane(ecex_t *ed, buffer_t *preview) { if (!ed || !preview || ed->window_count == 0) return ECEX_ERR; size_t browser_window = ed->current_window_index; if (ed->window_count == 1) { if (ecex_split_window_vertically(ed) != ECEX_OK) return ECEX_ERR; ed->windows[ed->current_window_index].buffer = preview; ed->current_window_index = browser_window; return ecex_sync_current_buffer(ed); } size_t preview_window = (browser_window + 1) % ed->window_count; if (preview_window == browser_window && ed->window_count > 1) preview_window = 1; ed->windows[preview_window].buffer = preview; ed->current_window_index = browser_window; return ecex_sync_current_buffer(ed); } static int ecex_file_browser_close_preview_pane(ecex_t *ed) { if (!ed || ed->window_count <= 1) return ECEX_OK; buffer_t *preview = ecex_find_buffer(ed, "*file-preview*"); if (!preview) return ECEX_OK; for (size_t i = 0; i < ed->window_count; i++) { if (ed->windows[i].buffer != preview) continue; memmove(&ed->windows[i], &ed->windows[i + 1], (ed->window_count - i - 1) * sizeof(ed->windows[0])); ed->window_count--; if (ed->current_window_index >= ed->window_count) ed->current_window_index = ed->window_count - 1; ecex_balance_windows(ed); return ecex_sync_current_buffer(ed); } return ECEX_OK; } static int ecex_file_browser_fill_preview(ecex_t *ed, buffer_t *preview, const char *path) { if (!ed || !preview || !path) return ECEX_ERR; if (ecex_path_is_media(path)) { return ecex_media_load_into_buffer(ed, path, preview); } ecex_media_buffer_clear(preview); ecex_buffer_set_major_mode_by_name(ed, preview, "special-mode"); preview->read_only = 0; buffer_clear(preview); char line[8192]; snprintf(line, sizeof(line), "Preview: %s\n\n", path); buffer_append(preview, line); if (ecex_path_is_dir(path)) { buffer_append(preview, "Directory. Press RET/l in *file-browser* to open it.\n"); } else if (ecex_path_is_file(path)) { long long size = ecex_path_file_size(path); snprintf(line, sizeof(line), "File size: %lld bytes.\n\n", size); buffer_append(preview, line); FILE *f = fopen(path, "rb"); if (f) { char chunk[8193]; size_t n = fread(chunk, 1, sizeof(chunk) - 1, f); fclose(f); chunk[n] = '\0'; int binary = 0; for (size_t i = 0; i < n; i++) { unsigned char c = (unsigned char)chunk[i]; if (c == 0 || (c < 8) || (c > 13 && c < 32)) { binary = 1; break; } } if (binary) buffer_append(preview, "Binary file; text preview suppressed.\n"); else buffer_append(preview, chunk); } } preview->modified = 0; preview->read_only = 1; return ECEX_OK; } static int ecex_file_browser_preview_current(ecex_t *ed) { char path[4096]; if (ecex_file_browser_current_payload(ed, path, sizeof(path)) != ECEX_OK) return ECEX_ERR; buffer_t *preview = ecex_file_preview_buffer(ed); if (!preview) return ECEX_ERR; int result = ecex_file_browser_fill_preview(ed, preview, path); ecex_file_browser_show_preview_pane(ed, preview); return result; } static int ecex_file_browser_update_preview_if_enabled(ecex_t *ed) { if (!ed || !ecex_fb_preview_expanded) return ECEX_OK; buffer_t *buf = ecex_current_buffer(ed); const char *mode = buf ? ecex_buffer_major_mode_name(ed, buf) : NULL; if (!mode || strcmp(mode, "file-browser-mode") != 0) return ECEX_OK; return ecex_file_browser_preview_current(ed); } static int cmd_file_browser(ecex_t *ed) { char cwd[4096]; ecex_path_cwd(cwd, sizeof(cwd)); return ecex_file_browser_populate(ed, cwd, 1); } static int cmd_file_browser_here(ecex_t *ed) { buffer_t *buf = ecex_current_buffer(ed); if (buf && buf->path && buf->path[0]) { char *dir = ecex_path_dirname(buf->path); if (!dir) return ECEX_ERR; int result = ecex_file_browser_populate(ed, dir, 1); free(dir); return result; } return cmd_file_browser(ed); } static int cmd_file_browser_refresh(ecex_t *ed) { return ecex_file_browser_populate(ed, ecex_fb_cwd, 0); } static int cmd_file_browser_parent(ecex_t *ed) { char *parent = ecex_path_dirname(ecex_fb_cwd); if (!parent) return ECEX_ERR; int result = ecex_file_browser_populate(ed, parent, 1); free(parent); return result; } static int cmd_file_browser_open(ecex_t *ed) { return ecex_interactive_activate_current_line(ed); } static int cmd_file_browser_preview(ecex_t *ed) { return ecex_file_browser_preview_current(ed); } static int cmd_file_browser_toggle_preview(ecex_t *ed) { ecex_fb_preview_expanded = !ecex_fb_preview_expanded; if (!ecex_fb_preview_expanded) { ecex_file_browser_close_preview_pane(ed); return ecex_file_browser_populate(ed, ecex_fb_cwd, 0); } int result = ecex_file_browser_populate(ed, ecex_fb_cwd, 0); if (result == ECEX_OK) ecex_file_browser_preview_current(ed); return result; } static int cmd_file_browser_history_back(ecex_t *ed) { if (ecex_fb_history_count == 0 || ecex_fb_history_index == 0) return ECEX_ERR; ecex_fb_history_index--; return ecex_file_browser_populate(ed, ecex_fb_history[ecex_fb_history_index], 0); } static int cmd_file_browser_history_forward(ecex_t *ed) { if (ecex_fb_history_count == 0 || ecex_fb_history_index + 1 >= ecex_fb_history_count) return ECEX_ERR; ecex_fb_history_index++; return ecex_file_browser_populate(ed, ecex_fb_history[ecex_fb_history_index], 0); } static int cmd_media_play_pause(ecex_t *ed) { return ecex_media_toggle_playback(ed); } static int ecex_register_builtins(ecex_t *ed) { ECEX_COMMAND("quit", cmd_quit); ECEX_COMMAND("force-quit", cmd_force_quit); ECEX_COMMAND("find-file", cmd_find_file); ECEX_COMMAND("file-browser", cmd_file_browser); ECEX_COMMAND("file-browser-here", cmd_file_browser_here); ECEX_COMMAND("file-browser-refresh", cmd_file_browser_refresh); ECEX_COMMAND("file-browser-parent", cmd_file_browser_parent); ECEX_COMMAND("file-browser-open", cmd_file_browser_open); ECEX_COMMAND("file-browser-preview", cmd_file_browser_preview); ECEX_COMMAND("file-browser-toggle-preview", cmd_file_browser_toggle_preview); ECEX_COMMAND("file-browser-history-back", cmd_file_browser_history_back); ECEX_COMMAND("file-browser-history-forward", cmd_file_browser_history_forward); ECEX_COMMAND("media-play-pause", cmd_media_play_pause); ECEX_COMMAND("save-buffer", cmd_save_buffer); ECEX_COMMAND("write-file", cmd_write_file); ECEX_COMMAND("eval-buffer", cmd_eval_buffer); ECEX_COMMAND("eval-line", cmd_eval_line); ECEX_COMMAND("eval-region", cmd_eval_region); ECEX_COMMAND("eval-selection", cmd_eval_region); ECEX_COMMAND("eval-file", cmd_eval_file); ECEX_COMMAND("eval-rerun-last", cmd_eval_rerun_last); ECEX_COMMAND("revert-eval-buffer", cmd_eval_rerun_last); ECEX_COMMAND("quit-window", cmd_quit_window); ECEX_COMMAND("reload-config", cmd_reload_config); ECEX_COMMAND("list-commands", cmd_list_commands); ECEX_COMMAND("list-buffers", cmd_list_buffers); ECEX_COMMAND("switch-buffer", cmd_switch_buffer); ECEX_COMMAND("kill-buffer", cmd_kill_buffer_command); ECEX_COMMAND("force-kill-buffer", cmd_force_kill_buffer_command); ECEX_COMMAND("compile", cmd_compile); ECEX_COMMAND("recompile", cmd_recompile); ECEX_COMMAND("grep", cmd_grep); ECEX_COMMAND("regrep", cmd_regrep); ECEX_COMMAND("next-error", cmd_next_error); ECEX_COMMAND("previous-error", cmd_previous_error); ECEX_COMMAND("comment-region", cmd_comment_region); ECEX_COMMAND("uncomment-region", cmd_uncomment_region); ECEX_COMMAND("toggle-line-numbers", cmd_toggle_line_numbers); ECEX_COMMAND("toggle-current-line", cmd_toggle_current_line); ECEX_COMMAND("isearch-forward", cmd_isearch_forward); ECEX_COMMAND("isearch-backward", cmd_isearch_backward); ECEX_COMMAND("indent-for-tab-command", cmd_indent_for_tab); ECEX_COMMAND("complete-at-point", cmd_complete_at_point); ECEX_COMMAND("complete-at-point-previous", cmd_complete_at_point_previous); ECEX_COMMAND("next-buffer", cmd_next_buffer); ECEX_COMMAND("previous-buffer", cmd_previous_buffer); ECEX_COMMAND("split-window-right", cmd_split_window_right); ECEX_COMMAND("split-window-vertically", cmd_split_window_right); ECEX_COMMAND("split-window-below", cmd_split_window_below); ECEX_COMMAND("split-window-horizontally", cmd_split_window_below); ECEX_COMMAND("other-window", cmd_other_window); ECEX_COMMAND("previous-window", cmd_previous_window); ECEX_COMMAND("delete-window", cmd_delete_window); ECEX_COMMAND("delete-other-windows", cmd_delete_other_windows); ECEX_COMMAND("balance-windows", cmd_balance_windows); ECEX_COMMAND("move-left", cmd_move_left); ECEX_COMMAND("move-right", cmd_move_right); ECEX_COMMAND("move-up", cmd_move_up); ECEX_COMMAND("move-down", cmd_move_down); ECEX_COMMAND("move-word-left", cmd_move_word_left); ECEX_COMMAND("move-word-right", cmd_move_word_right); ECEX_COMMAND("beginning-of-line", cmd_beginning_of_line); ECEX_COMMAND("end-of-line", cmd_end_of_line); ECEX_COMMAND("beginning-of-buffer", cmd_beginning_of_buffer); ECEX_COMMAND("end-of-buffer", cmd_end_of_buffer); ECEX_COMMAND("undo", cmd_undo); ECEX_COMMAND("redo", cmd_redo); ECEX_COMMAND("backspace", cmd_backspace); ECEX_COMMAND("delete-forward", cmd_delete_forward); ECEX_COMMAND("newline", cmd_newline); ECEX_COMMAND("kill-line", cmd_kill_line); ECEX_COMMAND("clear-buffer", cmd_clear_buffer); ECEX_COMMAND("set-mark", cmd_set_mark); ECEX_COMMAND("clear-mark", cmd_clear_mark); ECEX_COMMAND("yank", cmd_yank); ECEX_COMMAND("paste", cmd_yank); ECEX_COMMAND("clipboard-yank", cmd_yank); ECEX_COMMAND("copy", cmd_copy_region); ECEX_COMMAND("copy-region", cmd_copy_region); ECEX_COMMAND("copy-region-as-kill", cmd_copy_region); ECEX_COMMAND("cut", cmd_kill_region); ECEX_COMMAND("kill-region", cmd_kill_region); ECEX_COMMAND("clipboard-kill-region", cmd_kill_region); ECEX_BIND("LEFT", "move-left"); ECEX_BIND("RIGHT", "move-right"); ECEX_BIND("UP", "move-up"); ECEX_BIND("DOWN", "move-down"); ECEX_BIND("HOME", "beginning-of-line"); ECEX_BIND("END", "end-of-line"); ECEX_BIND("C-HOME", "beginning-of-buffer"); ECEX_BIND("C-END", "end-of-buffer"); ECEX_BIND("C-b", "move-left"); ECEX_BIND("C-f", "move-right"); ECEX_BIND("C-p", "move-up"); ECEX_BIND("C-n", "move-down"); ECEX_BIND("M-b", "move-word-left"); ECEX_BIND("M-f", "move-word-right"); ECEX_BIND("C-a", "beginning-of-line"); ECEX_BIND("C-e", "end-of-line"); ECEX_BIND("M-<", "beginning-of-buffer"); ECEX_BIND("M->", "end-of-buffer"); ECEX_BIND("C-k", "kill-line"); ECEX_BIND("C-SPC", "set-mark"); ECEX_BIND("C-y", "yank"); ECEX_BIND("C-v", "paste"); ECEX_BIND("C-S-v", "paste"); ECEX_BIND("C-c", "copy-region"); ECEX_BIND("C-S-c", "copy-region"); ECEX_BIND("C-S-x", "kill-region"); ECEX_BIND("M-w", "copy-region-as-kill"); ECEX_BIND("C-w", "kill-region"); ECEX_BIND("C-s", "isearch-forward"); ECEX_BIND("C-r", "isearch-backward"); ECEX_BIND("C-/", "undo"); ECEX_BIND("C-?", "undo"); ECEX_BIND("C-_", "undo"); ECEX_BIND("C-z", "undo"); ECEX_BIND("C-S-z", "redo"); ECEX_BIND("C-x u", "undo"); ECEX_BIND("C-x C-u", "undo"); ECEX_BIND("C-x C-f", "find-file"); ECEX_BIND("C-x f", "find-file"); ECEX_BIND("C-x d", "file-browser"); ECEX_BIND("C-x C-d", "file-browser-here"); ECEX_BIND("C-x C-s", "save-buffer"); ECEX_BIND("C-x C-w", "write-file"); ECEX_BIND("C-x C-b", "list-buffers"); ECEX_BIND("C-x b", "switch-buffer"); ECEX_BIND("C-x k", "kill-buffer"); ECEX_BIND("C-x K", "force-kill-buffer"); ECEX_BIND("C-x 2", "split-window-below"); ECEX_BIND("C-x 3", "split-window-right"); ECEX_BIND("C-x o", "other-window"); ECEX_BIND("C-x O", "previous-window"); ECEX_BIND("C-x 0", "delete-window"); ECEX_BIND("C-x 1", "delete-other-windows"); ECEX_BIND("C-x +", "balance-windows"); ECEX_BIND("C-x C-c", "quit"); ECEX_BIND("C-x C-S-c", "force-quit"); ECEX_BIND("C-x e b", "eval-buffer"); ECEX_BIND("C-x e l", "eval-line"); ECEX_BIND("C-x C-e", "eval-line"); ECEX_BIND("C-x e r", "eval-region"); ECEX_BIND("C-x e f", "eval-file"); ECEX_BIND("C-x C-r", "reload-config"); ECEX_BIND("F5", "reload-config"); ECEX_BIND("M-g n", "next-error"); ECEX_BIND("M-g p", "previous-error"); ECEX_BIND("M-;", "comment-region"); ECEX_BIND("BACKSPACE", "backspace"); ECEX_BIND("DELETE", "delete-forward"); ECEX_BIND("ENTER", "newline"); ECEX_BIND("TAB", "indent-for-tab-command"); ECEX_BIND("C-TAB", "complete-at-point"); ECEX_BIND("C-S-TAB", "complete-at-point-previous"); ecex_define_major_mode(ed, "fundamental-mode"); ecex_define_major_mode(ed, "eval-output-mode"); ecex_define_major_mode(ed, "special-mode"); ecex_define_major_mode(ed, "file-browser-mode"); ecex_define_major_mode(ed, "media-preview-mode"); ecex_define_major_mode(ed, "markdown-mode"); ecex_bind_mode_key(ed, "eval-output-mode", "g", "eval-rerun-last"); ecex_bind_mode_key(ed, "eval-output-mode", "q", "quit-window"); ecex_bind_mode_key(ed, "eval-output-mode", "r", "eval-rerun-last"); ecex_bind_mode_key(ed, "special-mode", "q", "quit-window"); ecex_bind_mode_key(ed, "file-browser-mode", "g", "file-browser-refresh"); ecex_bind_mode_key(ed, "file-browser-mode", "r", "file-browser-refresh"); ecex_bind_mode_key(ed, "file-browser-mode", "h", "file-browser-parent"); ecex_bind_mode_key(ed, "file-browser-mode", "l", "file-browser-open"); ecex_bind_mode_key(ed, "file-browser-mode", "v", "file-browser-preview"); ecex_bind_mode_key(ed, "file-browser-mode", "m", "file-browser-toggle-preview"); ecex_bind_mode_key(ed, "file-browser-mode", "b", "file-browser-history-back"); ecex_bind_mode_key(ed, "file-browser-mode", "f", "file-browser-history-forward"); ecex_bind_mode_key(ed, "file-browser-mode", "q", "quit-window"); ecex_bind_mode_key(ed, "media-preview-mode", "SPC", "media-play-pause"); ecex_bind_mode_key(ed, "media-preview-mode", "p", "media-play-pause"); ecex_bind_mode_key(ed, "media-preview-mode", "q", "quit-window"); ecex_bind_mode_key(ed, "special-mode", "g", "recompile"); ecex_bind_mode_key(ed, "special-mode", "n", "next-error"); ecex_bind_mode_key(ed, "special-mode", "p", "previous-error"); return ECEX_OK; } #undef ECEX_COMMAND #undef ECEX_BIND #undef CURRENT_BUFFER_OR_ERR ecex_t *ecex_new(void) { ecex_t *ed = calloc(1, sizeof(*ed)); if (!ed) return NULL; ed->plugins = ecex_plugin_runtime_new(); if (!ed->plugins) { ecex_free(ed); return NULL; } ecex_theme_set_defaults(ed); buffer_t *scratch = buffer_new("*scratch*", NULL, 0); if (!scratch || ecex_add_buffer(ed, scratch) != ECEX_OK) { buffer_free(scratch); ecex_free(ed); return NULL; } ed->current_buffer_index = 0; ed->current_buffer = scratch; ed->next_major_mode_id = 1; ed->window_cap = ECEX_INITIAL_WINDOW_CAP; ed->windows = calloc(ed->window_cap, sizeof(*ed->windows)); if (!ed->windows) { ecex_free(ed); return NULL; } ed->window_count = 1; ed->current_window_index = 0; ed->windows[0].buffer = scratch; ed->windows[0].x = 0.0f; ed->windows[0].y = 0.0f; ed->windows[0].w = 1.0f; ed->windows[0].h = 1.0f; if (ecex_register_builtins(ed) != ECEX_OK) { ecex_free(ed); return NULL; } ecex_buffer_set_major_mode_by_name(ed, scratch, "fundamental-mode"); return ed; } void ecex_free(ecex_t *ed) { if (!ed) return; /* Buffers may hold renderer/animation callbacks and userdata destructors * compiled by CCDJIT config modules. Run those destructors while their JIT * modules are still alive; freeing modules first leaves callback pointers * dangling and can segfault during buffer teardown. */ for (size_t i = 0; i < ed->buffer_count; i++) { buffer_free(ed->buffers[i]); } ecex_clear_command_hooks(ed); ecex_clear_prefix_hooks(ed); ecex_clear_buffer_hooks(ed); ecex_clear_completion_providers(ed); for (size_t i = 0; i < ed->jit_module_count; i++) { ccdjit_module_free((ccdjit_module *)ed->jit_modules[i]); } ecex_plugin_runtime_free(ed->plugins); ed->plugins = NULL; for (size_t i = 0; i < ed->command_count; i++) { free(ed->commands[i].name); } for (size_t i = 0; i < ed->keybind_count; i++) { free(ed->keybinds[i].key); free(ed->keybinds[i].command); } for (size_t i = 0; i < ed->mode_keybind_count; i++) { free(ed->mode_keybinds[i].key); free(ed->mode_keybinds[i].command); } for (size_t i = 0; i < ed->major_mode_count; i++) { free(ed->major_modes[i].name); } free(ed->jit_modules); free(ed->windows); free(ed->buffers); free(ed->commands); free(ed->keybinds); free(ed->mode_keybinds); free(ed->command_hooks); free(ed->prefix_hooks); free(ed->buffer_hooks); free(ed->completion_providers); free(ed->major_modes); free(ed->last_eval_source); free(ed->last_eval_filename); free(ed->last_compile_command); free(ed->last_grep_command); free(ed->config_path); free(ed->clipboard_text); free(ed->theme.font_path); free(ed); ecex_log_flush(); } int ecex_reserve_buffers(ecex_t *ed, size_t needed) { ECEX_RETURN_ERR_IF_NULL(ed); if (needed <= ed->buffer_cap) return ECEX_OK; while (ed->buffer_cap < needed) { if (ECEX_GROW_ARRAY(ed->buffers, ed->buffer_count, ed->buffer_cap, ECEX_INITIAL_BUFFER_CAP) != ECEX_OK) { return ECEX_ERR; } } return ECEX_OK; } int ecex_add_buffer(ecex_t *ed, buffer_t *buffer) { if (!ed || !buffer) return ECEX_ERR; if (ecex_reserve_buffers(ed, ed->buffer_count + 1) != ECEX_OK) return ECEX_ERR; ed->buffers[ed->buffer_count++] = buffer; if (!ed->current_buffer) { ed->current_buffer_index = 0; ed->current_buffer = buffer; } return ECEX_OK; } buffer_t *ecex_create_buffer(ecex_t *ed, const char *name, const char *path, int read_only) { if (!ed || !name) return NULL; buffer_t *buffer = buffer_new(name, path, read_only); if (!buffer) return NULL; if (ecex_add_buffer(ed, buffer) != ECEX_OK) { buffer_free(buffer); return NULL; } ecex_notify_buffer_hooks(ed, buffer, ECEX_BUFFER_HOOK_CREATE); return buffer; } buffer_t *ecex_find_buffer(ecex_t *ed, const char *name) { if (!ed || !name) return NULL; for (size_t i = 0; i < ed->buffer_count; i++) { if (strcmp(ed->buffers[i]->name, name) == 0) { return ed->buffers[i]; } } return NULL; } static int ecex_buffer_index_of(ecex_t *ed, buffer_t *buffer, size_t *out_index) { if (!ed || !buffer) return ECEX_ERR; for (size_t i = 0; i < ed->buffer_count; i++) { if (ed->buffers[i] == buffer) { if (out_index) *out_index = i; return ECEX_OK; } } return ECEX_ERR; } static int ecex_set_current_buffer_index(ecex_t *ed, size_t index) { if (!ed || index >= ed->buffer_count) return ECEX_ERR; buffer_t *next = ed->buffers[index]; if (ed->current_buffer && ed->current_buffer != next) { ed->previous_buffer = ed->current_buffer; } ed->current_buffer_index = index; ed->current_buffer = next; ecex_window_t *win = ecex_current_window(ed); if (win) win->buffer = ed->current_buffer; ecex_notify_buffer_hooks(ed, next, ECEX_BUFFER_HOOK_SWITCH); return ECEX_OK; } int ecex_sync_current_buffer(ecex_t *ed) { if (!ed) return ECEX_ERR; ecex_window_t *win = ecex_current_window(ed); if (!win || !win->buffer) return ECEX_ERR; buffer_t *next = win->buffer; if (ed->current_buffer && ed->current_buffer != next) { ed->previous_buffer = ed->current_buffer; } ed->current_buffer = next; for (size_t i = 0; i < ed->buffer_count; i++) { if (ed->buffers[i] == next) { ed->current_buffer_index = i; ecex_notify_buffer_hooks(ed, next, ECEX_BUFFER_HOOK_SWITCH); return ECEX_OK; } } return ECEX_ERR; } int ecex_switch_buffer(ecex_t *ed, const char *name) { if (!ed) return ECEX_ERR; if (!name || name[0] == '\0') { buffer_t *other = ecex_other_buffer(ed); size_t index = 0; if (!other || ecex_buffer_index_of(ed, other, &index) != ECEX_OK) return ECEX_ERR; return ecex_set_current_buffer_index(ed, index); } for (size_t i = 0; i < ed->buffer_count; i++) { if (strcmp(ed->buffers[i]->name, name) == 0) { return ecex_set_current_buffer_index(ed, i); } } return ECEX_ERR; } buffer_t *ecex_current_buffer(ecex_t *ed) { if (!ed) return NULL; ecex_window_t *win = ecex_current_window(ed); if (win && win->buffer) return win->buffer; return ed->current_buffer; } buffer_t *ecex_other_buffer(ecex_t *ed) { if (!ed || ed->buffer_count == 0) return NULL; buffer_t *current = ecex_current_buffer(ed); if (ed->previous_buffer && ed->previous_buffer != current && ecex_buffer_index_of(ed, ed->previous_buffer, NULL) == ECEX_OK) { return ed->previous_buffer; } for (size_t i = 0; i < ed->buffer_count; i++) { if (ed->buffers[i] && ed->buffers[i] != current) return ed->buffers[i]; } return current; } ecex_window_t *ecex_current_window(ecex_t *ed) { if (!ed || ed->window_count == 0 || ed->current_window_index >= ed->window_count) return NULL; return &ed->windows[ed->current_window_index]; } size_t ecex_window_count(ecex_t *ed) { return ed ? ed->window_count : 0; } static int ecex_reserve_windows(ecex_t *ed, size_t needed) { if (!ed) return ECEX_ERR; if (needed <= ed->window_cap) return ECEX_OK; while (ed->window_cap < needed) { if (ECEX_GROW_ARRAY(ed->windows, ed->window_count, ed->window_cap, ECEX_INITIAL_WINDOW_CAP) != ECEX_OK) { return ECEX_ERR; } } return ECEX_OK; } static int ecex_add_window(ecex_t *ed, ecex_window_t win) { if (!ed || !win.buffer) return ECEX_ERR; if (ecex_reserve_windows(ed, ed->window_count + 1) != ECEX_OK) return ECEX_ERR; ed->windows[ed->window_count++] = win; return ECEX_OK; } int ecex_split_window_vertically(ecex_t *ed) { ecex_window_t *active = ecex_current_window(ed); if (!active || !active->buffer || active->w < 0.08f) return ECEX_ERR; ecex_window_t new_win = *active; float half = active->w * 0.5f; active->w = half; new_win.x = active->x + half; new_win.w = half; if (ecex_add_window(ed, new_win) != ECEX_OK) return ECEX_ERR; ed->current_window_index = ed->window_count - 1; return ecex_sync_current_buffer(ed); } int ecex_split_window_horizontally(ecex_t *ed) { ecex_window_t *active = ecex_current_window(ed); if (!active || !active->buffer || active->h < 0.08f) return ECEX_ERR; ecex_window_t new_win = *active; float half = active->h * 0.5f; active->h = half; new_win.y = active->y + half; new_win.h = half; if (ecex_add_window(ed, new_win) != ECEX_OK) return ECEX_ERR; ed->current_window_index = ed->window_count - 1; return ecex_sync_current_buffer(ed); } int ecex_other_window(ecex_t *ed) { if (!ed || ed->window_count == 0) return ECEX_ERR; ed->current_window_index = (ed->current_window_index + 1) % ed->window_count; return ecex_sync_current_buffer(ed); } int ecex_previous_window(ecex_t *ed) { if (!ed || ed->window_count == 0) return ECEX_ERR; if (ed->current_window_index == 0) ed->current_window_index = ed->window_count - 1; else ed->current_window_index--; return ecex_sync_current_buffer(ed); } int ecex_delete_window(ecex_t *ed) { if (!ed || ed->window_count <= 1 || ed->current_window_index >= ed->window_count) return ECEX_ERR; size_t index = ed->current_window_index; for (size_t i = index; i + 1 < ed->window_count; i++) { ed->windows[i] = ed->windows[i + 1]; } ed->window_count--; if (ed->current_window_index >= ed->window_count) ed->current_window_index = ed->window_count - 1; return ecex_sync_current_buffer(ed); } int ecex_delete_other_windows(ecex_t *ed) { ecex_window_t *active = ecex_current_window(ed); if (!ed || !active) return ECEX_ERR; ecex_window_t keep = *active; keep.x = 0.0f; keep.y = 0.0f; keep.w = 1.0f; keep.h = 1.0f; ed->windows[0] = keep; ed->window_count = 1; ed->current_window_index = 0; return ecex_sync_current_buffer(ed); } int ecex_balance_windows(ecex_t *ed) { if (!ed || ed->window_count == 0) return ECEX_ERR; size_t n = ed->window_count; size_t cols = 1; while (cols * cols < n) cols++; size_t rows = (n + cols - 1) / cols; for (size_t i = 0; i < n; i++) { size_t row = i / cols; size_t col = i % cols; ed->windows[i].x = (float)col / (float)cols; ed->windows[i].y = (float)row / (float)rows; ed->windows[i].w = 1.0f / (float)cols; ed->windows[i].h = 1.0f / (float)rows; } return ecex_sync_current_buffer(ed); } int ecex_next_buffer(ecex_t *ed) { if (!ed || ed->buffer_count == 0) return ECEX_ERR; ecex_sync_current_buffer(ed); return ecex_set_current_buffer_index(ed, (ed->current_buffer_index + 1) % ed->buffer_count); } int ecex_previous_buffer(ecex_t *ed) { if (!ed || ed->buffer_count == 0) return ECEX_ERR; ecex_sync_current_buffer(ed); if (ed->current_buffer_index == 0) { ed->current_buffer_index = ed->buffer_count - 1; } else { ed->current_buffer_index--; } return ecex_set_current_buffer_index(ed, ed->current_buffer_index); } static int ecex_kill_buffer_impl(ecex_t *ed, const char *name, int force) { if (!ed || ed->buffer_count == 0) return ECEX_ERR; size_t index = ed->buffer_count; if (!name || name[0] == '\0') { buffer_t *current = ecex_current_buffer(ed); if (!current || ecex_buffer_index_of(ed, current, &index) != ECEX_OK) return ECEX_ERR; } else { for (size_t i = 0; i < ed->buffer_count; i++) { if (strcmp(ed->buffers[i]->name, name) == 0) { index = i; break; } } } if (index == ed->buffer_count) return ECEX_ERR; buffer_t *victim = ed->buffers[index]; if (ed->previous_buffer == victim) ed->previous_buffer = NULL; if (!force && victim->modified && !victim->read_only) { fprintf(stderr, "ecex: refusing to kill modified buffer '%s'; save it or use force-kill-buffer.\n", victim->name ? victim->name : ""); return ECEX_ERR; } for (size_t i = index; i + 1 < ed->buffer_count; i++) ed->buffers[i] = ed->buffers[i + 1]; ed->buffer_count--; buffer_t *fallback = ecex_other_buffer(ed); if (fallback == victim) fallback = ed->buffer_count > 0 ? ed->buffers[0] : NULL; for (size_t i = 0; i < ed->window_count; i++) { if (ed->windows[i].buffer == victim) ed->windows[i].buffer = fallback; } ecex_notify_buffer_hooks(ed, victim, ECEX_BUFFER_HOOK_KILL); buffer_free(victim); if (ed->buffer_count == 0) { ed->current_buffer = NULL; ed->current_buffer_index = 0; ed->window_count = 0; ed->current_window_index = 0; return ECEX_OK; } if (ed->current_window_index >= ed->window_count) { ed->current_window_index = ed->window_count ? ed->window_count - 1 : 0; } return ecex_sync_current_buffer(ed); } int ecex_kill_buffer(ecex_t *ed, const char *name) { return ecex_kill_buffer_impl(ed, name, 0); } int ecex_kill_buffer_force(ecex_t *ed, const char *name) { return ecex_kill_buffer_impl(ed, name, 1); } int ecex_has_modified_buffers(ecex_t *ed) { if (!ed) return 0; for (size_t i = 0; i < ed->buffer_count; i++) { buffer_t *buf = ed->buffers[i]; if (buf && buf->modified && !buf->read_only) return 1; } return 0; } int ecex_keep_jit_module(ecex_t *ed, void *module) { if (!ed || !module) return ECEX_ERR; if (ECEX_GROW_ARRAY(ed->jit_modules, ed->jit_module_count, ed->jit_module_cap, 8) != ECEX_OK) { return ECEX_ERR; } ed->jit_modules[ed->jit_module_count++] = module; return ECEX_OK; } int ecex_set_config_path(ecex_t *ed, const char *path) { if (!ed) return ECEX_ERR; char *copy = NULL; if (path && path[0]) { copy = ecex_strdup(path); if (!copy) return ECEX_ERR; } free(ed->config_path); ed->config_path = copy; return ECEX_OK; } const char *ecex_config_path(ecex_t *ed) { if (!ed) return NULL; return ed->config_path; } static void ecex_clear_commands(ecex_t *ed) { if (!ed) return; for (size_t i = 0; i < ed->command_count; i++) { free(ed->commands[i].name); } ed->command_count = 0; } static void ecex_clear_keybinds(ecex_t *ed) { if (!ed) return; for (size_t i = 0; i < ed->keybind_count; i++) { free(ed->keybinds[i].key); free(ed->keybinds[i].command); } ed->keybind_count = 0; } static void ecex_clear_mode_keybinds(ecex_t *ed) { if (!ed) return; for (size_t i = 0; i < ed->mode_keybind_count; i++) { free(ed->mode_keybinds[i].key); free(ed->mode_keybinds[i].command); } ed->mode_keybind_count = 0; } static void ecex_command_hook_clear(ecex_command_hook_t *hook) { if (!hook) return; if (hook->free_fn && hook->userdata) hook->free_fn(hook->userdata); free(hook->name); memset(hook, 0, sizeof(*hook)); } static void ecex_prefix_hook_clear(ecex_prefix_hook_t *hook) { if (!hook) return; if (hook->free_fn && hook->userdata) hook->free_fn(hook->userdata); free(hook->name); memset(hook, 0, sizeof(*hook)); } static void ecex_buffer_hook_clear(ecex_buffer_hook_t *hook) { if (!hook) return; if (hook->free_fn && hook->userdata) hook->free_fn(hook->userdata); free(hook->name); memset(hook, 0, sizeof(*hook)); } static void ecex_completion_provider_clear(ecex_completion_provider_t *provider) { if (!provider) return; if (provider->free_fn && provider->userdata) provider->free_fn(provider->userdata); for (size_t i = 0; i < provider->word_count; i++) { free(provider->words[i]); if (provider->word_details) free(provider->word_details[i]); } free(provider->words); free(provider->word_details); free(provider->detail); free(provider->name); memset(provider, 0, sizeof(*provider)); } static void ecex_clear_command_hooks(ecex_t *ed) { if (!ed) return; for (size_t i = 0; i < ed->command_hook_count; i++) { ecex_command_hook_clear(&ed->command_hooks[i]); } ed->command_hook_count = 0; } static void ecex_clear_prefix_hooks(ecex_t *ed) { if (!ed) return; for (size_t i = 0; i < ed->prefix_hook_count; i++) { ecex_prefix_hook_clear(&ed->prefix_hooks[i]); } ed->prefix_hook_count = 0; } static void ecex_clear_buffer_hooks(ecex_t *ed) { if (!ed) return; for (size_t i = 0; i < ed->buffer_hook_count; i++) { ecex_buffer_hook_clear(&ed->buffer_hooks[i]); } ed->buffer_hook_count = 0; } static void ecex_clear_completion_providers(ecex_t *ed) { if (!ed) return; for (size_t i = 0; i < ed->completion_provider_count; i++) { ecex_completion_provider_clear(&ed->completion_providers[i]); } ed->completion_provider_count = 0; } typedef struct ecex_binding_snapshot { ecex_command_t *commands; size_t command_count; size_t command_cap; ecex_keybind_t *keybinds; size_t keybind_count; size_t keybind_cap; ecex_mode_keybind_t *mode_keybinds; size_t mode_keybind_count; size_t mode_keybind_cap; ecex_command_hook_t *command_hooks; size_t command_hook_count; size_t command_hook_cap; ecex_prefix_hook_t *prefix_hooks; size_t prefix_hook_count; size_t prefix_hook_cap; ecex_buffer_hook_t *buffer_hooks; size_t buffer_hook_count; size_t buffer_hook_cap; ecex_completion_provider_t *completion_providers; size_t completion_provider_count; size_t completion_provider_cap; ecex_theme_t theme; } ecex_binding_snapshot_t; static void ecex_theme_snapshot_free(ecex_theme_t *theme) { if (!theme) return; free(theme->font_path); theme->font_path = NULL; } static int ecex_theme_clone(ecex_theme_t *out, const ecex_theme_t *in) { if (!out || !in) return ECEX_ERR; *out = *in; out->font_path = NULL; if (in->font_path) { out->font_path = ecex_strdup(in->font_path); if (!out->font_path) return ECEX_ERR; } return ECEX_OK; } static void ecex_snapshot_free(ecex_binding_snapshot_t *snap) { if (!snap) return; for (size_t i = 0; i < snap->command_count; i++) free(snap->commands[i].name); for (size_t i = 0; i < snap->keybind_count; i++) { free(snap->keybinds[i].key); free(snap->keybinds[i].command); } for (size_t i = 0; i < snap->mode_keybind_count; i++) { free(snap->mode_keybinds[i].key); free(snap->mode_keybinds[i].command); } for (size_t i = 0; i < snap->command_hook_count; i++) { ecex_command_hook_clear(&snap->command_hooks[i]); } for (size_t i = 0; i < snap->prefix_hook_count; i++) { ecex_prefix_hook_clear(&snap->prefix_hooks[i]); } for (size_t i = 0; i < snap->buffer_hook_count; i++) { ecex_buffer_hook_clear(&snap->buffer_hooks[i]); } for (size_t i = 0; i < snap->completion_provider_count; i++) { ecex_completion_provider_clear(&snap->completion_providers[i]); } free(snap->commands); free(snap->keybinds); free(snap->mode_keybinds); free(snap->command_hooks); free(snap->prefix_hooks); free(snap->buffer_hooks); free(snap->completion_providers); ecex_theme_snapshot_free(&snap->theme); memset(snap, 0, sizeof(*snap)); } static int ecex_snapshot_clone(ecex_binding_snapshot_t *snap, ecex_t *ed) { if (!snap || !ed) return ECEX_ERR; memset(snap, 0, sizeof(*snap)); if (ecex_theme_clone(&snap->theme, &ed->theme) != ECEX_OK) return ECEX_ERR; if (ed->command_cap) { snap->commands = calloc(ed->command_cap, sizeof(*snap->commands)); if (!snap->commands) goto fail; snap->command_cap = ed->command_cap; snap->command_count = ed->command_count; for (size_t i = 0; i < ed->command_count; i++) { snap->commands[i].fn = ed->commands[i].fn; snap->commands[i].name = ecex_strdup(ed->commands[i].name); if (!snap->commands[i].name) goto fail; } } if (ed->keybind_cap) { snap->keybinds = calloc(ed->keybind_cap, sizeof(*snap->keybinds)); if (!snap->keybinds) goto fail; snap->keybind_cap = ed->keybind_cap; snap->keybind_count = ed->keybind_count; for (size_t i = 0; i < ed->keybind_count; i++) { snap->keybinds[i].key = ecex_strdup(ed->keybinds[i].key); snap->keybinds[i].command = ecex_strdup(ed->keybinds[i].command); if (!snap->keybinds[i].key || !snap->keybinds[i].command) goto fail; } } if (ed->mode_keybind_cap) { snap->mode_keybinds = calloc(ed->mode_keybind_cap, sizeof(*snap->mode_keybinds)); if (!snap->mode_keybinds) goto fail; snap->mode_keybind_cap = ed->mode_keybind_cap; snap->mode_keybind_count = ed->mode_keybind_count; for (size_t i = 0; i < ed->mode_keybind_count; i++) { snap->mode_keybinds[i].mode = ed->mode_keybinds[i].mode; snap->mode_keybinds[i].key = ecex_strdup(ed->mode_keybinds[i].key); snap->mode_keybinds[i].command = ecex_strdup(ed->mode_keybinds[i].command); if (!snap->mode_keybinds[i].key || !snap->mode_keybinds[i].command) goto fail; } } snap->command_hooks = ed->command_hooks; snap->command_hook_count = ed->command_hook_count; snap->command_hook_cap = ed->command_hook_cap; ed->command_hooks = NULL; ed->command_hook_count = 0; ed->command_hook_cap = 0; snap->prefix_hooks = ed->prefix_hooks; snap->prefix_hook_count = ed->prefix_hook_count; snap->prefix_hook_cap = ed->prefix_hook_cap; ed->prefix_hooks = NULL; ed->prefix_hook_count = 0; ed->prefix_hook_cap = 0; snap->buffer_hooks = ed->buffer_hooks; snap->buffer_hook_count = ed->buffer_hook_count; snap->buffer_hook_cap = ed->buffer_hook_cap; ed->buffer_hooks = NULL; ed->buffer_hook_count = 0; ed->buffer_hook_cap = 0; snap->completion_providers = ed->completion_providers; snap->completion_provider_count = ed->completion_provider_count; snap->completion_provider_cap = ed->completion_provider_cap; ed->completion_providers = NULL; ed->completion_provider_count = 0; ed->completion_provider_cap = 0; return ECEX_OK; fail: ecex_snapshot_free(snap); return ECEX_ERR; } static void ecex_restore_snapshot(ecex_t *ed, ecex_binding_snapshot_t *snap) { if (!ed || !snap) return; ecex_clear_commands(ed); ecex_clear_keybinds(ed); ecex_clear_mode_keybinds(ed); ecex_clear_command_hooks(ed); ecex_clear_prefix_hooks(ed); ecex_clear_buffer_hooks(ed); ecex_clear_completion_providers(ed); free(ed->commands); free(ed->keybinds); free(ed->mode_keybinds); free(ed->command_hooks); free(ed->prefix_hooks); free(ed->buffer_hooks); free(ed->completion_providers); free(ed->theme.font_path); ed->commands = snap->commands; ed->command_count = snap->command_count; ed->command_cap = snap->command_cap; ed->keybinds = snap->keybinds; ed->keybind_count = snap->keybind_count; ed->keybind_cap = snap->keybind_cap; ed->mode_keybinds = snap->mode_keybinds; ed->mode_keybind_count = snap->mode_keybind_count; ed->mode_keybind_cap = snap->mode_keybind_cap; ed->command_hooks = snap->command_hooks; ed->command_hook_count = snap->command_hook_count; ed->command_hook_cap = snap->command_hook_cap; ed->prefix_hooks = snap->prefix_hooks; ed->prefix_hook_count = snap->prefix_hook_count; ed->prefix_hook_cap = snap->prefix_hook_cap; ed->buffer_hooks = snap->buffer_hooks; ed->buffer_hook_count = snap->buffer_hook_count; ed->buffer_hook_cap = snap->buffer_hook_cap; ed->completion_providers = snap->completion_providers; ed->completion_provider_count = snap->completion_provider_count; ed->completion_provider_cap = snap->completion_provider_cap; ed->theme = snap->theme; memset(snap, 0, sizeof(*snap)); } int ecex_reload_config(ecex_t *ed) { if (!ed || !ed->config_path || !ed->config_path[0]) { fprintf(stderr, "ecex: no config file to reload; start with --config path/to/ecexrc.c\n"); return ECEX_ERR; } char *path = ecex_strdup(ed->config_path); if (!path) return ECEX_ERR; ecex_binding_snapshot_t snapshot; if (ecex_snapshot_clone(&snapshot, ed) != ECEX_OK) { free(path); fprintf(stderr, "ecex: failed to snapshot config state before reload\n"); return ECEX_ERR; } ecex_clear_commands(ed); ecex_clear_keybinds(ed); ecex_clear_mode_keybinds(ed); int result = ECEX_ERR; if (ecex_register_builtins(ed) == ECEX_OK) { result = ecex_load_c_config(ed, path); if (result == ECEX_OK) result = ecex_validate_bindings(ed); } if (result != ECEX_OK) { fprintf(stderr, "ecex: config reload failed; keeping previous config active\n"); ecex_restore_snapshot(ed, &snapshot); } else { ecex_snapshot_free(&snapshot); } free(path); return result; } int ecex_config_register_commands(ecex_t *ed, const ecex_config_command_t *commands, size_t count) { if (!ed || (!commands && count != 0)) return ECEX_ERR; for (size_t i = 0; i < count; i++) { if (!commands[i].name || !commands[i].fn) return ECEX_ERR; if (ecex_register_command(ed, commands[i].name, commands[i].fn) != ECEX_OK) return ECEX_ERR; } return ECEX_OK; } int ecex_config_bind_keys(ecex_t *ed, const ecex_config_keybind_t *bindings, size_t count) { if (!ed || (!bindings && count != 0)) return ECEX_ERR; for (size_t i = 0; i < count; i++) { if (!bindings[i].key || !bindings[i].command) return ECEX_ERR; if (ecex_bind_key(ed, bindings[i].key, bindings[i].command) != ECEX_OK) return ECEX_ERR; } return ECEX_OK; } int ecex_config_bind_mode_keys(ecex_t *ed, const ecex_config_mode_keybind_t *bindings, size_t count) { if (!ed || (!bindings && count != 0)) return ECEX_ERR; for (size_t i = 0; i < count; i++) { if (!bindings[i].mode || !bindings[i].key || !bindings[i].command) return ECEX_ERR; if (ecex_bind_mode_key(ed, bindings[i].mode, bindings[i].key, bindings[i].command) != ECEX_OK) return ECEX_ERR; } return ECEX_OK; } int ecex_config_define_modes(ecex_t *ed, const char *const *modes, size_t count) { if (!ed || (!modes && count != 0)) return ECEX_ERR; for (size_t i = 0; i < count; i++) { if (!modes[i] || !ecex_define_major_mode(ed, modes[i])) return ECEX_ERR; } return ECEX_OK; } int ecex_apply_theme(ecex_t *ed, const ecex_theme_t *theme) { if (!ed || !theme) return ECEX_ERR; char *font = NULL; if (theme->font_path) { font = ecex_strdup(theme->font_path); if (!font) return ECEX_ERR; } int font_changed = ed->theme.font_size != theme->font_size || ((ed->theme.font_path || theme->font_path) && (!ed->theme.font_path || !theme->font_path || strcmp(ed->theme.font_path, theme->font_path) != 0)); free(ed->theme.font_path); ed->theme = *theme; ed->theme.font_path = font; if (font_changed) ecex_mark_font_changed(ed); else ecex_mark_ui_changed(ed); return ECEX_OK; } int ecex_register_command(ecex_t *ed, const char *name, ecex_command_fn fn) { if (!ed || !name || !fn) return ECEX_ERR; for (size_t i = 0; i < ed->command_count; i++) { if (strcmp(ed->commands[i].name, name) == 0) { fprintf(stderr, "ecex: command warning: replacing existing command '%s'\n", name); ed->commands[i].fn = fn; return ECEX_OK; } } if (ECEX_GROW_ARRAY(ed->commands, ed->command_count, ed->command_cap, ECEX_INITIAL_COMMAND_CAP) != ECEX_OK) { return ECEX_ERR; } char *copy = ecex_strdup(name); if (!copy) return ECEX_ERR; ed->commands[ed->command_count].name = copy; ed->commands[ed->command_count].fn = fn; ed->command_count++; return ECEX_OK; } int ecex_execute_command(ecex_t *ed, const char *name) { if (!ed || !name) return ECEX_ERR; for (size_t i = 0; i < ed->command_count; i++) { if (strcmp(ed->commands[i].name, name) == 0) { ecex_command_fn fn = ed->commands[i].fn; for (size_t j = 0; j < ed->command_hook_count; j++) { ecex_command_hook_t *hook = &ed->command_hooks[j]; if (hook->fn) hook->fn(ed, name, ECEX_COMMAND_HOOK_BEFORE, 0, hook->userdata); } int result = fn(ed); for (size_t j = 0; j < ed->command_hook_count; j++) { ecex_command_hook_t *hook = &ed->command_hooks[j]; if (hook->fn) hook->fn(ed, name, ECEX_COMMAND_HOOK_AFTER, result, hook->userdata); } return result; } } return ECEX_ERR; } int ecex_add_command_hook(ecex_t *ed, const char *name, ecex_command_hook_fn fn, void *userdata, ecex_hook_free_fn free_fn) { if (!ed || !name || !name[0] || !fn) return ECEX_ERR; char *name_copy = ecex_strdup(name); if (!name_copy) return ECEX_ERR; for (size_t i = 0; i < ed->command_hook_count; i++) { if (strcmp(ed->command_hooks[i].name, name) == 0) { ecex_command_hook_clear(&ed->command_hooks[i]); ed->command_hooks[i].name = name_copy; ed->command_hooks[i].fn = fn; ed->command_hooks[i].userdata = userdata; ed->command_hooks[i].free_fn = free_fn; return ECEX_OK; } } if (ECEX_GROW_ARRAY(ed->command_hooks, ed->command_hook_count, ed->command_hook_cap, ECEX_INITIAL_HOOK_CAP) != ECEX_OK) { free(name_copy); return ECEX_ERR; } ecex_command_hook_t *hook = &ed->command_hooks[ed->command_hook_count++]; memset(hook, 0, sizeof(*hook)); hook->name = name_copy; hook->fn = fn; hook->userdata = userdata; hook->free_fn = free_fn; return ECEX_OK; } int ecex_remove_command_hook(ecex_t *ed, const char *name) { if (!ed || !name) return ECEX_ERR; for (size_t i = 0; i < ed->command_hook_count; i++) { if (strcmp(ed->command_hooks[i].name, name) != 0) continue; ecex_command_hook_clear(&ed->command_hooks[i]); if (i + 1 < ed->command_hook_count) { memmove(&ed->command_hooks[i], &ed->command_hooks[i + 1], (ed->command_hook_count - i - 1) * sizeof(ed->command_hooks[i])); } ed->command_hook_count--; return ECEX_OK; } return ECEX_ERR; } int ecex_add_prefix_hook(ecex_t *ed, const char *name, ecex_prefix_hook_fn fn, void *userdata, ecex_hook_free_fn free_fn) { if (!ed || !name || !name[0] || !fn) return ECEX_ERR; char *name_copy = ecex_strdup(name); if (!name_copy) return ECEX_ERR; for (size_t i = 0; i < ed->prefix_hook_count; i++) { if (strcmp(ed->prefix_hooks[i].name, name) == 0) { ecex_prefix_hook_clear(&ed->prefix_hooks[i]); ed->prefix_hooks[i].name = name_copy; ed->prefix_hooks[i].fn = fn; ed->prefix_hooks[i].userdata = userdata; ed->prefix_hooks[i].free_fn = free_fn; return ECEX_OK; } } if (ECEX_GROW_ARRAY(ed->prefix_hooks, ed->prefix_hook_count, ed->prefix_hook_cap, ECEX_INITIAL_HOOK_CAP) != ECEX_OK) { free(name_copy); return ECEX_ERR; } ecex_prefix_hook_t *hook = &ed->prefix_hooks[ed->prefix_hook_count++]; memset(hook, 0, sizeof(*hook)); hook->name = name_copy; hook->fn = fn; hook->userdata = userdata; hook->free_fn = free_fn; return ECEX_OK; } int ecex_remove_prefix_hook(ecex_t *ed, const char *name) { if (!ed || !name) return ECEX_ERR; for (size_t i = 0; i < ed->prefix_hook_count; i++) { if (strcmp(ed->prefix_hooks[i].name, name) != 0) continue; ecex_prefix_hook_clear(&ed->prefix_hooks[i]); if (i + 1 < ed->prefix_hook_count) { memmove(&ed->prefix_hooks[i], &ed->prefix_hooks[i + 1], (ed->prefix_hook_count - i - 1) * sizeof(ed->prefix_hooks[i])); } ed->prefix_hook_count--; return ECEX_OK; } return ECEX_ERR; } void ecex_notify_prefix_hooks(ecex_t *ed, const char *prefix, int event) { if (!ed || !prefix) return; for (size_t i = 0; i < ed->prefix_hook_count; i++) { ecex_prefix_hook_t *hook = &ed->prefix_hooks[i]; if (hook->fn) hook->fn(ed, prefix, event, hook->userdata); } } int ecex_add_buffer_hook(ecex_t *ed, const char *name, ecex_buffer_hook_fn fn, void *userdata, ecex_hook_free_fn free_fn) { if (!ed || !name || !name[0] || !fn) return ECEX_ERR; char *name_copy = ecex_strdup(name); if (!name_copy) return ECEX_ERR; for (size_t i = 0; i < ed->buffer_hook_count; i++) { if (strcmp(ed->buffer_hooks[i].name, name) == 0) { ecex_buffer_hook_clear(&ed->buffer_hooks[i]); ed->buffer_hooks[i].name = name_copy; ed->buffer_hooks[i].fn = fn; ed->buffer_hooks[i].userdata = userdata; ed->buffer_hooks[i].free_fn = free_fn; return ECEX_OK; } } if (ECEX_GROW_ARRAY(ed->buffer_hooks, ed->buffer_hook_count, ed->buffer_hook_cap, ECEX_INITIAL_HOOK_CAP) != ECEX_OK) { free(name_copy); return ECEX_ERR; } ecex_buffer_hook_t *hook = &ed->buffer_hooks[ed->buffer_hook_count++]; memset(hook, 0, sizeof(*hook)); hook->name = name_copy; hook->fn = fn; hook->userdata = userdata; hook->free_fn = free_fn; return ECEX_OK; } int ecex_remove_buffer_hook(ecex_t *ed, const char *name) { if (!ed || !name) return ECEX_ERR; for (size_t i = 0; i < ed->buffer_hook_count; i++) { if (strcmp(ed->buffer_hooks[i].name, name) != 0) continue; ecex_buffer_hook_clear(&ed->buffer_hooks[i]); if (i + 1 < ed->buffer_hook_count) { memmove(&ed->buffer_hooks[i], &ed->buffer_hooks[i + 1], (ed->buffer_hook_count - i - 1) * sizeof(ed->buffer_hooks[i])); } ed->buffer_hook_count--; return ECEX_OK; } return ECEX_ERR; } void ecex_notify_buffer_hooks(ecex_t *ed, buffer_t *buffer, int event) { if (!ed || !buffer) return; for (size_t i = 0; i < ed->buffer_hook_count; i++) { ecex_buffer_hook_t *hook = &ed->buffer_hooks[i]; if (hook->fn) hook->fn(ed, buffer, event, hook->userdata); } } void ecex_message(ecex_t *ed, const char *message) { if (!ed) return; snprintf(ed->message, sizeof(ed->message), "%s", message ? message : ""); ed->message_revision++; ecex_mark_ui_changed(ed); } static int ecex_path_is_executable(const char *path) { return path && path[0] && access(path, X_OK) == 0; } int ecex_dependency_available(const char *program) { if (!program || !program[0]) return 0; if (strchr(program, '/')) return ecex_path_is_executable(program); const char *path = getenv("PATH"); if (!path || !path[0]) path = "/bin:/usr/bin:/usr/local/bin"; const char *start = path; while (*start) { const char *end = strchr(start, ':'); size_t dir_len = end ? (size_t)(end - start) : strlen(start); if (dir_len == 0) { start = end ? end + 1 : start + strlen(start); continue; } char candidate[4096]; size_t prog_len = strlen(program); if (dir_len + 1 + prog_len < sizeof(candidate)) { memcpy(candidate, start, dir_len); candidate[dir_len] = '/'; memcpy(candidate + dir_len + 1, program, prog_len + 1); if (ecex_path_is_executable(candidate)) return 1; } if (!end) break; start = end + 1; } return 0; } int ecex_plugin_require_dependency(ecex_t *ed, const char *plugin_id, const char *program) { if (!program || !program[0]) return ECEX_ERR; if (ecex_dependency_available(program)) return ECEX_OK; char message[256]; snprintf(message, sizeof(message), "%s disabled: missing dependency '%s'", plugin_id && plugin_id[0] ? plugin_id : "plugin", program); ecex_message(ed, message); fprintf(stderr, "ecex: %s\n", message); return ECEX_ERR; } int ecex_add_completion_provider(ecex_t *ed, const char *name, const char *mode_name, ecex_completion_provider_fn fn, void *userdata, ecex_hook_free_fn free_fn) { if (!ed || !name || !name[0] || !fn) return ECEX_ERR; int mode = 0; if (mode_name && mode_name[0]) { mode = ecex_define_major_mode(ed, mode_name); if (mode == 0) return ECEX_ERR; } char *name_copy = ecex_strdup(name); if (!name_copy) return ECEX_ERR; for (size_t i = 0; i < ed->completion_provider_count; i++) { if (strcmp(ed->completion_providers[i].name, name) == 0) { ecex_completion_provider_clear(&ed->completion_providers[i]); ed->completion_providers[i].name = name_copy; ed->completion_providers[i].mode = mode; ed->completion_providers[i].fn = fn; ed->completion_providers[i].userdata = userdata; ed->completion_providers[i].free_fn = free_fn; return ECEX_OK; } } if (ECEX_GROW_ARRAY(ed->completion_providers, ed->completion_provider_count, ed->completion_provider_cap, ECEX_INITIAL_HOOK_CAP) != ECEX_OK) { free(name_copy); return ECEX_ERR; } ecex_completion_provider_t *provider = &ed->completion_providers[ed->completion_provider_count++]; memset(provider, 0, sizeof(*provider)); provider->name = name_copy; provider->mode = mode; provider->fn = fn; provider->userdata = userdata; provider->free_fn = free_fn; return ECEX_OK; } static int ecex_completion_provider_mode(ecex_t *ed, const char *mode_name, int *out_mode) { if (!ed || !out_mode) return ECEX_ERR; *out_mode = 0; if (mode_name && mode_name[0]) { *out_mode = ecex_define_major_mode(ed, mode_name); if (*out_mode == 0) return ECEX_ERR; } return ECEX_OK; } static char **ecex_completion_words_copy(const char *const *words, size_t count) { if (!words || count == 0) return NULL; char **copy = calloc(count, sizeof(*copy)); if (!copy) return NULL; for (size_t i = 0; i < count; i++) { copy[i] = ecex_strdup(words[i] ? words[i] : ""); if (!copy[i]) { for (size_t j = 0; j < i; j++) free(copy[j]); free(copy); return NULL; } } return copy; } int ecex_define_word_completion_provider(ecex_t *ed, const char *name, const char *mode_name, int flags) { if (!ed || !name || !name[0]) return ECEX_ERR; int mode = 0; if (ecex_completion_provider_mode(ed, mode_name, &mode) != ECEX_OK) return ECEX_ERR; char *name_copy = ecex_strdup(name); if (!name_copy) return ECEX_ERR; for (size_t i = 0; i < ed->completion_provider_count; i++) { if (strcmp(ed->completion_providers[i].name, name) == 0) { ecex_completion_provider_clear(&ed->completion_providers[i]); ed->completion_providers[i].name = name_copy; ed->completion_providers[i].mode = mode; ed->completion_providers[i].flags = flags; return ECEX_OK; } } if (ECEX_GROW_ARRAY(ed->completion_providers, ed->completion_provider_count, ed->completion_provider_cap, ECEX_INITIAL_HOOK_CAP) != ECEX_OK) { free(name_copy); return ECEX_ERR; } ecex_completion_provider_t *provider = &ed->completion_providers[ed->completion_provider_count++]; memset(provider, 0, sizeof(*provider)); provider->name = name_copy; provider->mode = mode; provider->flags = flags; return ECEX_OK; } int ecex_completion_provider_add_word_detail(ecex_t *ed, const char *name, const char *word, const char *detail) { if (!ed || !name || !word || !word[0]) return ECEX_ERR; for (size_t i = 0; i < ed->completion_provider_count; i++) { ecex_completion_provider_t *provider = &ed->completion_providers[i]; if (strcmp(provider->name, name) != 0) continue; char *word_copy = ecex_strdup(word); if (!word_copy) return ECEX_ERR; char *detail_copy = NULL; if (detail && detail[0]) { detail_copy = ecex_strdup(detail); if (!detail_copy) { free(word_copy); return ECEX_ERR; } } char **grown = realloc(provider->words, (provider->word_count + 1) * sizeof(*provider->words)); if (!grown) { free(word_copy); free(detail_copy); return ECEX_ERR; } provider->words = grown; char **grown_details = NULL; if (provider->word_details) { grown_details = realloc(provider->word_details, (provider->word_count + 1) * sizeof(*provider->word_details)); } else { grown_details = calloc(provider->word_count + 1, sizeof(*provider->word_details)); } if (!grown_details) { free(word_copy); free(detail_copy); return ECEX_ERR; } provider->word_details = grown_details; provider->words[provider->word_count] = word_copy; provider->word_details[provider->word_count] = detail_copy; provider->word_count++; return ECEX_OK; } return ECEX_ERR; } int ecex_completion_provider_add_word(ecex_t *ed, const char *name, const char *word) { return ecex_completion_provider_add_word_detail(ed, name, word, NULL); } int ecex_completion_provider_add_words(ecex_t *ed, const char *name, const char *words) { if (!ed || !name || !words) return ECEX_ERR; const char *p = words; while (*p) { while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; if (!*p) break; char word[256]; size_t len = 0; while (*p && *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') { if (len + 1 < sizeof(word)) word[len++] = *p; p++; } word[len] = '\0'; if (len > 0 && ecex_completion_provider_add_word(ed, name, word) != ECEX_OK) { return ECEX_ERR; } } return ECEX_OK; } int ecex_completion_provider_add_entries(ecex_t *ed, const char *name, const char *entries) { if (!ed || !name || !entries) return ECEX_ERR; const char *p = entries; while (*p) { while (*p == '\n' || *p == '\r') p++; if (!*p) break; char word[256]; char detail[512]; size_t word_len = 0; size_t detail_len = 0; while (*p && *p != '\t' && *p != '\n' && *p != '\r') { if (word_len + 1 < sizeof(word)) word[word_len++] = *p; p++; } word[word_len] = '\0'; if (*p == '\t') { p++; while (*p && *p != '\n' && *p != '\r') { if (detail_len + 1 < sizeof(detail)) detail[detail_len++] = *p; p++; } } detail[detail_len] = '\0'; while (*p == '\n' || *p == '\r') p++; if (word_len > 0 && ecex_completion_provider_add_word_detail(ed, name, word, detail_len > 0 ? detail : NULL) != ECEX_OK) { return ECEX_ERR; } } return ECEX_OK; } int ecex_completion_provider_set_detail(ecex_t *ed, const char *name, const char *detail) { if (!ed || !name || !name[0]) return ECEX_ERR; for (size_t i = 0; i < ed->completion_provider_count; i++) { ecex_completion_provider_t *provider = &ed->completion_providers[i]; if (strcmp(provider->name, name) != 0) continue; char *copy = NULL; if (detail && detail[0]) { copy = ecex_strdup(detail); if (!copy) return ECEX_ERR; } free(provider->detail); provider->detail = copy; return ECEX_OK; } return ECEX_ERR; } int ecex_add_word_completion_provider(ecex_t *ed, const char *name, const char *mode_name, const char *const *words, size_t word_count, int flags) { if (!ed || !name || !name[0] || !words || word_count == 0) return ECEX_ERR; int mode = 0; if (ecex_completion_provider_mode(ed, mode_name, &mode) != ECEX_OK) return ECEX_ERR; char *name_copy = ecex_strdup(name); if (!name_copy) return ECEX_ERR; char **words_copy = ecex_completion_words_copy(words, word_count); if (!words_copy) { free(name_copy); return ECEX_ERR; } for (size_t i = 0; i < ed->completion_provider_count; i++) { if (strcmp(ed->completion_providers[i].name, name) == 0) { ecex_completion_provider_clear(&ed->completion_providers[i]); ed->completion_providers[i].name = name_copy; ed->completion_providers[i].mode = mode; ed->completion_providers[i].words = words_copy; ed->completion_providers[i].word_count = word_count; ed->completion_providers[i].flags = flags; return ECEX_OK; } } if (ECEX_GROW_ARRAY(ed->completion_providers, ed->completion_provider_count, ed->completion_provider_cap, ECEX_INITIAL_HOOK_CAP) != ECEX_OK) { for (size_t i = 0; i < word_count; i++) free(words_copy[i]); free(words_copy); free(name_copy); return ECEX_ERR; } ecex_completion_provider_t *provider = &ed->completion_providers[ed->completion_provider_count++]; memset(provider, 0, sizeof(*provider)); provider->name = name_copy; provider->mode = mode; provider->words = words_copy; provider->word_count = word_count; provider->flags = flags; return ECEX_OK; } int ecex_remove_completion_provider(ecex_t *ed, const char *name) { if (!ed || !name) return ECEX_ERR; for (size_t i = 0; i < ed->completion_provider_count; i++) { if (strcmp(ed->completion_providers[i].name, name) != 0) continue; ecex_completion_provider_clear(&ed->completion_providers[i]); if (i + 1 < ed->completion_provider_count) { memmove(&ed->completion_providers[i], &ed->completion_providers[i + 1], (ed->completion_provider_count - i - 1) * sizeof(ed->completion_providers[i])); } ed->completion_provider_count--; return ECEX_OK; } return ECEX_ERR; } void ecex_set_clipboard_callbacks(ecex_t *ed, ecex_clipboard_get_fn get_fn, ecex_clipboard_set_fn set_fn, void *userdata) { if (!ed) return; ed->clipboard_get = get_fn; ed->clipboard_set = set_fn; ed->clipboard_userdata = userdata; } const char *ecex_clipboard_get(ecex_t *ed) { if (!ed) return NULL; if (ed->clipboard_get) { const char *external = ed->clipboard_get(ed->clipboard_userdata); if (external) { if (!ed->clipboard_text || strcmp(ed->clipboard_text, external) != 0) { char *copy = ecex_strdup(external); if (copy) { free(ed->clipboard_text); ed->clipboard_text = copy; } } } } return ed->clipboard_text; } int ecex_clipboard_set(ecex_t *ed, const char *text) { if (!ed || !text) return ECEX_ERR; char *copy = ecex_strdup(text); if (!copy) return ECEX_ERR; free(ed->clipboard_text); ed->clipboard_text = copy; if (ed->clipboard_set) { ed->clipboard_set(ed->clipboard_userdata, text); } return ECEX_OK; } static int ecex_command_exists(ecex_t *ed, const char *name) { if (!ed || !name) return 0; for (size_t i = 0; i < ed->command_count; i++) { if (ed->commands[i].name && strcmp(ed->commands[i].name, name) == 0) return 1; } return 0; } static int ecex_key_is_prefix_of(const char *prefix, const char *key) { if (!prefix || !key) return 0; size_t n = strlen(prefix); return strncmp(prefix, key, n) == 0 && key[n] == ' '; } static void ecex_warn_keybind_issue(const char *scope, const char *key, const char *detail) { if (scope && scope[0]) { fprintf(stderr, "ecex: keybind warning [%s]: %s%s%s\n", scope, key ? key : "", detail && detail[0] ? " " : "", detail ? detail : ""); } else { fprintf(stderr, "ecex: keybind warning: %s%s%s\n", key ? key : "", detail && detail[0] ? " " : "", detail ? detail : ""); } } static void ecex_warn_keybind_conflicts(ecex_t *ed, const char *scope, const char *key, int mode) { if (!ed || !key) return; for (size_t i = 0; i < ed->keybind_count; i++) { if (mode != 0) break; const char *existing = ed->keybinds[i].key; if (existing && ecex_key_is_prefix_of(key, existing)) ecex_warn_keybind_issue(scope, key, "is a prefix of an existing binding"); if (existing && ecex_key_is_prefix_of(existing, key)) ecex_warn_keybind_issue(scope, key, "extends an existing complete binding"); } for (size_t i = 0; i < ed->mode_keybind_count; i++) { if (mode != ed->mode_keybinds[i].mode) continue; const char *existing = ed->mode_keybinds[i].key; if (existing && ecex_key_is_prefix_of(key, existing)) ecex_warn_keybind_issue(scope, key, "is a prefix of an existing mode binding"); if (existing && ecex_key_is_prefix_of(existing, key)) ecex_warn_keybind_issue(scope, key, "extends an existing complete mode binding"); } } int ecex_bind_key(ecex_t *ed, const char *key, const char *command) { if (!ed || !key || !command) return ECEX_ERR; if (!ecex_command_exists(ed, command)) { ecex_warn_keybind_issue("global", key, "targets a command that is not registered yet"); } ecex_warn_keybind_conflicts(ed, "global", key, 0); for (size_t i = 0; i < ed->keybind_count; i++) { if (strcmp(ed->keybinds[i].key, key) == 0) { fprintf(stderr, "ecex: keybind warning [global]: replacing %s from %s to %s\n", key, ed->keybinds[i].command, command); char *new_command = ecex_strdup(command); if (!new_command) return ECEX_ERR; free(ed->keybinds[i].command); ed->keybinds[i].command = new_command; return ECEX_OK; } } if (ECEX_GROW_ARRAY(ed->keybinds, ed->keybind_count, ed->keybind_cap, ECEX_INITIAL_KEYBIND_CAP) != ECEX_OK) { return ECEX_ERR; } char *key_copy = ecex_strdup(key); char *command_copy = ecex_strdup(command); if (!key_copy || !command_copy) { free(key_copy); free(command_copy); return ECEX_ERR; } ed->keybinds[ed->keybind_count].key = key_copy; ed->keybinds[ed->keybind_count].command = command_copy; ed->keybind_count++; return ECEX_OK; } const char *ecex_lookup_key(ecex_t *ed, const char *key) { if (!ed || !key) return NULL; for (size_t i = 0; i < ed->keybind_count; i++) { if (strcmp(ed->keybinds[i].key, key) == 0) { return ed->keybinds[i].command; } } return NULL; } int ecex_define_major_mode(ecex_t *ed, const char *name) { if (!ed || !name || !name[0]) return 0; for (size_t i = 0; i < ed->major_mode_count; i++) { if (strcmp(ed->major_modes[i].name, name) == 0) { return ed->major_modes[i].id; } } if (ECEX_GROW_ARRAY(ed->major_modes, ed->major_mode_count, ed->major_mode_cap, ECEX_INITIAL_MODE_CAP) != ECEX_OK) { return 0; } char *copy = ecex_strdup(name); if (!copy) return 0; int id = ed->next_major_mode_id++; ed->major_modes[ed->major_mode_count].id = id; ed->major_modes[ed->major_mode_count].name = copy; ed->major_mode_count++; return id; } int ecex_major_mode_by_name(ecex_t *ed, const char *name) { if (!ed || !name) return 0; for (size_t i = 0; i < ed->major_mode_count; i++) { if (strcmp(ed->major_modes[i].name, name) == 0) return ed->major_modes[i].id; } return 0; } const char *ecex_major_mode_name(ecex_t *ed, int mode) { if (!ed || mode == 0) return "fundamental-mode"; for (size_t i = 0; i < ed->major_mode_count; i++) { if (ed->major_modes[i].id == mode) return ed->major_modes[i].name; } return "unknown-mode"; } int ecex_buffer_set_major_mode(buffer_t *buffer, int mode) { if (!buffer) return ECEX_ERR; buffer->major_mode = mode; return ECEX_OK; } int ecex_buffer_set_major_mode_by_name(ecex_t *ed, buffer_t *buffer, const char *name) { if (!ed || !buffer || !name) return ECEX_ERR; int mode = ecex_define_major_mode(ed, name); if (!mode) return ECEX_ERR; int old_mode = buffer->major_mode; int result = ecex_buffer_set_major_mode(buffer, mode); if (result == ECEX_OK && old_mode != mode) { ecex_notify_buffer_hooks(ed, buffer, ECEX_BUFFER_HOOK_MODE_CHANGE); } return result; } const char *ecex_buffer_major_mode_name(ecex_t *ed, buffer_t *buffer) { if (!buffer) return "fundamental-mode"; return ecex_major_mode_name(ed, buffer->major_mode); } int ecex_bind_mode_key(ecex_t *ed, const char *mode_name, const char *key, const char *command) { if (!ed || !mode_name || !key || !command) return ECEX_ERR; int mode = ecex_define_major_mode(ed, mode_name); if (!mode) return ECEX_ERR; if (!ecex_command_exists(ed, command)) { ecex_warn_keybind_issue(mode_name, key, "targets a command that is not registered yet"); } ecex_warn_keybind_conflicts(ed, mode_name, key, mode); for (size_t i = 0; i < ed->mode_keybind_count; i++) { if (ed->mode_keybinds[i].mode == mode && strcmp(ed->mode_keybinds[i].key, key) == 0) { fprintf(stderr, "ecex: keybind warning [%s]: replacing %s from %s to %s\n", mode_name, key, ed->mode_keybinds[i].command, command); char *new_command = ecex_strdup(command); if (!new_command) return ECEX_ERR; free(ed->mode_keybinds[i].command); ed->mode_keybinds[i].command = new_command; return ECEX_OK; } } if (ECEX_GROW_ARRAY(ed->mode_keybinds, ed->mode_keybind_count, ed->mode_keybind_cap, ECEX_INITIAL_MODE_KEYBIND_CAP) != ECEX_OK) { return ECEX_ERR; } char *key_copy = ecex_strdup(key); char *command_copy = ecex_strdup(command); if (!key_copy || !command_copy) { free(key_copy); free(command_copy); return ECEX_ERR; } ed->mode_keybinds[ed->mode_keybind_count].mode = mode; ed->mode_keybinds[ed->mode_keybind_count].key = key_copy; ed->mode_keybinds[ed->mode_keybind_count].command = command_copy; ed->mode_keybind_count++; return ECEX_OK; } int ecex_validate_bindings(ecex_t *ed) { if (!ed) return ECEX_ERR; int ok = ECEX_OK; for (size_t i = 0; i < ed->keybind_count; i++) { const char *command = ed->keybinds[i].command; if (!ecex_command_exists(ed, command)) { ecex_warn_keybind_issue("global", ed->keybinds[i].key, "targets a missing command"); ok = ECEX_ERR; } for (size_t j = i + 1; j < ed->keybind_count; j++) { if (ecex_key_is_prefix_of(ed->keybinds[i].key, ed->keybinds[j].key) || ecex_key_is_prefix_of(ed->keybinds[j].key, ed->keybinds[i].key)) { ecex_warn_keybind_issue("global", ed->keybinds[i].key, "has a prefix conflict"); } } } for (size_t i = 0; i < ed->mode_keybind_count; i++) { const char *command = ed->mode_keybinds[i].command; const char *mode = ecex_major_mode_name(ed, ed->mode_keybinds[i].mode); if (!ecex_command_exists(ed, command)) { ecex_warn_keybind_issue(mode, ed->mode_keybinds[i].key, "targets a missing command"); ok = ECEX_ERR; } for (size_t j = i + 1; j < ed->mode_keybind_count; j++) { if (ed->mode_keybinds[i].mode != ed->mode_keybinds[j].mode) continue; if (ecex_key_is_prefix_of(ed->mode_keybinds[i].key, ed->mode_keybinds[j].key) || ecex_key_is_prefix_of(ed->mode_keybinds[j].key, ed->mode_keybinds[i].key)) { ecex_warn_keybind_issue(mode, ed->mode_keybinds[i].key, "has a mode prefix conflict"); } } } return ok; } const char *ecex_lookup_key_for_buffer(ecex_t *ed, buffer_t *buffer, const char *key) { if (!ed || !key) return NULL; int mode = buffer ? buffer->major_mode : 0; if (mode) { for (size_t i = 0; i < ed->mode_keybind_count; i++) { if (ed->mode_keybinds[i].mode == mode && strcmp(ed->mode_keybinds[i].key, key) == 0) { return ed->mode_keybinds[i].command; } } } return ecex_lookup_key(ed, key); } int ecex_key_sequence_has_prefix_for_buffer(ecex_t *ed, buffer_t *buffer, const char *prefix) { if (!ed || !prefix) return 0; size_t prefix_len = strlen(prefix); int mode = buffer ? buffer->major_mode : 0; if (mode) { for (size_t i = 0; i < ed->mode_keybind_count; i++) { const char *key = ed->mode_keybinds[i].key; if (ed->mode_keybinds[i].mode == mode && strncmp(key, prefix, prefix_len) == 0 && key[prefix_len] == ' ') { return 1; } } } for (size_t i = 0; i < ed->keybind_count; i++) { const char *key = ed->keybinds[i].key; if (strncmp(key, prefix, prefix_len) == 0 && key[prefix_len] == ' ') return 1; } return 0; } typedef struct ecex_key_prefix_item { char key[64]; char command[128]; int has_children; } ecex_key_prefix_item_t; static int ecex_key_prefix_item_exists(ecex_key_prefix_item_t *items, size_t count, const char *key) { for (size_t i = 0; i < count; i++) { if (strcmp(items[i].key, key) == 0) return 1; } return 0; } static void ecex_key_prefix_collect_binding(ecex_key_prefix_item_t *items, size_t *count, size_t cap, const char *prefix, const char *binding, const char *command) { if (!items || !count || *count >= cap || !prefix || !binding || !command) return; size_t prefix_len = strlen(prefix); if (strncmp(binding, prefix, prefix_len) != 0 || binding[prefix_len] != ' ') return; const char *rest = binding + prefix_len + 1; if (!rest[0]) return; const char *space = strchr(rest, ' '); size_t key_len = space ? (size_t)(space - rest) : strlen(rest); if (key_len == 0) return; char key[64]; if (key_len >= sizeof(key)) key_len = sizeof(key) - 1; memcpy(key, rest, key_len); key[key_len] = '\0'; if (ecex_key_prefix_item_exists(items, *count, key)) return; ecex_key_prefix_item_t *item = &items[(*count)++]; memset(item, 0, sizeof(*item)); snprintf(item->key, sizeof(item->key), "%s", key); item->has_children = space ? 1 : 0; snprintf(item->command, sizeof(item->command), "%s", item->has_children ? "prefix" : command); } int ecex_describe_key_prefix(ecex_t *ed, buffer_t *buffer, const char *prefix, char *out, size_t out_size, size_t max_items) { if (out && out_size) out[0] = '\0'; if (!ed || !prefix || !prefix[0] || !out || out_size == 0) return ECEX_ERR; ecex_key_prefix_item_t items[64]; size_t count = 0; int mode = buffer ? buffer->major_mode : 0; if (mode) { for (size_t i = 0; i < ed->mode_keybind_count; i++) { if (ed->mode_keybinds[i].mode != mode) continue; ecex_key_prefix_collect_binding(items, &count, ECEX_ARRAY_LEN(items), prefix, ed->mode_keybinds[i].key, ed->mode_keybinds[i].command); } } for (size_t i = 0; i < ed->keybind_count; i++) { ecex_key_prefix_collect_binding(items, &count, ECEX_ARRAY_LEN(items), prefix, ed->keybinds[i].key, ed->keybinds[i].command); } if (count == 0) { snprintf(out, out_size, "%s: no continuations", prefix); return 0; } if (max_items == 0 || max_items > count) max_items = count; size_t used = (size_t)snprintf(out, out_size, "%s: ", prefix); if (used >= out_size) { out[out_size - 1] = '\0'; return (int)count; } size_t items_per_row = max_items > 8 ? 4 : max_items + 1; for (size_t i = 0; i < max_items; i++) { const char *sep = ""; if (i > 0) sep = (i % items_per_row) == 0 ? "\n " : " | "; int n = snprintf(out + used, out_size - used, "%s%s %s", sep, items[i].key, items[i].command); if (n < 0) break; if ((size_t)n >= out_size - used) { used = out_size - 1; out[used] = '\0'; break; } used += (size_t)n; } if (max_items < count && used + 16 < out_size) { snprintf(out + used, out_size - used, " | +%zu more", count - max_items); } return (int)count; } int ecex_auto_set_major_mode(ecex_t *ed, buffer_t *buffer) { if (!ed || !buffer) return ECEX_ERR; const char *name = "fundamental-mode"; if (buffer_is_interactive(buffer) && buffer->name && buffer->name[0] == '*') { name = "special-mode"; } return ecex_buffer_set_major_mode_by_name(ed, buffer, name); } int ecex_list_commands(ecex_t *ed) { if (!ed) return ECEX_ERR; buffer_t *buf = ecex_find_buffer(ed, "*commands*"); if (!buf) { buf = ecex_create_buffer(ed, "*commands*", NULL, 0); if (!buf) return ECEX_ERR; } if (buffer_clear(buf) != ECEX_OK) return ECEX_ERR; buffer_append(buf, "Commands:\n\n"); for (size_t i = 0; i < ed->command_count; i++) { const char *command_name = ed->commands[i].name; int first_key = 1; buffer_append(buf, " "); buffer_append(buf, command_name); for (size_t j = 0; j < ed->keybind_count; j++) { if (strcmp(ed->keybinds[j].command, command_name) == 0) { buffer_append(buf, first_key ? " [" : ", "); buffer_append(buf, ed->keybinds[j].key); first_key = 0; } } for (size_t j = 0; j < ed->mode_keybind_count; j++) { if (strcmp(ed->mode_keybinds[j].command, command_name) == 0) { buffer_append(buf, first_key ? " [" : ", "); buffer_append(buf, ecex_major_mode_name(ed, ed->mode_keybinds[j].mode)); buffer_append(buf, ":"); buffer_append(buf, ed->mode_keybinds[j].key); first_key = 0; } } if (!first_key) buffer_append(buf, "]"); buffer_append(buf, "\n"); } buf->point = 0; buf->modified = 0; return ecex_switch_buffer(ed, "*commands*"); } static int ecex_interactive_switch_buffer_action(ecex_t *ed, buffer_t *buffer, size_t line, const char *payload, void *userdata) { (void)buffer; (void)line; (void)userdata; if (!ed || !payload) return ECEX_ERR; return ecex_switch_buffer(ed, payload); } int ecex_list_buffers(ecex_t *ed) { if (!ed) return ECEX_ERR; buffer_t *buf = ecex_find_buffer(ed, "*buffers*"); if (!buf) { buf = ecex_create_interactive_buffer(ed, "*buffers*"); if (!buf) return ECEX_ERR; } buffer_set_interactive(buf, 1); if (buffer_clear(buf) != ECEX_OK) return ECEX_ERR; buffer_set_interactive(buf, 1); buffer_append(buf, "Buffers:\n"); buffer_append(buf, "Press ENTER on a buffer to switch to it.\n\n"); for (size_t i = 0; i < ed->buffer_count; i++) { buffer_t *b = ed->buffers[i]; char line[1024]; snprintf(line, sizeof(line), "%s %s %-24s %-18s size:%zu%s%s", b == ed->current_buffer ? "*" : " ", b->modified ? "+" : " ", b->name ? b->name : "(unnamed)", ecex_buffer_major_mode_name(ed, b), b->len, b->path ? " " : "", b->path ? b->path : ""); if (ecex_interactive_append_line(ed, buf, line, ecex_interactive_switch_buffer_action, b->name, NULL) != ECEX_OK) { return ECEX_ERR; } } buf->point = 0; buf->modified = 0; return ecex_switch_buffer(ed, "*buffers*"); } buffer_t *ecex_create_interactive_buffer(ecex_t *ed, const char *name) { if (!ed || !name) return NULL; buffer_t *buffer = ecex_find_buffer(ed, name); if (!buffer) { buffer = ecex_create_buffer(ed, name, NULL, 0); if (!buffer) return NULL; } buffer_set_interactive(buffer, 1); if (buffer->major_mode == 0) ecex_buffer_set_major_mode_by_name(ed, buffer, "special-mode"); return buffer; } int ecex_interactive_append_line(ecex_t *ed, buffer_t *buffer, const char *text, ecex_interactive_line_fn fn, const char *payload, void *userdata) { (void)ed; if (!buffer || !text) return ECEX_ERR; size_t line = buffer_line_count(buffer); if (line > 0) line--; if (fn) { if (buffer_add_interactive_action(buffer, line, fn, payload, userdata) != ECEX_OK) { return ECEX_ERR; } } if (buffer_append(buffer, text) != ECEX_OK) return ECEX_ERR; if (buffer_append(buffer, "\n") != ECEX_OK) return ECEX_ERR; buffer_set_interactive(buffer, 1); return ECEX_OK; } int ecex_interactive_activate_current_line(ecex_t *ed) { if (!ed) return ECEX_ERR; buffer_t *buffer = ecex_current_buffer(ed); if (!buffer || !buffer_is_interactive(buffer)) return ECEX_ERR; size_t line = buffer_current_line_number(buffer); if (line > 0) line--; ecex_interactive_line_action_t *action = buffer_interactive_action_at_line(buffer, line); if (!action || !action->fn) return ECEX_ERR; return action->fn(ed, buffer, line, action->payload, action->userdata); } static int ecex_buffer_path_equal(buffer_t *buffer, const char *path) { return buffer && buffer->path && path && strcmp(buffer->path, path) == 0; } int ecex_find_file(ecex_t *ed, const char *path) { if (!ed || !path || !path[0]) return ECEX_ERR; char *normal_path = ecex_path_normalize(path); if (!normal_path) return ECEX_ERR; if (ecex_path_is_dir(normal_path)) { int result = ecex_file_browser_populate(ed, normal_path, 1); free(normal_path); return result; } if (ecex_path_is_file(normal_path) && ecex_path_is_media(normal_path)) { int result = ecex_media_open(ed, normal_path); free(normal_path); return result; } for (size_t i = 0; i < ed->buffer_count; i++) { if (ecex_buffer_path_equal(ed->buffers[i], normal_path)) { int result; free(normal_path); result = ecex_set_current_buffer_index(ed, i); if (result == ECEX_OK && !ecex_buffer_has_renderer(ed->buffers[i])) ecex_run_plugin_file_handlers(ed, ed->buffers[i]); return result; } } char *name = ecex_path_basename_dup(normal_path); if (!name) { free(normal_path); return ECEX_ERR; } buffer_t *buf = ecex_create_buffer(ed, name, NULL, 0); free(name); if (!buf) { free(normal_path); return ECEX_ERR; } if (ecex_file_exists(normal_path)) { if (buffer_load_file(buf, normal_path) != ECEX_OK) { ecex_kill_buffer_force(ed, buf->name); free(normal_path); return ECEX_ERR; } } else { free(buf->path); buf->path = normal_path; normal_path = NULL; buf->modified = 0; } free(normal_path); ecex_auto_set_major_mode(ed, buf); int switch_result = ecex_set_current_buffer_index(ed, ed->buffer_count ? ed->buffer_count - 1 : 0); if (switch_result == ECEX_OK) ecex_run_plugin_file_handlers(ed, buf); return switch_result; } int ecex_save_current_buffer(ecex_t *ed) { buffer_t *buf = ecex_current_buffer(ed); if (!buf) return ECEX_ERR; int result = buffer_save(buf); if (result == ECEX_OK) ecex_notify_buffer_hooks(ed, buf, ECEX_BUFFER_HOOK_SAVE); return result; } int ecex_write_current_buffer(ecex_t *ed, const char *path) { buffer_t *buf = ecex_current_buffer(ed); if (!buf || !path || !path[0]) return ECEX_ERR; char *normal_path = ecex_path_normalize(path); if (!normal_path) return ECEX_ERR; int result = buffer_save_as(buf, normal_path); free(normal_path); if (result == ECEX_OK) { ecex_notify_buffer_hooks(ed, buf, ECEX_BUFFER_HOOK_SAVE); ecex_auto_set_major_mode(ed, buf); } return result; } void ecex_request_prompt(ecex_t *ed, ecex_prompt_request_t request, const char *message) { if (!ed) return; ed->prompt_request = request; snprintf(ed->prompt_message, sizeof(ed->prompt_message), "%s", message ? message : ""); } void ecex_clear_prompt_request(ecex_t *ed) { if (!ed) return; ed->prompt_request = ECEX_PROMPT_NONE; ed->prompt_message[0] = '\0'; } int ecex_indent_line_to(buffer_t *buffer, int target_cols) { if (!buffer || buffer->read_only || buffer_is_interactive(buffer)) return ECEX_ERR; if (target_cols < 0) target_cols = 0; if (target_cols > 240) target_cols = 240; size_t line_start = buffer_current_line_start(buffer); size_t line_end = buffer_current_line_end(buffer); size_t first = line_start; while (first < line_end && (buffer->data[first] == ' ' || buffer->data[first] == '\t')) first++; size_t point = buffer->point; size_t point_after_indent = point > first ? point - first : 0; char spaces[256]; memset(spaces, ' ', (size_t)target_cols); spaces[target_cols] = '\0'; if (buffer_delete_range(buffer, line_start, first) != ECEX_OK) return ECEX_ERR; if (target_cols > 0 && buffer_insert_at(buffer, line_start, spaces) != ECEX_OK) return ECEX_ERR; size_t new_first = line_start + (size_t)target_cols; if (point <= first) { buffer_set_point(buffer, new_first); } else { buffer_set_point(buffer, new_first + point_after_indent); } return ECEX_OK; } static int ecex_indent_for_tab(ecex_t *ed) { if (!ed) return ECEX_ERR; buffer_t *buffer = ecex_current_buffer(ed); if (!buffer || buffer->read_only || buffer_is_interactive(buffer)) return ECEX_ERR; size_t col = buffer_current_column(buffer); size_t spaces = 4 - (col % 4); if (spaces == 0) spaces = 4; char text[5]; memset(text, ' ', spaces); text[spaces] = '\0'; return buffer_insert(buffer, text); } static int ecex_fuzzy_score(const char *candidate, const char *query) { if (!candidate || !query) return -1; if (query[0] == '\0') return 0; int score = 0; int consecutive = 0; int last_match = -1; size_t ci = 0; size_t qi = 0; while (candidate[ci] && query[qi]) { char c = candidate[ci]; char q = query[qi]; if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a'); if (q >= 'A' && q <= 'Z') q = (char)(q - 'A' + 'a'); if (c == q) { score += 10; if ((int)ci == last_match + 1) { consecutive++; score += 5 * consecutive; } else { consecutive = 0; } if (ci == 0) score += 20; if (ci > 0 && (candidate[ci - 1] == '-' || candidate[ci - 1] == '_' || candidate[ci - 1] == ' ')) { score += 15; } last_match = (int)ci; qi++; } ci++; } if (query[qi] != '\0') return -1; score -= (int)strlen(candidate); if (strncmp(candidate, query, strlen(query)) == 0) score += 100; return score; } #define ECEX_COMPLETION_MAX_CANDIDATES 96 typedef struct ecex_completion_candidate { char text[256]; char detail[256]; int score; } ecex_completion_candidate_t; static void ecex_completion_cycle_reset(ecex_t *ed) { if (!ed) return; ed->completion_cycle_active = 0; ed->completion_cycle_buffer = NULL; ed->completion_cycle_start = 0; ed->completion_cycle_index = 0; ed->completion_cycle_prefix[0] = '\0'; ed->completion_cycle_current[0] = '\0'; } static int ecex_completion_candidates_add(ecex_completion_candidate_t *items, size_t cap, size_t *count, const char *text, const char *detail, int score) { if (!items || !count || *count >= cap || !text || !text[0] || score < 0) return ECEX_ERR; for (size_t i = 0; i < *count; i++) { if (strcmp(items[i].text, text) == 0) { if (score > items[i].score) items[i].score = score; if ((!items[i].detail[0]) && detail && detail[0]) { snprintf(items[i].detail, sizeof(items[i].detail), "%s", detail); } return ECEX_OK; } } ecex_completion_candidate_t *item = &items[(*count)++]; memset(item, 0, sizeof(*item)); snprintf(item->text, sizeof(item->text), "%s", text); if (detail && detail[0]) snprintf(item->detail, sizeof(item->detail), "%s", detail); item->score = score; return ECEX_OK; } static int ecex_completion_candidate_compare(const void *a, const void *b) { const ecex_completion_candidate_t *ca = (const ecex_completion_candidate_t *)a; const ecex_completion_candidate_t *cb = (const ecex_completion_candidate_t *)b; if (ca->score != cb->score) return cb->score - ca->score; size_t la = strlen(ca->text); size_t lb = strlen(cb->text); if (la != lb) return la < lb ? -1 : 1; return strcmp(ca->text, cb->text); } #define ECEX_CLANGD_TIMEOUT_MS 4500 #define ECEX_CLANGD_MAX_MESSAGES 96 typedef struct ecex_lsp_process { pid_t pid; int in_fd; int out_fd; } ecex_lsp_process_t; static char *ecex_lsp_format(const char *fmt, ...) { va_list ap; va_start(ap, fmt); int needed = vsnprintf(NULL, 0, fmt, ap); va_end(ap); if (needed < 0) return NULL; char *out = malloc((size_t)needed + 1); if (!out) return NULL; va_start(ap, fmt); vsnprintf(out, (size_t)needed + 1, fmt, ap); va_end(ap); return out; } static int ecex_lsp_write_all(int fd, const char *data, size_t len) { size_t written = 0; while (written < len) { ssize_t n = write(fd, data + written, len - written); if (n < 0 && errno == EINTR) continue; if (n <= 0) return ECEX_ERR; written += (size_t)n; } return ECEX_OK; } static int ecex_lsp_send(int fd, const char *json) { if (!json) return ECEX_ERR; char header[80]; int n = snprintf(header, sizeof(header), "Content-Length: %zu\r\n\r\n", strlen(json)); if (n <= 0 || (size_t)n >= sizeof(header)) return ECEX_ERR; if (ecex_lsp_write_all(fd, header, (size_t)n) != ECEX_OK) return ECEX_ERR; return ecex_lsp_write_all(fd, json, strlen(json)); } static int ecex_lsp_wait_readable(int fd, int timeout_ms) { struct pollfd pfd; pfd.fd = fd; pfd.events = POLLIN; pfd.revents = 0; for (;;) { int rc = poll(&pfd, 1, timeout_ms); if (rc < 0 && errno == EINTR) continue; if (rc <= 0) return ECEX_ERR; if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) return ECEX_ERR; if (pfd.revents & POLLIN) return ECEX_OK; } } static int ecex_lsp_read_byte(int fd, char *out, int timeout_ms) { if (!out) return ECEX_ERR; if (ecex_lsp_wait_readable(fd, timeout_ms) != ECEX_OK) return ECEX_ERR; for (;;) { ssize_t n = read(fd, out, 1); if (n < 0 && errno == EINTR) continue; if (n == 1) return ECEX_OK; return ECEX_ERR; } } static int ecex_lsp_read_line(int fd, char *out, size_t out_size, int timeout_ms) { if (!out || out_size == 0) return ECEX_ERR; size_t used = 0; for (;;) { char ch = '\0'; if (ecex_lsp_read_byte(fd, &ch, timeout_ms) != ECEX_OK) return ECEX_ERR; if (ch == '\n') break; if (used + 1 < out_size) out[used++] = ch; } if (used > 0 && out[used - 1] == '\r') used--; out[used] = '\0'; return ECEX_OK; } static int ecex_lsp_read_exact(int fd, char *out, size_t len, int timeout_ms) { if (!out) return ECEX_ERR; size_t got = 0; while (got < len) { if (ecex_lsp_wait_readable(fd, timeout_ms) != ECEX_OK) return ECEX_ERR; ssize_t n = read(fd, out + got, len - got); if (n < 0 && errno == EINTR) continue; if (n <= 0) return ECEX_ERR; got += (size_t)n; } return ECEX_OK; } static int ecex_lsp_read_message(int fd, char **out_body, int timeout_ms) { if (!out_body) return ECEX_ERR; *out_body = NULL; size_t content_len = 0; int have_content_len = 0; for (int i = 0; i < 64; i++) { char line[256]; if (ecex_lsp_read_line(fd, line, sizeof(line), timeout_ms) != ECEX_OK) return ECEX_ERR; if (line[0] == '\0') break; if (strncmp(line, "Content-Length:", 15) == 0) { char *end = NULL; unsigned long n = strtoul(line + 15, &end, 10); if (n > 0) { content_len = (size_t)n; have_content_len = 1; } } } if (!have_content_len || content_len == 0 || content_len > 32u * 1024u * 1024u) { return ECEX_ERR; } char *body = malloc(content_len + 1); if (!body) return ECEX_ERR; if (ecex_lsp_read_exact(fd, body, content_len, timeout_ms) != ECEX_OK) { free(body); return ECEX_ERR; } body[content_len] = '\0'; *out_body = body; return ECEX_OK; } static int ecex_json_has_id(const char *json, int id) { if (!json) return 0; const char *p = json; while ((p = strstr(p, "\"id\"")) != NULL) { p += 4; while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') p++; if (*p != ':') continue; p++; while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') p++; char *end = NULL; long value = strtol(p, &end, 10); if (end != p && value == id) return 1; } return 0; } static int ecex_lsp_read_response(int fd, int id, char **out_body) { if (!out_body) return ECEX_ERR; *out_body = NULL; for (int i = 0; i < ECEX_CLANGD_MAX_MESSAGES; i++) { char *body = NULL; if (ecex_lsp_read_message(fd, &body, ECEX_CLANGD_TIMEOUT_MS) != ECEX_OK) return ECEX_ERR; if (ecex_json_has_id(body, id)) { *out_body = body; return ECEX_OK; } free(body); } return ECEX_ERR; } static int ecex_lsp_start_clangd(ecex_lsp_process_t *proc) { if (!proc) return ECEX_ERR; memset(proc, 0, sizeof(*proc)); proc->pid = -1; proc->in_fd = -1; proc->out_fd = -1; signal(SIGPIPE, SIG_IGN); int in_pipe[2]; int out_pipe[2]; if (pipe(in_pipe) != 0) return ECEX_ERR; if (pipe(out_pipe) != 0) { close(in_pipe[0]); close(in_pipe[1]); return ECEX_ERR; } pid_t pid = fork(); if (pid < 0) { close(in_pipe[0]); close(in_pipe[1]); close(out_pipe[0]); close(out_pipe[1]); return ECEX_ERR; } if (pid == 0) { dup2(in_pipe[0], STDIN_FILENO); dup2(out_pipe[1], STDOUT_FILENO); int null_fd = open("/dev/null", O_WRONLY); if (null_fd >= 0) { dup2(null_fd, STDERR_FILENO); close(null_fd); } close(in_pipe[0]); close(in_pipe[1]); close(out_pipe[0]); close(out_pipe[1]); execlp("clangd", "clangd", "--log=error", "--pretty=false", (char *)NULL); _exit(127); } close(in_pipe[0]); close(out_pipe[1]); proc->pid = pid; proc->in_fd = in_pipe[1]; proc->out_fd = out_pipe[0]; return ECEX_OK; } static void ecex_lsp_finish(ecex_lsp_process_t *proc) { if (!proc || proc->pid <= 0) return; if (proc->in_fd >= 0) { const char *shutdown = "{\"jsonrpc\":\"2.0\",\"id\":99,\"method\":\"shutdown\",\"params\":null}"; const char *exit_msg = "{\"jsonrpc\":\"2.0\",\"method\":\"exit\",\"params\":{}}"; ecex_lsp_send(proc->in_fd, shutdown); ecex_lsp_send(proc->in_fd, exit_msg); close(proc->in_fd); proc->in_fd = -1; } if (proc->out_fd >= 0) { close(proc->out_fd); proc->out_fd = -1; } for (int i = 0; i < 50; i++) { int status = 0; pid_t rc = waitpid(proc->pid, &status, WNOHANG); if (rc == proc->pid) { proc->pid = -1; return; } if (rc < 0 && errno != EINTR) break; poll(NULL, 0, 10); } kill(proc->pid, SIGKILL); waitpid(proc->pid, NULL, 0); proc->pid = -1; } static char *ecex_json_escape_len(const char *text, size_t len) { if (!text && len != 0) return NULL; size_t cap = len * 6 + 1; char *out = malloc(cap); if (!out) return NULL; size_t used = 0; static const char hex[] = "0123456789abcdef"; for (size_t i = 0; i < len; i++) { unsigned char c = (unsigned char)text[i]; if (c == '"' || c == '\\') { out[used++] = '\\'; out[used++] = (char)c; } else if (c == '\n') { out[used++] = '\\'; out[used++] = 'n'; } else if (c == '\r') { out[used++] = '\\'; out[used++] = 'r'; } else if (c == '\t') { out[used++] = '\\'; out[used++] = 't'; } else if (c < 0x20) { out[used++] = '\\'; out[used++] = 'u'; out[used++] = '0'; out[used++] = '0'; out[used++] = hex[(c >> 4) & 0x0f]; out[used++] = hex[c & 0x0f]; } else { out[used++] = (char)c; } } out[used] = '\0'; return out; } static char *ecex_json_escape(const char *text) { return ecex_json_escape_len(text ? text : "", text ? strlen(text) : 0); } static int ecex_uri_unreserved(unsigned char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' || c == '~' || c == '/'; } static char *ecex_lsp_file_uri(const char *path) { if (!path || !path[0]) return NULL; char *normal = ecex_path_normalize(path); if (!normal) return NULL; size_t len = strlen(normal); char *out = malloc(7 + len * 3 + 1); if (!out) { free(normal); return NULL; } memcpy(out, "file://", 7); size_t used = 7; static const char hex[] = "0123456789ABCDEF"; for (size_t i = 0; i < len; i++) { unsigned char c = (unsigned char)normal[i]; if (ecex_uri_unreserved(c)) { out[used++] = (char)c; } else { out[used++] = '%'; out[used++] = hex[(c >> 4) & 0x0f]; out[used++] = hex[c & 0x0f]; } } out[used] = '\0'; free(normal); return out; } static const char *ecex_lsp_language_id(const char *path) { if (!path) return "c"; const char *dot = strrchr(path, '.'); if (!dot) return "c"; if (strcmp(dot, ".cc") == 0 || strcmp(dot, ".cpp") == 0 || strcmp(dot, ".cxx") == 0 || strcmp(dot, ".hh") == 0 || strcmp(dot, ".hpp") == 0 || strcmp(dot, ".hxx") == 0) { return "cpp"; } return "c"; } static void ecex_buffer_lsp_position(buffer_t *buffer, int *out_line, int *out_character) { int line = 0; int character = 0; if (buffer && buffer->data) { size_t point = buffer->point > buffer->len ? buffer->len : buffer->point; for (size_t i = 0; i < point; i++) { if (buffer->data[i] == '\n') { line++; character = 0; } else { character++; } } } if (out_line) *out_line = line; if (out_character) *out_character = character; } static int ecex_json_hex_value(char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return c - 'a' + 10; if (c >= 'A' && c <= 'F') return c - 'A' + 10; return -1; } static const char *ecex_json_decode_string(const char *p, char *out, size_t out_size) { if (!p || *p != '"' || !out || out_size == 0) return NULL; p++; size_t used = 0; while (*p && *p != '"') { unsigned char c = (unsigned char)*p++; if (c == '\\') { char esc = *p++; if (esc == '"' || esc == '\\' || esc == '/') c = (unsigned char)esc; else if (esc == 'b') c = '\b'; else if (esc == 'f') c = '\f'; else if (esc == 'n') c = '\n'; else if (esc == 'r') c = '\r'; else if (esc == 't') c = '\t'; else if (esc == 'u') { int h1 = ecex_json_hex_value(p[0]); int h2 = ecex_json_hex_value(p[1]); int h3 = ecex_json_hex_value(p[2]); int h4 = ecex_json_hex_value(p[3]); if (h1 < 0 || h2 < 0 || h3 < 0 || h4 < 0) return NULL; unsigned int code = (unsigned int)((h1 << 12) | (h2 << 8) | (h3 << 4) | h4); p += 4; c = code < 0x80 ? (unsigned char)code : '?'; } else { return NULL; } } if (used + 1 < out_size) out[used++] = (char)c; } if (*p != '"') return NULL; out[used] = '\0'; return p + 1; } static int ecex_json_string_between(const char *start, const char *end, const char *key, char *out, size_t out_size) { if (out && out_size) out[0] = '\0'; if (!start || !end || !key || !out || out_size == 0 || start >= end) return 0; char pattern[64]; snprintf(pattern, sizeof(pattern), "\"%s\"", key); const char *p = start; while ((p = strstr(p, pattern)) != NULL && p < end) { p += strlen(pattern); while (p < end && (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n')) p++; if (p >= end || *p != ':') continue; p++; while (p < end && (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n')) p++; if (p >= end || *p != '"') continue; return ecex_json_decode_string(p, out, out_size) ? 1 : 0; } return 0; } static void ecex_trim_in_place(char *text) { if (!text) return; char *start = text; while (*start == ' ' || *start == '\t' || *start == '\r' || *start == '\n') start++; if (start != text) memmove(text, start, strlen(start) + 1); size_t len = strlen(text); while (len > 0 && (text[len - 1] == ' ' || text[len - 1] == '\t' || text[len - 1] == '\r' || text[len - 1] == '\n')) { text[--len] = '\0'; } } static void ecex_lsp_completion_detail(const char *label, const char *item_start, const char *item_end, char *out, size_t out_size) { if (out && out_size) out[0] = '\0'; if (!label || !item_start || !item_end || !out || out_size == 0) return; char detail[192]; char description[128]; char documentation[192]; detail[0] = '\0'; description[0] = '\0'; documentation[0] = '\0'; ecex_json_string_between(item_start, item_end, "detail", detail, sizeof(detail)); ecex_json_string_between(item_start, item_end, "description", description, sizeof(description)); ecex_json_string_between(item_start, item_end, "documentation", documentation, sizeof(documentation)); ecex_trim_in_place(detail); ecex_trim_in_place(description); ecex_trim_in_place(documentation); if (detail[0] == '(') { snprintf(out, out_size, "%s%s%s%s", label, detail, description[0] ? " -> " : "", description); } else if (detail[0] && description[0]) { snprintf(out, out_size, "%s; %s", detail, description); } else if (detail[0]) { snprintf(out, out_size, "%s", detail); } else if (description[0]) { snprintf(out, out_size, "%s", description); } else if (documentation[0]) { snprintf(out, out_size, "%s", documentation); } } static int ecex_lsp_collect_completions(const char *json, const char *prefix, ecex_completion_candidate_t *items, size_t cap, size_t *count) { if (!json || !prefix || !items || !count) return ECEX_ERR; const char *p = json; while ((p = strstr(p, "\"label\"")) != NULL) { const char *item_start = p; p += 7; while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') p++; if (*p != ':') continue; p++; while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') p++; if (*p != '"') continue; char label[256]; const char *next = ecex_json_decode_string(p, label, sizeof(label)); if (!next) { p++; continue; } p = next; char *trimmed = label; while (*trimmed == ' ' || *trimmed == '\t' || *trimmed == '\r' || *trimmed == '\n') trimmed++; size_t trimmed_len = strlen(trimmed); while (trimmed_len > 0 && (trimmed[trimmed_len - 1] == ' ' || trimmed[trimmed_len - 1] == '\t' || trimmed[trimmed_len - 1] == '\r' || trimmed[trimmed_len - 1] == '\n')) { trimmed[--trimmed_len] = '\0'; } if (trimmed_len == 0) continue; int score = ecex_fuzzy_score(trimmed, prefix); if (score < 0) continue; score += 7000; const char *item_end = strstr(p, "\"label\""); if (!item_end) item_end = json + strlen(json); char detail[256]; ecex_lsp_completion_detail(trimmed, item_start, item_end, detail, sizeof(detail)); ecex_completion_candidates_add(items, cap, count, trimmed, detail, score); } return *count > 0 ? ECEX_OK : ECEX_ERR; } static int ecex_clangd_collect_candidates(buffer_t *buffer, const char *prefix, ecex_completion_candidate_t *items, size_t cap, size_t *count) { if (!buffer || !buffer->path || !buffer->path[0] || !prefix || !items || !count) return ECEX_ERR; char *uri = ecex_lsp_file_uri(buffer->path); char *root_path = ecex_path_dirname(buffer->path); char *root_uri = root_path ? ecex_lsp_file_uri(root_path) : NULL; char *escaped_uri = ecex_json_escape(uri); char *escaped_root_uri = ecex_json_escape(root_uri ? root_uri : uri); char *escaped_text = ecex_json_escape_len(buffer->data ? buffer->data : "", buffer->len); if (!uri || !escaped_uri || !escaped_root_uri || !escaped_text) { free(uri); free(root_path); free(root_uri); free(escaped_uri); free(escaped_root_uri); free(escaped_text); return ECEX_ERR; } int line = 0; int character = 0; ecex_buffer_lsp_position(buffer, &line, &character); const char *language_id = ecex_lsp_language_id(buffer->path); char *initialize = ecex_lsp_format( "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\"," "\"params\":{\"processId\":null,\"rootUri\":\"%s\"," "\"capabilities\":{\"textDocument\":{\"completion\":{\"completionItem\":{" "\"snippetSupport\":false,\"labelDetailsSupport\":true," "\"documentationFormat\":[\"plaintext\"]}}}}," "\"clientInfo\":{\"name\":\"ecex\",\"version\":\"0\"}}}", escaped_root_uri); char *initialized = ecex_lsp_format( "{\"jsonrpc\":\"2.0\",\"method\":\"initialized\",\"params\":{}}"); char *did_open = ecex_lsp_format( "{\"jsonrpc\":\"2.0\",\"method\":\"textDocument/didOpen\"," "\"params\":{\"textDocument\":{\"uri\":\"%s\",\"languageId\":\"%s\"," "\"version\":1,\"text\":\"%s\"}}}", escaped_uri, language_id, escaped_text); char *completion = ecex_lsp_format( "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"textDocument/completion\"," "\"params\":{\"textDocument\":{\"uri\":\"%s\"}," "\"position\":{\"line\":%d,\"character\":%d}," "\"context\":{\"triggerKind\":1}}}", escaped_uri, line, character); free(uri); free(root_path); free(root_uri); free(escaped_uri); free(escaped_root_uri); free(escaped_text); if (!initialize || !initialized || !did_open || !completion) { free(initialize); free(initialized); free(did_open); free(completion); return ECEX_ERR; } int result = ECEX_ERR; ecex_lsp_process_t proc; if (ecex_lsp_start_clangd(&proc) == ECEX_OK) { char *response = NULL; if (ecex_lsp_send(proc.in_fd, initialize) == ECEX_OK && ecex_lsp_read_response(proc.out_fd, 1, &response) == ECEX_OK) { free(response); response = NULL; if (ecex_lsp_send(proc.in_fd, initialized) == ECEX_OK && ecex_lsp_send(proc.in_fd, did_open) == ECEX_OK && ecex_lsp_send(proc.in_fd, completion) == ECEX_OK && ecex_lsp_read_response(proc.out_fd, 2, &response) == ECEX_OK) { result = ecex_lsp_collect_completions(response, prefix, items, cap, count); } } free(response); ecex_lsp_finish(&proc); } free(initialize); free(initialized); free(did_open); free(completion); return result; } static int ecex_clangd_completion_provider(ecex_t *ed, buffer_t *buffer, const char *prefix, char *out, size_t out_size, void *userdata) { (void)ed; (void)userdata; if (out && out_size) out[0] = '\0'; if (!buffer || !prefix || !out || out_size == 0) return -1; ecex_completion_candidate_t items[ECEX_COMPLETION_MAX_CANDIDATES]; size_t count = 0; memset(items, 0, sizeof(items)); if (ecex_clangd_collect_candidates(buffer, prefix, items, ECEX_COMPLETION_MAX_CANDIDATES, &count) != ECEX_OK || count == 0) { return -1; } qsort(items, count, sizeof(items[0]), ecex_completion_candidate_compare); snprintf(out, out_size, "%s", items[0].text); return items[0].score; } int ecex_add_clangd_completion_provider(ecex_t *ed, const char *name, const char *mode_name) { return ecex_add_completion_provider(ed, name, mode_name, ecex_clangd_completion_provider, NULL, NULL); } const char *ecex_complete_command(ecex_t *ed, const char *query) { if (!ed || !query || !ed->theme.completion_enabled) return NULL; const char *best = NULL; int best_score = -1; for (size_t i = 0; i < ed->command_count; i++) { const char *name = ed->commands[i].name; int score = ecex_fuzzy_score(name, query); if (score > best_score) { best_score = score; best = name; } } return best; } static int ecex_identifier_char(int c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'; } static size_t ecex_buffer_identifier_start(buffer_t *buffer) { if (!buffer || !buffer->data) return 0; size_t pos = buffer->point > buffer->len ? buffer->len : buffer->point; while (pos > 0 && ecex_identifier_char((unsigned char)buffer->data[pos - 1])) { pos--; } return pos; } int ecex_buffer_identifier_prefix(buffer_t *buffer, char *out, size_t out_size) { if (out && out_size) out[0] = '\0'; if (!buffer || !out || out_size == 0) return ECEX_ERR; size_t end = buffer->point > buffer->len ? buffer->len : buffer->point; size_t start = ecex_buffer_identifier_start(buffer); size_t len = end > start ? end - start : 0; if (len >= out_size) len = out_size - 1; if (len > 0) memcpy(out, buffer->data + start, len); out[len] = '\0'; return (int)len; } static int ecex_buffer_ed_arrow_context(buffer_t *buffer, const char *prefix) { if (!buffer || !buffer->data || !prefix) return 0; size_t prefix_len = strlen(prefix); size_t point = buffer->point > buffer->len ? buffer->len : buffer->point; if (point < prefix_len + 2) return 0; size_t start = point - prefix_len; if (buffer->data[start - 2] != '-' || buffer->data[start - 1] != '>') return 0; size_t name_end = start - 2; size_t name_start = name_end; while (name_start > 0 && ecex_identifier_char((unsigned char)buffer->data[name_start - 1])) { name_start--; } return name_end - name_start == 2 && buffer->data[name_start] == 'e' && buffer->data[name_start + 1] == 'd'; } static int ecex_completion_provider_context_matches(ecex_completion_provider_t *provider, int ed_arrow_context) { if (!provider) return 0; if (ed_arrow_context) return (provider->flags & ECEX_COMPLETION_ED_ARROW) != 0; return (provider->flags & ECEX_COMPLETION_ED_ARROW) == 0; } static const char *ecex_completion_provider_detail(ecex_completion_provider_t *provider) { if (!provider || !provider->name) return ""; if (provider->detail && provider->detail[0]) return provider->detail; return provider->name; } static void ecex_completion_collect_from_provider(ecex_t *ed, buffer_t *buffer, ecex_completion_provider_t *provider, const char *prefix, ecex_completion_candidate_t *items, size_t cap, size_t *count) { if (!ed || !buffer || !provider || !prefix || !items || !count) return; const char *detail = ecex_completion_provider_detail(provider); if (provider->word_count > 0) { for (size_t i = 0; i < provider->word_count; i++) { int score = ecex_fuzzy_score(provider->words[i], prefix); if (score < 0) continue; const char *word_detail = detail; if (provider->word_details && provider->word_details[i] && provider->word_details[i][0]) { word_detail = provider->word_details[i]; } ecex_completion_candidates_add(items, cap, count, provider->words[i], word_detail, score); } return; } if (provider->fn == ecex_clangd_completion_provider) { ecex_clangd_collect_candidates(buffer, prefix, items, cap, count); return; } if (provider->fn) { char candidate[256]; candidate[0] = '\0'; int score = provider->fn(ed, buffer, prefix, candidate, sizeof(candidate), provider->userdata); if (score == 0) score = ecex_fuzzy_score(candidate, prefix); if (score >= 0 && candidate[0]) { ecex_completion_candidates_add(items, cap, count, candidate, detail, score); } } } static int ecex_completion_cycle_valid(ecex_t *ed, buffer_t *buffer) { if (!ed || !buffer || !ed->completion_cycle_active) return 0; if (ed->completion_cycle_buffer != buffer) return 0; size_t len = strlen(ed->completion_cycle_current); size_t start = ed->completion_cycle_start; if (start + len > buffer->len || start + len != buffer->point) return 0; return strncmp(buffer->data + start, ed->completion_cycle_current, len) == 0; } static void ecex_completion_describe(ecex_t *ed, const ecex_completion_candidate_t *item, size_t index, size_t count) { if (!ed || !item) return; char message[700]; if (item->detail[0]) { size_t text_len = strlen(item->text); if (strncmp(item->detail, item->text, text_len) == 0) { snprintf(message, sizeof(message), "Completion %zu/%zu: %s", index + 1, count, item->detail); } else { snprintf(message, sizeof(message), "Completion %zu/%zu: %s - %s", index + 1, count, item->text, item->detail); } } else { snprintf(message, sizeof(message), "Completion %zu/%zu: %s", index + 1, count, item->text); } ecex_message(ed, message); } static int ecex_complete_at_point_direction(ecex_t *ed, int direction) { if (!ed) return ECEX_ERR; buffer_t *buffer = ecex_current_buffer(ed); if (!buffer || buffer->read_only || buffer_is_interactive(buffer)) return ECEX_ERR; char prefix[256]; size_t start = 0; size_t end = buffer->point > buffer->len ? buffer->len : buffer->point; int cycling = ecex_completion_cycle_valid(ed, buffer); if (cycling) { snprintf(prefix, sizeof(prefix), "%s", ed->completion_cycle_prefix); start = ed->completion_cycle_start; } else { int prefix_len = ecex_buffer_identifier_prefix(buffer, prefix, sizeof(prefix)); if (prefix_len <= 0) { ecex_completion_cycle_reset(ed); ecex_message(ed, "No completion prefix"); return ECEX_OK; } start = ecex_buffer_identifier_start(buffer); } if (!prefix[0]) { ecex_completion_cycle_reset(ed); ecex_message(ed, "No completion prefix"); return ECEX_OK; } ecex_completion_candidate_t items[ECEX_COMPLETION_MAX_CANDIDATES]; size_t count = 0; memset(items, 0, sizeof(items)); int mode = buffer->major_mode; int ed_arrow_context = ecex_buffer_ed_arrow_context(buffer, prefix); for (size_t i = 0; i < ed->completion_provider_count; i++) { ecex_completion_provider_t *provider = &ed->completion_providers[i]; if (provider->mode != 0 && provider->mode != mode) continue; if (!ecex_completion_provider_context_matches(provider, ed_arrow_context)) continue; ecex_completion_collect_from_provider(ed, buffer, provider, prefix, items, ECEX_COMPLETION_MAX_CANDIDATES, &count); } if (count == 0) { ecex_completion_cycle_reset(ed); ecex_message(ed, "No completion"); return ECEX_OK; } qsort(items, count, sizeof(items[0]), ecex_completion_candidate_compare); size_t index = direction < 0 ? count - 1 : 0; if (cycling) { size_t current = count; for (size_t i = 0; i < count; i++) { if (strcmp(items[i].text, ed->completion_cycle_current) == 0) { current = i; break; } } if (current < count) { if (direction < 0) index = current == 0 ? count - 1 : current - 1; else index = (current + 1) % count; } } ecex_completion_candidate_t *choice = &items[index]; size_t current_len = end >= start ? end - start : 0; if (current_len == strlen(choice->text) && strncmp(buffer->data + start, choice->text, current_len) == 0) { ed->completion_cycle_active = 1; ed->completion_cycle_buffer = buffer; ed->completion_cycle_start = start; ed->completion_cycle_index = index; snprintf(ed->completion_cycle_prefix, sizeof(ed->completion_cycle_prefix), "%s", prefix); snprintf(ed->completion_cycle_current, sizeof(ed->completion_cycle_current), "%s", choice->text); ecex_completion_describe(ed, choice, index, count); return ECEX_OK; } if (buffer_delete_range(buffer, start, end) != ECEX_OK) return ECEX_ERR; if (buffer_insert_at(buffer, start, choice->text) != ECEX_OK) return ECEX_ERR; ed->completion_cycle_active = 1; ed->completion_cycle_buffer = buffer; ed->completion_cycle_start = start; ed->completion_cycle_index = index; snprintf(ed->completion_cycle_prefix, sizeof(ed->completion_cycle_prefix), "%s", prefix); snprintf(ed->completion_cycle_current, sizeof(ed->completion_cycle_current), "%s", choice->text); ecex_completion_describe(ed, choice, index, count); return ECEX_OK; } int ecex_complete_at_point(ecex_t *ed) { return ecex_complete_at_point_direction(ed, 1); } static int ecex_set_owned_string(char **slot, const char *value) { if (!slot) return ECEX_ERR; char *copy = NULL; if (value && value[0]) { copy = ecex_strdup(value); if (!copy) return ECEX_ERR; } free(*slot); *slot = copy; return ECEX_OK; } static int ecex_parse_file_line(const char *text, char *path, size_t path_size, size_t *out_line) { if (!text || !path || path_size == 0 || !out_line) return ECEX_ERR; const char *colon = strchr(text, ':'); if (!colon || colon == text) return ECEX_ERR; char *end = NULL; long line = strtol(colon + 1, &end, 10); if (line <= 0 || end == colon + 1) return ECEX_ERR; size_t len = (size_t)(colon - text); if (len >= path_size) len = path_size - 1; memcpy(path, text, len); path[len] = '\0'; *out_line = (size_t)line; return ECEX_OK; } static int ecex_goto_file_line_action(ecex_t *ed, buffer_t *buffer, size_t line, const char *payload, void *userdata) { (void)buffer; (void)line; (void)userdata; if (!ed || !payload) return ECEX_ERR; char path[1024]; size_t target_line = 1; if (ecex_parse_file_line(payload, path, sizeof(path), &target_line) != ECEX_OK) return ECEX_ERR; if (ecex_find_file(ed, path) != ECEX_OK) return ECEX_ERR; buffer_t *buf = ecex_current_buffer(ed); if (!buf) return ECEX_ERR; size_t pos = 0; for (size_t l = 1; l < target_line && pos < buf->len; pos++) { if (buf->data[pos] == '\n') l++; } buffer_set_point(buf, pos); return ECEX_OK; } static int ecex_append_shell_output(ecex_t *ed, const char *name, const char *command) { if (!ed || !name || !command || !command[0]) return ECEX_ERR; buffer_t *buf = ecex_create_interactive_buffer(ed, name); if (!buf) return ECEX_ERR; buffer_set_interactive(buf, 1); if (buffer_clear(buf) != ECEX_OK) return ECEX_ERR; buffer_set_interactive(buf, 1); ecex_buffer_set_major_mode_by_name(ed, buf, "special-mode"); char header[2048]; snprintf(header, sizeof(header), "%s\n\nKeys: g/r rerun, n/p next/previous result, RET jump, q quit.\n\n", command); buffer_append(buf, header); char shell_command[4096]; snprintf(shell_command, sizeof(shell_command), "%s 2>&1", command); FILE *pipe = popen(shell_command, "r"); if (!pipe) { buffer_append(buf, "Failed to start command.\n"); return ecex_switch_buffer(ed, name); } char line[4096]; while (fgets(line, sizeof(line), pipe)) { size_t len = strlen(line); while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) line[--len] = '\0'; char path[1024]; size_t target = 0; ecex_interactive_line_fn fn = NULL; char payload[1200]; payload[0] = '\0'; if (ecex_parse_file_line(line, path, sizeof(path), &target) == ECEX_OK) { snprintf(payload, sizeof(payload), "%s:%zu", path, target); fn = ecex_goto_file_line_action; } ecex_interactive_append_line(ed, buf, line, fn, payload[0] ? payload : NULL, NULL); } int status = pclose(pipe); char footer[128]; snprintf(footer, sizeof(footer), "\n[process exited: %d]\n", status); buffer_append(buf, footer); buf->point = 0; buf->modified = 0; return ecex_switch_buffer(ed, name); } int ecex_compile(ecex_t *ed, const char *command) { if (!ed || !command || !command[0]) return ECEX_ERR; if (ecex_set_owned_string(&ed->last_compile_command, command) != ECEX_OK) return ECEX_ERR; return ecex_append_shell_output(ed, "*compilation*", command); } int ecex_grep(ecex_t *ed, const char *command) { if (!ed || !command || !command[0]) return ECEX_ERR; if (ecex_set_owned_string(&ed->last_grep_command, command) != ECEX_OK) return ECEX_ERR; return ecex_append_shell_output(ed, "*grep*", command); } int ecex_rerun_compile(ecex_t *ed) { if (!ed || !ed->last_compile_command) return ECEX_ERR; return ecex_append_shell_output(ed, "*compilation*", ed->last_compile_command); } int ecex_rerun_grep(ecex_t *ed) { if (!ed || !ed->last_grep_command) return ECEX_ERR; return ecex_append_shell_output(ed, "*grep*", ed->last_grep_command); } int ecex_next_interactive_action(ecex_t *ed) { buffer_t *buf = ecex_current_buffer(ed); if (!buf || !buffer_is_interactive(buf) || buf->interactive_action_count == 0) return ECEX_ERR; size_t current = buffer_current_line_number(buf); if (current > 0) current--; size_t best = (size_t)-1; for (size_t i = 0; i < buf->interactive_action_count; i++) { size_t line = buf->interactive_actions[i].line; if (line > current && (best == (size_t)-1 || line < best)) best = line; } if (best == (size_t)-1) best = buf->interactive_actions[0].line; size_t pos = 0; for (size_t l = 0; l < best && pos < buf->len; pos++) if (buf->data[pos] == '\n') l++; buffer_set_point(buf, pos); return ECEX_OK; } int ecex_previous_interactive_action(ecex_t *ed) { buffer_t *buf = ecex_current_buffer(ed); if (!buf || !buffer_is_interactive(buf) || buf->interactive_action_count == 0) return ECEX_ERR; size_t current = buffer_current_line_number(buf); if (current > 0) current--; size_t best = (size_t)-1; for (size_t i = 0; i < buf->interactive_action_count; i++) { size_t line = buf->interactive_actions[i].line; if (line < current && (best == (size_t)-1 || line > best)) best = line; } if (best == (size_t)-1) best = buf->interactive_actions[buf->interactive_action_count - 1].line; size_t pos = 0; for (size_t l = 0; l < best && pos < buf->len; pos++) if (buf->data[pos] == '\n') l++; buffer_set_point(buf, pos); return ECEX_OK; } static int ecex_region_lines(buffer_t *buf, size_t *out_start, size_t *out_end) { if (!buf || !buffer_has_selection(buf)) return ECEX_ERR; size_t start = 0, end = 0; buffer_selection_range(buf, &start, &end); start = buffer_line_start_at(buf, start); end = buffer_line_end_at(buf, end); if (end < buf->len) end++; if (out_start) *out_start = start; if (out_end) *out_end = end; return ECEX_OK; } int ecex_comment_region(ecex_t *ed) { buffer_t *buf = ecex_current_buffer(ed); size_t start = 0, end = 0; if (!buf || ecex_region_lines(buf, &start, &end) != ECEX_OK) return ECEX_ERR; size_t pos = start; while (pos <= end && pos <= buf->len) { if (buffer_insert_at(buf, pos, "// ") != ECEX_OK) return ECEX_ERR; pos += 3; end += 3; size_t eol = buffer_line_end_at(buf, pos); if (eol >= end || eol >= buf->len) break; pos = eol + 1; } return ECEX_OK; } int ecex_uncomment_region(ecex_t *ed) { buffer_t *buf = ecex_current_buffer(ed); size_t start = 0, end = 0; if (!buf || ecex_region_lines(buf, &start, &end) != ECEX_OK) return ECEX_ERR; size_t pos = start; while (pos < end && pos < buf->len) { if (pos + 2 <= buf->len && memcmp(buf->data + pos, "//", 2) == 0) { size_t del = (pos + 3 <= buf->len && buf->data[pos + 2] == ' ') ? 3 : 2; if (buffer_delete_range(buf, pos, pos + del) != ECEX_OK) return ECEX_ERR; end = end > del ? end - del : 0; } size_t eol = buffer_line_end_at(buf, pos); if (eol >= end || eol >= buf->len) break; pos = eol + 1; } return ECEX_OK; } int ecex_set_font(ecex_t *ed, const char *path) { if (!ed || !path) return ECEX_ERR; if (ed->theme.font_path && strcmp(ed->theme.font_path, path) == 0) return ECEX_OK; char *copy = ecex_strdup(path); if (!copy) return ECEX_ERR; free(ed->theme.font_path); ed->theme.font_path = copy; ecex_mark_font_changed(ed); return ECEX_OK; } float ecex_get_font_size(ecex_t *ed) { if (!ed) return 0.0f; return ed->theme.font_size; } int ecex_set_font_size(ecex_t *ed, float size) { if (!ed) return ECEX_ERR; float clamped = ECEX_CLAMP(size, 8.0f, 96.0f); if (ed->theme.font_size == clamped) return ECEX_OK; ed->theme.font_size = clamped; ecex_mark_font_changed(ed); return ECEX_OK; } int ecex_adjust_font_size(ecex_t *ed, float delta) { if (!ed) return ECEX_ERR; return ecex_set_font_size(ed, ed->theme.font_size + delta); } void ecex_set_bg_color(ecex_t *ed, float r, float g, float b) { if (ed) { ed->theme.bg = ecex_color(r, g, b); ecex_mark_ui_changed(ed); } } void ecex_set_fg_color(ecex_t *ed, float r, float g, float b) { if (ed) { ed->theme.fg = ecex_color(r, g, b); ecex_mark_ui_changed(ed); } } void ecex_set_status_bg_color(ecex_t *ed, float r, float g, float b) { if (ed) { ed->theme.status_bg = ecex_color(r, g, b); ecex_mark_ui_changed(ed); } } void ecex_set_status_fg_color(ecex_t *ed, float r, float g, float b) { if (ed) { ed->theme.status_fg = ecex_color(r, g, b); ecex_mark_ui_changed(ed); } } void ecex_set_status_border_color(ecex_t *ed, float r, float g, float b) { if (ed) { ed->theme.status_border = ecex_color(r, g, b); ecex_mark_ui_changed(ed); } } void ecex_set_cursor_color(ecex_t *ed, float r, float g, float b) { if (ed) { ed->theme.cursor = ecex_color(r, g, b); ecex_mark_ui_changed(ed); } } void ecex_set_region_bg_color(ecex_t *ed, float r, float g, float b) { if (ed) { ed->theme.region_bg = ecex_color(r, g, b); ecex_mark_ui_changed(ed); } } void ecex_set_minibuffer_bg_color(ecex_t *ed, float r, float g, float b) { if (ed) { ed->theme.minibuffer_bg = ecex_color(r, g, b); ecex_mark_ui_changed(ed); } } void ecex_set_minibuffer_fg_color(ecex_t *ed, float r, float g, float b) { if (ed) { ed->theme.minibuffer_fg = ecex_color(r, g, b); ecex_mark_ui_changed(ed); } } void ecex_set_completion_fg_color(ecex_t *ed, float r, float g, float b) { if (ed) { ed->theme.completion_fg = ecex_color(r, g, b); ecex_mark_ui_changed(ed); } } void ecex_set_completion_enabled(ecex_t *ed, int enabled) { if (ed) { ed->theme.completion_enabled = enabled ? 1 : 0; ecex_mark_ui_changed(ed); } } void ecex_set_interactive_highlight_bg_color(ecex_t *ed, float r, float g, float b) { if (ed) { ed->theme.interactive_highlight_bg = ecex_color(r, g, b); ecex_mark_ui_changed(ed); } } void ecex_set_interactive_highlight_fg_color(ecex_t *ed, float r, float g, float b) { if (ed) { ed->theme.interactive_highlight_fg = ecex_color(r, g, b); ecex_mark_ui_changed(ed); } } void ecex_set_current_line_bg_color(ecex_t *ed, float r, float g, float b) { if (ed) { ed->theme.current_line_bg = ecex_color(r, g, b); ecex_mark_ui_changed(ed); } } void ecex_set_search_bg_color(ecex_t *ed, float r, float g, float b) { if (ed) { ed->theme.search_bg = ecex_color(r, g, b); ecex_mark_ui_changed(ed); } } void ecex_set_line_numbers_enabled(ecex_t *ed, int enabled) { if (ed) { ed->theme.line_numbers_enabled = enabled ? 1 : 0; ecex_mark_ui_changed(ed); } } void ecex_set_current_line_enabled(ecex_t *ed, int enabled) { if (ed) { ed->theme.current_line_enabled = enabled ? 1 : 0; ecex_mark_ui_changed(ed); } }