diff options
Diffstat (limited to 'src/ecex.c')
| -rw-r--r-- | src/ecex.c | 2600 |
1 files changed, 1994 insertions, 606 deletions
@@ -7,15 +7,24 @@ #include "util.h" #include "path.h" +#include <errno.h> +#include <fcntl.h> +#include <poll.h> +#include <signal.h> +#include <stdarg.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <dirent.h> +#include <sys/types.h> #include <sys/stat.h> #include <sys/time.h> +#include <sys/wait.h> +#include <unistd.h> 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 @@ -23,8 +32,15 @@ extern int pclose(FILE *stream); #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) { @@ -47,149 +63,12 @@ double ecex_time_seconds(void) { return (double)tv.tv_sec + (double)tv.tv_usec / 1000000.0; } -static int ecex_env_enabled(const char *name) { - const char *v; - if (!name) return 0; - v = getenv(name); - return v && v[0] && v[0] != '0'; -} - -static int ecex_log_enabled(void) { - return ecex_env_enabled("ECEX_LOG") || - ecex_env_enabled("ECEX_TRACE_CALLBACKS") || - ecex_env_enabled("ECEX_TRACE_TETRIS"); -} - -void ecex_log(const char *message) { - if (!ecex_log_enabled()) return; - fprintf(stderr, "ecex-log: %s\n", message ? message : "(null)"); - fflush(stderr); -} - -void ecex_log_int(const char *message, int value) { - if (!ecex_log_enabled()) return; - fprintf(stderr, "ecex-log: %s%d\n", message ? message : "", value); - fflush(stderr); -} - -void ecex_log_double(const char *message, double value) { - if (!ecex_log_enabled()) return; - fprintf(stderr, "ecex-log: %s%.6f\n", message ? message : "", value); - fflush(stderr); -} - -void ecex_log_ptr(const char *message, const void *ptr) { - if (!ecex_log_enabled()) return; - fprintf(stderr, "ecex-log: %s%p\n", message ? message : "", ptr); - fflush(stderr); -} - void ecex_mem_zero(void *ptr, size_t size) { if (!ptr || size == 0) return; memset(ptr, 0, size); } -static ecex_object_entry_t *ecex_object_find_entry(ecex_t *ed, void *object) { - if (!ed || !object) return NULL; - for (size_t i = 0; i < ed->object_count; ++i) { - if (ed->objects[i].ptr == object) return &ed->objects[i]; - } - return NULL; -} - -static int ecex_reserve_objects(ecex_t *ed, size_t needed) { - ecex_object_entry_t *new_objects; - size_t new_cap; - if (!ed) return ECEX_ERR; - if (needed <= ed->object_cap) return ECEX_OK; - new_cap = ed->object_cap ? ed->object_cap * 2 : 16; - while (new_cap < needed) new_cap *= 2; - new_objects = realloc(ed->objects, new_cap * sizeof(*new_objects)); - if (!new_objects) return ECEX_ERR; - memset(new_objects + ed->object_cap, 0, (new_cap - ed->object_cap) * sizeof(*new_objects)); - ed->objects = new_objects; - ed->object_cap = new_cap; - return ECEX_OK; -} - -void *ecex_object_alloc(ecex_t *ed, size_t size) { - void *ptr; - if (!ed) return NULL; - if (size == 0) size = 1; - if (ecex_reserve_objects(ed, ed->object_count + 1) != ECEX_OK) return NULL; - ptr = malloc(size); - if (!ptr) return NULL; - ed->objects[ed->object_count].ptr = ptr; - ed->objects[ed->object_count].size = size; - ed->object_count++; - return ptr; -} - -void *ecex_object_calloc(ecex_t *ed, size_t count, size_t size) { - void *ptr; - size_t total; - if (!ed) return NULL; - if (count == 0) count = 1; - if (size == 0) size = 1; - if (count > ((size_t)-1) / size) return NULL; - total = count * size; - ptr = ecex_object_alloc(ed, total); - if (!ptr) return NULL; - memset(ptr, 0, total); - return ptr; -} - -int ecex_object_free(ecex_t *ed, void *object) { - if (!ed || !object) return ECEX_ERR; - for (size_t i = 0; i < ed->object_count; ++i) { - if (ed->objects[i].ptr == object) { - free(ed->objects[i].ptr); - if (i + 1 < ed->object_count) { - memmove(&ed->objects[i], &ed->objects[i + 1], (ed->object_count - i - 1) * sizeof(ed->objects[i])); - } - --ed->object_count; - if (ed->object_count < ed->object_cap) memset(&ed->objects[ed->object_count], 0, sizeof(ed->objects[ed->object_count])); - return ECEX_OK; - } - } - return ECEX_ERR; -} - -int ecex_object_valid(ecex_t *ed, void *object) { - return ecex_object_find_entry(ed, object) ? 1 : 0; -} - -int ecex_object_i32_get(ecex_t *ed, void *object, size_t byte_offset, int fallback) { - ecex_object_entry_t *o = ecex_object_find_entry(ed, object); - int value; - if (!o || !o->ptr || byte_offset > o->size || o->size - byte_offset < sizeof(int)) return fallback; - memcpy(&value, (const char *)o->ptr + byte_offset, sizeof(value)); - return value; -} - -int ecex_object_i32_set(ecex_t *ed, void *object, size_t byte_offset, int value) { - ecex_object_entry_t *o = ecex_object_find_entry(ed, object); - if (!o || !o->ptr || byte_offset > o->size || o->size - byte_offset < sizeof(int)) return ECEX_ERR; - memcpy((char *)o->ptr + byte_offset, &value, sizeof(value)); - return ECEX_OK; -} - -void *ecex_object_ptr_get(ecex_t *ed, void *object, size_t byte_offset) { - ecex_object_entry_t *o = ecex_object_find_entry(ed, object); - void *value = NULL; - if (!o || !o->ptr || byte_offset > o->size || o->size - byte_offset < sizeof(void *)) return NULL; - memcpy(&value, (const char *)o->ptr + byte_offset, sizeof(value)); - return value; -} - -int ecex_object_ptr_set(ecex_t *ed, void *object, size_t byte_offset, void *value) { - ecex_object_entry_t *o = ecex_object_find_entry(ed, object); - if (!o || !o->ptr || byte_offset > o->size || o->size - byte_offset < sizeof(void *)) return ECEX_ERR; - memcpy((char *)o->ptr + byte_offset, &value, sizeof(value)); - return ECEX_OK; -} - int ecex_i32_get(const int *items, size_t index) { if (!items) return 0; return items[index]; @@ -200,372 +79,8 @@ void ecex_i32_set(int *items, size_t index, int value) { items[index] = value; } -static char *ecex_var_strdup(const char *s) { - size_t n; - char *out; - if (!s) s = ""; - n = strlen(s) + 1; - out = malloc(n); - if (!out) return NULL; - memcpy(out, s, n); - return out; -} - -static int ecex_var_name_eq(const char *a, const char *b) { - if (!a) a = ""; - if (!b) b = ""; - return strcmp(a, b) == 0; -} - -static ecex_var_entry_t *ecex_var_find_entry(ecex_t *ed, void *owner, const char *name) { - size_t i; - if (!ed || !name) return NULL; - for (i = 0; i < ed->var_count; ++i) { - ecex_var_entry_t *v = &ed->vars[i]; - if (v->owner == owner && ecex_var_name_eq(v->name, name)) return v; - } - return NULL; -} - -static int ecex_reserve_vars(ecex_t *ed, size_t needed) { - ecex_var_entry_t *new_vars; - size_t new_cap; - if (!ed) return ECEX_ERR; - if (needed <= ed->var_cap) return ECEX_OK; - new_cap = ed->var_cap ? ed->var_cap * 2 : 16; - while (new_cap < needed) new_cap *= 2; - new_vars = realloc(ed->vars, new_cap * sizeof(*new_vars)); - if (!new_vars) return ECEX_ERR; - memset(new_vars + ed->var_cap, 0, (new_cap - ed->var_cap) * sizeof(*new_vars)); - ed->vars = new_vars; - ed->var_cap = new_cap; - return ECEX_OK; -} - -void *ecex_var_get(ecex_t *ed, void *owner, const char *name) { - ecex_var_entry_t *v = ecex_var_find_entry(ed, owner, name); - return v ? v->data : NULL; -} - -void *ecex_var_get_or_alloc(ecex_t *ed, void *owner, const char *name, size_t count, size_t elem_size) { - ecex_var_entry_t *v; - void *data; - if (!ed || !name) return NULL; - if (count == 0) count = 1; - if (elem_size == 0) elem_size = 1; - - v = ecex_var_find_entry(ed, owner, name); - if (v) { - if (v->elem_size != elem_size) { - ecex_log("ecex_var_get_or_alloc: existing slot element-size mismatch"); - return NULL; - } - if (v->count < count) { - void *new_data; - if (!v->dynamic) { - ecex_log("ecex_var_get_or_alloc: static slot too small"); - return NULL; - } - new_data = realloc(v->data, count * elem_size); - if (!new_data) return NULL; - memset((char *)new_data + v->count * elem_size, 0, (count - v->count) * elem_size); - v->data = new_data; - v->count = count; - } - return v->data; - } - - if (ecex_reserve_vars(ed, ed->var_count + 1) != ECEX_OK) return NULL; - data = calloc(count, elem_size); - if (!data) return NULL; - - v = &ed->vars[ed->var_count++]; - memset(v, 0, sizeof(*v)); - v->owner = owner; - v->name = ecex_var_strdup(name); - if (!v->name) { - free(data); - --ed->var_count; - return NULL; - } - v->data = data; - v->elem_size = elem_size; - v->count = count; - v->kind = (elem_size == sizeof(int)) ? ECEX_VAR_I32 : ECEX_VAR_BYTES; - v->dynamic = 1; - return data; -} - -int ecex_var_bind_static(ecex_t *ed, void *owner, const char *name, void *data, size_t count, size_t elem_size) { - ecex_var_entry_t *v; - if (!ed || !name || !data) return ECEX_ERR; - if (count == 0) count = 1; - if (elem_size == 0) elem_size = 1; - - v = ecex_var_find_entry(ed, owner, name); - if (!v) { - if (ecex_reserve_vars(ed, ed->var_count + 1) != ECEX_OK) return ECEX_ERR; - v = &ed->vars[ed->var_count++]; - memset(v, 0, sizeof(*v)); - v->name = ecex_var_strdup(name); - if (!v->name) { - --ed->var_count; - return ECEX_ERR; - } - } else if (v->dynamic) { - free(v->data); - } - - v->owner = owner; - v->data = data; - v->elem_size = elem_size; - v->count = count; - v->kind = (elem_size == sizeof(int)) ? ECEX_VAR_I32 : ECEX_VAR_BYTES; - v->dynamic = 0; - return ECEX_OK; -} - -static void ecex_var_entry_clear(ecex_var_entry_t *v) { - if (!v) return; - if (v->dynamic) free(v->data); - free(v->name); - memset(v, 0, sizeof(*v)); -} - -int ecex_var_free(ecex_t *ed, void *owner, const char *name) { - size_t i; - if (!ed || !name) return ECEX_ERR; - for (i = 0; i < ed->var_count; ++i) { - if (ed->vars[i].owner == owner && ecex_var_name_eq(ed->vars[i].name, name)) { - ecex_var_entry_clear(&ed->vars[i]); - if (i + 1 < ed->var_count) { - memmove(&ed->vars[i], &ed->vars[i + 1], (ed->var_count - i - 1) * sizeof(ed->vars[i])); - } - --ed->var_count; - if (ed->var_count < ed->var_cap) memset(&ed->vars[ed->var_count], 0, sizeof(ed->vars[ed->var_count])); - return ECEX_OK; - } - } - return ECEX_ERR; -} - -int ecex_var_free_owner(ecex_t *ed, void *owner) { - size_t i; - if (!ed) return ECEX_ERR; - i = 0; - while (i < ed->var_count) { - if (ed->vars[i].owner == owner) { - ecex_var_entry_clear(&ed->vars[i]); - if (i + 1 < ed->var_count) { - memmove(&ed->vars[i], &ed->vars[i + 1], (ed->var_count - i - 1) * sizeof(ed->vars[i])); - } - --ed->var_count; - if (ed->var_count < ed->var_cap) memset(&ed->vars[ed->var_count], 0, sizeof(ed->vars[ed->var_count])); - continue; - } - ++i; - } - return ECEX_OK; -} - -int ecex_var_i32_get(ecex_t *ed, void *owner, const char *name, size_t index, int fallback) { - ecex_var_entry_t *v = ecex_var_find_entry(ed, owner, name); - if (!v || !v->data || v->elem_size != sizeof(int) || index >= v->count) return fallback; - return ((int *)v->data)[index]; -} - -int ecex_var_i32_set(ecex_t *ed, void *owner, const char *name, size_t index, int value) { - int *data; - if (!ed || !name) return ECEX_ERR; - data = (int *)ecex_var_get_or_alloc(ed, owner, name, index + 1, sizeof(int)); - if (!data) return ECEX_ERR; - data[index] = value; - return ECEX_OK; -} - -int ecex_var_i32(ecex_t *ed, void *owner, const char *name, int fallback) { - return ecex_var_i32_get(ed, owner, name, 0, fallback); -} - -int ecex_var_i32_set_scalar(ecex_t *ed, void *owner, const char *name, int value) { - return ecex_var_i32_set(ed, owner, name, 0, value); -} - - - -static ecex_text_entry_t *ecex_text_find_entry(ecex_t *ed, void *owner, int id) { - if (!ed) return NULL; - for (size_t i = 0; i < ed->text_count; ++i) { - ecex_text_entry_t *t = &ed->texts[i]; - if (t->owner == owner && t->id == id) return t; - } - return NULL; -} - -static int ecex_reserve_texts(ecex_t *ed, size_t needed) { - ecex_text_entry_t *new_texts; - size_t new_cap; - if (!ed) return ECEX_ERR; - if (needed <= ed->text_cap) return ECEX_OK; - new_cap = ed->text_cap ? ed->text_cap * 2 : 32; - while (new_cap < needed) new_cap *= 2; - new_texts = realloc(ed->texts, new_cap * sizeof(*new_texts)); - if (!new_texts) return ECEX_ERR; - memset(new_texts + ed->text_cap, 0, (new_cap - ed->text_cap) * sizeof(*new_texts)); - ed->texts = new_texts; - ed->text_cap = new_cap; - return ECEX_OK; -} - -static void ecex_text_entry_clear(ecex_text_entry_t *t) { - if (!t) return; - free(t->text); - memset(t, 0, sizeof(*t)); -} - -int ecex_text_set(ecex_t *ed, void *owner, int id, const char *text, int len) { - ecex_text_entry_t *t; - char *copy; - size_t n; - if (!ed || id < 0) return ECEX_ERR; - if (!text) text = ""; - if (len < 0) n = strlen(text); - else n = (size_t)len; - copy = malloc(n + 1); - if (!copy) return ECEX_ERR; - if (n) memcpy(copy, text, n); - copy[n] = '\0'; - - t = ecex_text_find_entry(ed, owner, id); - if (!t) { - if (ecex_reserve_texts(ed, ed->text_count + 1) != ECEX_OK) { - free(copy); - return ECEX_ERR; - } - t = &ed->texts[ed->text_count++]; - memset(t, 0, sizeof(*t)); - t->owner = owner; - t->id = id; - } - free(t->text); - t->text = copy; - t->len = n; - return ECEX_OK; -} - -int ecex_text_set_buffer_title(ecex_t *ed, void *owner, int id, buffer_t *buffer) { - const char *title = NULL; - if (buffer) title = buffer->path ? buffer->path : buffer->name; - return ecex_text_set(ed, owner, id, title ? title : "(unnamed)", -1); -} - -int ecex_text_free(ecex_t *ed, void *owner, int id) { - if (!ed) return ECEX_ERR; - for (size_t i = 0; i < ed->text_count; ++i) { - if (ed->texts[i].owner == owner && ed->texts[i].id == id) { - ecex_text_entry_clear(&ed->texts[i]); - if (i + 1 < ed->text_count) { - memmove(&ed->texts[i], &ed->texts[i + 1], (ed->text_count - i - 1) * sizeof(ed->texts[i])); - } - --ed->text_count; - if (ed->text_count < ed->text_cap) memset(&ed->texts[ed->text_count], 0, sizeof(ed->texts[ed->text_count])); - return ECEX_OK; - } - } - return ECEX_ERR; -} - -int ecex_text_free_owner(ecex_t *ed, void *owner) { - if (!ed) return ECEX_ERR; - for (size_t i = 0; i < ed->text_count;) { - if (ed->texts[i].owner == owner) { - ecex_text_entry_clear(&ed->texts[i]); - if (i + 1 < ed->text_count) { - memmove(&ed->texts[i], &ed->texts[i + 1], (ed->text_count - i - 1) * sizeof(ed->texts[i])); - } - --ed->text_count; - if (ed->text_count < ed->text_cap) memset(&ed->texts[ed->text_count], 0, sizeof(ed->texts[ed->text_count])); - continue; - } - ++i; - } - return ECEX_OK; -} - -const char *ecex_text_get_for_draw(ecex_t *ed, void *owner, int id) { - ecex_text_entry_t *t = ecex_text_find_entry(ed, owner, id); - return t && t->text ? t->text : ""; -} - -static int ecex_ascii_equal_ci(const char *a, const char *b) { - unsigned char ca; - unsigned char cb; - if (!a || !b) return 0; - while (*a && *b) { - ca = (unsigned char)*a; - cb = (unsigned char)*b; - if (ca >= 'A' && ca <= 'Z') ca = (unsigned char)(ca - 'A' + 'a'); - if (cb >= 'A' && cb <= 'Z') cb = (unsigned char)(cb - 'A' + 'a'); - if (ca != cb) return 0; - ++a; - ++b; - } - return *a == '\0' && *b == '\0'; -} - -static const char *ecex_path_extension(const char *path) { - const char *dot; - if (!path) return NULL; - dot = strrchr(path, '.'); - return dot && dot[0] ? dot : NULL; -} - -static int ecex_reserve_file_handlers(ecex_t *ed, size_t needed) { - ecex_file_handler_t *new_handlers; - size_t new_cap; - if (!ed) return ECEX_ERR; - if (needed <= ed->file_handler_cap) return ECEX_OK; - new_cap = ed->file_handler_cap ? ed->file_handler_cap * 2 : 8; - while (new_cap < needed) new_cap *= 2; - new_handlers = realloc(ed->file_handlers, new_cap * sizeof(*new_handlers)); - if (!new_handlers) return ECEX_ERR; - memset(new_handlers + ed->file_handler_cap, 0, (new_cap - ed->file_handler_cap) * sizeof(*new_handlers)); - ed->file_handlers = new_handlers; - ed->file_handler_cap = new_cap; - return ECEX_OK; -} - -int ecex_register_file_handler(ecex_t *ed, const char *extension, ecex_file_handler_fn fn) { - char *copy; - if (!ed || !extension || !extension[0] || !fn) return ECEX_ERR; - for (size_t i = 0; i < ed->file_handler_count; ++i) { - if (ecex_ascii_equal_ci(ed->file_handlers[i].extension, extension)) { - ed->file_handlers[i].fn = fn; - return ECEX_OK; - } - } - if (ecex_reserve_file_handlers(ed, ed->file_handler_count + 1) != ECEX_OK) return ECEX_ERR; - copy = ecex_var_strdup(extension); - if (!copy) return ECEX_ERR; - ed->file_handlers[ed->file_handler_count].extension = copy; - ed->file_handlers[ed->file_handler_count].fn = fn; - ed->file_handler_count++; - return ECEX_OK; -} - -int ecex_run_file_handlers(ecex_t *ed, buffer_t *buffer) { - const char *ext; - const char *path; - if (!ed || !buffer) return ECEX_ERR; - path = buffer->path ? buffer->path : buffer->name; - ext = ecex_path_extension(path); - if (!ext) return ECEX_OK; - for (size_t i = 0; i < ed->file_handler_count; ++i) { - if (ecex_ascii_equal_ci(ed->file_handlers[i].extension, ext)) { - return ed->file_handlers[i].fn(ed, buffer); - } - } - return ECEX_OK; +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) { @@ -612,50 +127,6 @@ int ecex_random_bounded(int bound) { return ecex_prng_next_bounded(&state, bound); } -int ecex_tetris_shape_cell(int piece, int rot, int col, int row) { - int p = piece % 7; - int r = rot & 3; - if (p < 0) p += 7; - if (col < 0 || col >= 4 || row < 0 || row >= 4) return 0; - - /* I */ - if (p == 0) { - if ((r & 1) == 0) return row == 1; - return col == 1; - } - /* O */ - if (p == 1) return (row == 1 || row == 2) && (col == 1 || col == 2); - /* T */ - if (p == 2) { - if (r == 0) return (row == 1 && col >= 0 && col <= 2) || (row == 2 && col == 1); - if (r == 1) return (col == 1 && row >= 0 && row <= 2) || (row == 1 && col == 2); - if (r == 2) return (row == 1 && col >= 0 && col <= 2) || (row == 0 && col == 1); - return (col == 1 && row >= 0 && row <= 2) || (row == 1 && col == 0); - } - /* S */ - if (p == 3) { - if ((r & 1) == 0) return (row == 1 && (col == 1 || col == 2)) || (row == 2 && (col == 0 || col == 1)); - return (col == 1 && (row == 0 || row == 1)) || (col == 2 && (row == 1 || row == 2)); - } - /* Z */ - if (p == 4) { - if ((r & 1) == 0) return (row == 1 && (col == 0 || col == 1)) || (row == 2 && (col == 1 || col == 2)); - return (col == 2 && (row == 0 || row == 1)) || (col == 1 && (row == 1 || row == 2)); - } - /* J */ - if (p == 5) { - if (r == 0) return (row == 1 && col >= 0 && col <= 2) || (row == 0 && col == 0); - if (r == 1) return (col == 1 && row >= 0 && row <= 2) || (row == 0 && col == 2); - if (r == 2) return (row == 1 && col >= 0 && col <= 2) || (row == 2 && col == 2); - return (col == 1 && row >= 0 && row <= 2) || (row == 2 && col == 0); - } - /* L */ - if (r == 0) return (row == 1 && col >= 0 && col <= 2) || (row == 0 && col == 2); - if (r == 1) return (col == 1 && row >= 0 && row <= 2) || (row == 2 && col == 2); - if (r == 2) return (row == 1 && col >= 0 && col <= 2) || (row == 2 && col == 0); - return (col == 1 && row >= 0 && row <= 2) || (row == 0 && col == 0); -} - static int ecex_file_browser_preview_current(ecex_t *ed); static int ecex_file_browser_update_preview_if_enabled(ecex_t *ed); @@ -664,6 +135,17 @@ static ecex_color_t ecex_color(float r, float g, float 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; @@ -826,8 +308,17 @@ 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; ed->theme.line_numbers_enabled = !ed->theme.line_numbers_enabled; return ECEX_OK; } -static int cmd_toggle_current_line(ecex_t *ed) { if (!ed) return ECEX_ERR; ed->theme.current_line_enabled = !ed->theme.current_line_enabled; return ECEX_OK; } +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; } @@ -886,6 +377,18 @@ 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 -------------------------------------------------- */ @@ -1309,6 +812,9 @@ static int ecex_register_builtins(ecex_t *ed) { 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); @@ -1344,32 +850,52 @@ static int ecex_register_builtins(ecex_t *ed) { 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"); @@ -1388,6 +914,8 @@ static int ecex_register_builtins(ecex_t *ed) { 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"); @@ -1402,9 +930,11 @@ static int ecex_register_builtins(ecex_t *ed) { 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, "c-mode"); ecex_define_major_mode(ed, "eval-output-mode"); ecex_define_major_mode(ed, "special-mode"); ecex_define_major_mode(ed, "file-browser-mode"); @@ -1442,6 +972,12 @@ 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); @@ -1490,10 +1026,18 @@ void ecex_free(ecex_t *ed) { 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); } @@ -1512,40 +1056,26 @@ void ecex_free(ecex_t *ed) { free(ed->major_modes[i].name); } - for (size_t i = 0; i < ed->var_count; i++) { - ecex_var_entry_clear(&ed->vars[i]); - } - - for (size_t i = 0; i < ed->text_count; i++) { - ecex_text_entry_clear(&ed->texts[i]); - } - - for (size_t i = 0; i < ed->file_handler_count; i++) { - free(ed->file_handlers[i].extension); - } - - for (size_t i = 0; i < ed->object_count; i++) { - free(ed->objects[i].ptr); - } - 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->vars); - free(ed->texts); - free(ed->file_handlers); - free(ed->objects); 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) { @@ -1593,6 +1123,7 @@ buffer_t *ecex_create_buffer(ecex_t *ed, return NULL; } + ecex_notify_buffer_hooks(ed, buffer, ECEX_BUFFER_HOOK_CREATE); return buffer; } @@ -1632,6 +1163,7 @@ static int ecex_set_current_buffer_index(ecex_t *ed, size_t 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; } @@ -1649,6 +1181,7 @@ int ecex_sync_current_buffer(ecex_t *ed) { 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; } } @@ -1865,6 +1398,7 @@ static int ecex_kill_buffer_impl(ecex_t *ed, const char *name, int force) { 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) { @@ -1961,6 +1495,75 @@ static void ecex_clear_mode_keybinds(ecex_t *ed) { 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]); + } + free(provider->words); + 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; @@ -1971,6 +1574,18 @@ typedef struct ecex_binding_snapshot { 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; @@ -2002,9 +1617,25 @@ static void ecex_snapshot_free(ecex_binding_snapshot_t *snap) { 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)); } @@ -2052,6 +1683,34 @@ static int ecex_snapshot_clone(ecex_binding_snapshot_t *snap, ecex_t *ed) { } } + 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: @@ -2065,9 +1724,17 @@ static void ecex_restore_snapshot(ecex_t *ed, ecex_binding_snapshot_t *snap) { 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; @@ -2079,6 +1746,18 @@ static void ecex_restore_snapshot(ecex_t *ed, ecex_binding_snapshot_t *snap) { 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)); @@ -2164,9 +1843,16 @@ int ecex_apply_theme(ecex_t *ed, const ecex_theme_t *theme) { 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; } @@ -2202,13 +1888,521 @@ int ecex_execute_command(ecex_t *ed, const char *name) { for (size_t i = 0; i < ed->command_count; i++) { if (strcmp(ed->commands[i].name, name) == 0) { - return ed->commands[i].fn(ed); + 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(ecex_t *ed, const char *name, const char *word) { + 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 **grown = realloc(provider->words, + (provider->word_count + 1) * sizeof(*provider->words)); + if (!grown) return ECEX_ERR; + provider->words = grown; + + provider->words[provider->word_count] = ecex_strdup(word); + if (!provider->words[provider->word_count]) return ECEX_ERR; + provider->word_count++; + return ECEX_OK; + } + + return ECEX_ERR; +} + +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_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, @@ -2220,13 +2414,37 @@ void ecex_set_clipboard_callbacks(ecex_t *ed, } const char *ecex_clipboard_get(ecex_t *ed) { - if (!ed || !ed->clipboard_get) return NULL; - return ed->clipboard_get(ed->clipboard_userdata); + 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 || !ed->clipboard_set || !text) return ECEX_ERR; - ed->clipboard_set(ed->clipboard_userdata, 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; } @@ -2384,7 +2602,12 @@ int ecex_buffer_set_major_mode_by_name(ecex_t *ed, buffer_t *buffer, const char if (!ed || !buffer || !name) return ECEX_ERR; int mode = ecex_define_major_mode(ed, name); if (!mode) return ECEX_ERR; - return ecex_buffer_set_major_mode(buffer, mode); + 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) { @@ -2511,18 +2734,134 @@ int ecex_key_sequence_has_prefix_for_buffer(ecex_t *ed, buffer_t *buffer, const 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"; - const char *path = buffer->path ? buffer->path : buffer->name; - const char *dot = path ? strrchr(path, '.') : NULL; - - if (dot && (strcmp(dot, ".c") == 0 || strcmp(dot, ".h") == 0 || - strcmp(dot, ".cc") == 0 || strcmp(dot, ".cpp") == 0 || - strcmp(dot, ".hpp") == 0)) { - name = "c-mode"; - } if (buffer_is_interactive(buffer) && buffer->name && buffer->name[0] == '*') { name = "special-mode"; @@ -2721,7 +3060,7 @@ int ecex_find_file(ecex_t *ed, const char *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_file_handlers(ed, ed->buffers[i]); + if (result == ECEX_OK && !ecex_buffer_has_renderer(ed->buffers[i])) ecex_run_plugin_file_handlers(ed, ed->buffers[i]); return result; } } @@ -2748,14 +3087,16 @@ int ecex_find_file(ecex_t *ed, const char *path) { 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_file_handlers(ed, buf); + 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; - return buffer_save(buf); + 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) { @@ -2765,7 +3106,10 @@ int ecex_write_current_buffer(ecex_t *ed, const char *path) { if (!normal_path) return ECEX_ERR; int result = buffer_save_as(buf, normal_path); free(normal_path); - if (result == ECEX_OK) ecex_auto_set_major_mode(ed, buf); + if (result == ECEX_OK) { + ecex_notify_buffer_hooks(ed, buf, ECEX_BUFFER_HOOK_SAVE); + ecex_auto_set_major_mode(ed, buf); + } return result; } @@ -2787,6 +3131,51 @@ void ecex_clear_prompt_request(ecex_t *ed) { 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; @@ -2835,6 +3224,743 @@ static int ecex_fuzzy_score(const char *candidate, const char *query) { 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; @@ -2854,6 +3980,263 @@ const char *ecex_complete_command(ecex_t *ed, const char *query) { 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; + ecex_completion_candidates_add(items, cap, count, provider->words[i], 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; @@ -3051,12 +4434,14 @@ int ecex_uncomment_region(ecex_t *ed) { 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; } @@ -3067,7 +4452,10 @@ float ecex_get_font_size(ecex_t *ed) { int ecex_set_font_size(ecex_t *ed, float size) { if (!ed) return ECEX_ERR; - ed->theme.font_size = ECEX_CLAMP(size, 8.0f, 96.0f); + 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; } @@ -3076,20 +4464,20 @@ int ecex_adjust_font_size(ecex_t *ed, float delta) { 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); } -void ecex_set_fg_color(ecex_t *ed, float r, float g, float b) { if (ed) ed->theme.fg = ecex_color(r, g, b); } -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); } -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); } -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); } -void ecex_set_cursor_color(ecex_t *ed, float r, float g, float b) { if (ed) ed->theme.cursor = ecex_color(r, g, b); } -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); } -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); } -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); } -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); } -void ecex_set_completion_enabled(ecex_t *ed, int enabled) { if (ed) ed->theme.completion_enabled = enabled ? 1 : 0; } -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); } -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); } -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); } -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); } -void ecex_set_line_numbers_enabled(ecex_t *ed, int enabled) { if (ed) ed->theme.line_numbers_enabled = enabled ? 1 : 0; } -void ecex_set_current_line_enabled(ecex_t *ed, int enabled) { if (ed) ed->theme.current_line_enabled = enabled ? 1 : 0; } +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); } } |
