aboutsummaryrefslogtreecommitdiff
path: root/src/ecex.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/ecex.c')
-rw-r--r--src/ecex.c2600
1 files changed, 1994 insertions, 606 deletions
diff --git a/src/ecex.c b/src/ecex.c
index 564d05f..7206b96 100644
--- a/src/ecex.c
+++ b/src/ecex.c
@@ -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); } }