aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Moc <personal@cdatgoose.org>2026-06-02 13:50:21 +0200
committerDavid Moc <personal@cdatgoose.org>2026-06-02 13:50:21 +0200
commita15cb041654ae307add0b998b526c87c3f42bf5f (patch)
tree225bb4b70e9fa05aa5f4d2722a1a9cf5fc6fca7f
parent6aeaa171dc1ca43392f53cbd02097f76e1b1c5a0 (diff)
Add plugin hooks and mode plugins
-rw-r--r--Makefile8
-rw-r--r--config/c_mode_plugin.c192
-rw-r--r--config/c_tools_plugin.c210
-rw-r--r--config/ecex_api_completion_plugin.c806
-rw-r--r--config/ecexrc.c17
-rw-r--r--config/markdown_plugin.c68
-rw-r--r--config/render_demo.c118
-rw-r--r--config/tetris.c188
-rw-r--r--config/which_key_plugin.c107
-rw-r--r--docs/ccdjit-improvements.md68
-rw-r--r--docs/plugin-api.md329
-rw-r--r--include/app.h7
-rw-r--r--include/buffers.h10
-rw-r--r--include/ccdjit.h262
-rw-r--r--include/ecex.h127
-rw-r--r--include/libccdjit.abin507906 -> 613206 bytes
-rwxr-xr-xinclude/libccdjit.sobin369144 -> 431632 bytes
-rw-r--r--include/plugin.h110
-rw-r--r--include/types.h138
-rw-r--r--src/app.c223
-rw-r--r--src/buffers.c129
-rw-r--r--src/config.c195
-rw-r--r--src/ecex.c2600
-rw-r--r--src/eval.c217
-rw-r--r--src/log.c345
-rw-r--r--src/main.c8
-rw-r--r--src/media.c74
-rw-r--r--src/plugin.c613
-rw-r--r--src/render.c488
-rw-r--r--tests/test_core.c88
30 files changed, 6639 insertions, 1106 deletions
diff --git a/Makefile b/Makefile
index 8fa967e..3a23f4f 100644
--- a/Makefile
+++ b/Makefile
@@ -3,11 +3,11 @@ CC = clang
STATIC_LIB = include/libccdjit.a
SHARED_LIB = include/libccdjit.so
-SRC = src/main.c src/app.c src/completion.c src/path.c src/media.c src/render.c src/font.c src/util.c src/buffers.c src/ecex.c src/config.c src/eval.c
+SRC = src/main.c src/app.c src/completion.c src/path.c src/media.c src/render.c src/font.c src/util.c src/log.c src/buffers.c src/ecex.c src/plugin.c src/config.c src/eval.c
BIN = bin/ecex
-PKG_CFLAGS = $(shell pkg-config --cflags glfw3)
-PKG_LIBS = $(shell pkg-config --libs glfw3)
+PKG_CFLAGS = $(shell pkg-config --cflags glfw3 2>/dev/null)
+PKG_LIBS = $(shell pkg-config --libs glfw3 2>/dev/null || echo -lglfw)
CFLAGS ?= -std=c11 -Wall -Wextra -pedantic -Iinclude $(PKG_CFLAGS)
LDLIBS = $(PKG_LIBS) -lGL -lm
@@ -39,7 +39,7 @@ sanitize: static
check:
@mkdir -p bin
- $(CC) -std=c11 -Wall -Wextra -pedantic -Iinclude tests/test_core.c src/buffers.c src/completion.c src/path.c src/util.c -o bin/ecex-tests
+ $(CC) -std=c11 -Wall -Wextra -pedantic -Iinclude tests/test_core.c src/buffers.c src/completion.c src/path.c src/util.c src/log.c src/plugin.c -o bin/ecex-tests
./bin/ecex-tests
clean:
diff --git a/config/c_mode_plugin.c b/config/c_mode_plugin.c
new file mode 100644
index 0000000..e707bca
--- /dev/null
+++ b/config/c_mode_plugin.c
@@ -0,0 +1,192 @@
+#include "ecex.h"
+
+#include <string.h>
+
+#define C_MODE_PLUGIN_ID "c-mode"
+#define C_MODE_LSP_PROVIDER_NAME "c-mode-clangd"
+#define C_MODE_SLOT_CLANGD "clangd"
+
+static int c_mode_line_starts_with(buffer_t *buffer, size_t first, size_t line_end, const char *word) {
+ size_t len = strlen(word);
+ if (!buffer || !word || first + len > line_end) return 0;
+ if (strncmp(buffer->data + first, word, len) != 0) return 0;
+ if (first + len == line_end) return 1;
+ char c = buffer->data[first + len];
+ return !(c == '_' ||
+ (c >= 'a' && c <= 'z') ||
+ (c >= 'A' && c <= 'Z') ||
+ (c >= '0' && c <= '9'));
+}
+
+static int c_mode_line_ends_with_colon(buffer_t *buffer, size_t first, size_t line_end) {
+ if (!buffer || first >= line_end) return 0;
+ size_t p = line_end;
+ while (p > first && (buffer->data[p - 1] == ' ' || buffer->data[p - 1] == '\t')) p--;
+ return p > first && buffer->data[p - 1] == ':';
+}
+
+static size_t c_mode_previous_nonblank_line(buffer_t *buffer, size_t line_start) {
+ if (!buffer || line_start == 0) return (size_t)-1;
+
+ size_t end = line_start - 1;
+ while (end > 0 && buffer->data[end - 1] == '\n') end--;
+
+ for (;;) {
+ size_t start = buffer_line_start_at(buffer, end);
+ size_t line_end = buffer_line_end_at(buffer, start);
+ size_t p = start;
+ while (p < line_end && (buffer->data[p] == ' ' || buffer->data[p] == '\t')) p++;
+ if (p < line_end) return start;
+ if (start == 0) break;
+ end = start - 1;
+ }
+
+ return (size_t)-1;
+}
+
+static int c_mode_brace_depth_before(buffer_t *buffer, size_t limit) {
+ int depth = 0;
+ int in_block = 0;
+ int in_line = 0;
+ int in_string = 0;
+ int in_char = 0;
+ int escaped = 0;
+
+ if (!buffer || !buffer->data) return 0;
+ if (limit > buffer->len) limit = buffer->len;
+
+ for (size_t i = 0; i < limit; i++) {
+ char c = buffer->data[i];
+ char next = i + 1 < limit ? buffer->data[i + 1] : '\0';
+
+ if (in_line) {
+ if (c == '\n') in_line = 0;
+ continue;
+ }
+
+ if (in_block) {
+ if (c == '*' && next == '/') {
+ in_block = 0;
+ i++;
+ }
+ continue;
+ }
+
+ if (in_string) {
+ if (escaped) escaped = 0;
+ else if (c == '\\') escaped = 1;
+ else if (c == '"') in_string = 0;
+ else if (c == '\n') in_string = 0;
+ continue;
+ }
+
+ if (in_char) {
+ if (escaped) escaped = 0;
+ else if (c == '\\') escaped = 1;
+ else if (c == '\'') in_char = 0;
+ else if (c == '\n') in_char = 0;
+ continue;
+ }
+
+ if (c == '/' && next == '/') {
+ in_line = 1;
+ i++;
+ } else if (c == '/' && next == '*') {
+ in_block = 1;
+ i++;
+ } else if (c == '"') {
+ in_string = 1;
+ } else if (c == '\'') {
+ in_char = 1;
+ } else if (c == '{') {
+ depth++;
+ } else if (c == '}') {
+ if (depth > 0) depth--;
+ }
+ }
+
+ return depth;
+}
+
+static int cmd_c_indent_line(ecex_t *ed) {
+ if (!ed) return -1;
+ buffer_t *buffer = ecex_current_buffer(ed);
+ if (!buffer || buffer->read_only || buffer_is_interactive(buffer)) return -1;
+
+ 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++;
+
+ if (first < line_end && buffer->data[first] == '#') return ecex_indent_line_to(buffer, 0);
+
+ int depth = c_mode_brace_depth_before(buffer, line_start);
+ int target = depth * 4;
+
+ if (first < line_end && buffer->data[first] == '}') target -= 4;
+
+ size_t prev = c_mode_previous_nonblank_line(buffer, line_start);
+ if (prev != (size_t)-1) {
+ size_t prev_end = buffer_line_end_at(buffer, prev);
+ size_t prev_first = prev;
+ while (prev_first < prev_end && (buffer->data[prev_first] == ' ' || buffer->data[prev_first] == '\t')) prev_first++;
+
+ if ((c_mode_line_starts_with(buffer, prev_first, prev_end, "case") ||
+ c_mode_line_starts_with(buffer, prev_first, prev_end, "default")) &&
+ c_mode_line_ends_with_colon(buffer, prev_first, prev_end) &&
+ !(c_mode_line_starts_with(buffer, first, line_end, "case") ||
+ c_mode_line_starts_with(buffer, first, line_end, "default") ||
+ (first < line_end && buffer->data[first] == '}'))) {
+ target += 4;
+ }
+ }
+
+ if (target < 0) target = 0;
+ return ecex_indent_line_to(buffer, target);
+}
+
+static int cmd_c_complete(ecex_t *ed) {
+ return ecex_complete_at_point(ed);
+}
+
+static int c_mode_file_handler(ecex_t *ed, buffer_t *buffer) {
+ if (!ed || !buffer) return -1;
+ return ecex_buffer_set_major_mode_by_name(ed, buffer, "c-mode");
+}
+
+ECEX_PLUGIN_BEGIN(ecex_c_mode_plugin, C_MODE_PLUGIN_ID)
+ if (!ecex_define_major_mode(ed, "c-mode")) return -1;
+
+ ECEX_CONFIG_COMMAND("c-indent-line", cmd_c_indent_line);
+ ECEX_CONFIG_COMMAND("c-complete", cmd_c_complete);
+ ECEX_CONFIG_TRY(ecex_bind_mode_key(ed, "c-mode", "TAB", "c-indent-line"));
+ ECEX_CONFIG_TRY(ecex_bind_mode_key(ed, "c-mode", "C-TAB", "c-complete"));
+ ECEX_CONFIG_TRY(ecex_bind_mode_key(ed, "c-mode", "C-S-TAB", "complete-at-point-previous"));
+
+ if (ecex_dependency_available("clangd")) {
+ ECEX_CONFIG_TRY(ecex_add_clangd_completion_provider(ed,
+ C_MODE_LSP_PROVIDER_NAME,
+ "c-mode"));
+ ecex_plugin_slot_i32_set_scalar(plugin, C_MODE_SLOT_CLANGD, 1);
+ } else {
+ ecex_plugin_slot_i32_set_scalar(plugin, C_MODE_SLOT_CLANGD, 0);
+ ecex_message(ed, "c-mode: clangd not found; C completion disabled");
+ }
+
+ ECEX_CONFIG_TRY(ecex_plugin_file_handler_register(plugin, ".c", c_mode_file_handler));
+ ECEX_CONFIG_TRY(ecex_plugin_file_handler_register(plugin, ".h", c_mode_file_handler));
+ ECEX_CONFIG_TRY(ecex_plugin_file_handler_register(plugin, ".cc", c_mode_file_handler));
+ ECEX_CONFIG_TRY(ecex_plugin_file_handler_register(plugin, ".cpp", c_mode_file_handler));
+ ECEX_CONFIG_TRY(ecex_plugin_file_handler_register(plugin, ".cxx", c_mode_file_handler));
+ ECEX_CONFIG_TRY(ecex_plugin_file_handler_register(plugin, ".hh", c_mode_file_handler));
+ ECEX_CONFIG_TRY(ecex_plugin_file_handler_register(plugin, ".hpp", c_mode_file_handler));
+ ECEX_CONFIG_TRY(ecex_plugin_file_handler_register(plugin, ".hxx", c_mode_file_handler));
+
+ return 0;
+ECEX_PLUGIN_END
+
+#ifndef ECEX_NO_STANDALONE_CONFIG
+ECEX_CONFIG_BEGIN
+ ECEX_CONFIG_INCLUDE(ecex_c_mode_plugin);
+ECEX_CONFIG_END
+#endif
diff --git a/config/c_tools_plugin.c b/config/c_tools_plugin.c
new file mode 100644
index 0000000..c3c075f
--- /dev/null
+++ b/config/c_tools_plugin.c
@@ -0,0 +1,210 @@
+#include "ecex.h"
+
+#include <string.h>
+
+#define C_TOOLS_PLUGIN_ID "c-tools"
+
+static int c_tools_append_char(char *out, int out_cap, int *used, char ch) {
+ if (!out || !used || out_cap <= 0 || *used < 0 || *used + 1 >= out_cap) return -1;
+ out[*used] = ch;
+ *used = *used + 1;
+ out[*used] = '\0';
+ return 0;
+}
+
+static int c_tools_append_text(char *out, int out_cap, int *used, const char *text) {
+ if (!text) text = "";
+ for (int i = 0; text[i]; i++) {
+ if (c_tools_append_char(out, out_cap, used, text[i]) != 0) return -1;
+ }
+ return 0;
+}
+
+static int c_tools_copy_text(char *out, int out_cap, const char *text) {
+ int used = 0;
+ if (!out || out_cap <= 0) return -1;
+ out[0] = '\0';
+ return c_tools_append_text(out, out_cap, &used, text);
+}
+
+static int c_tools_shell_quote(const char *input, char *out, int out_cap) {
+ int used = 0;
+ if (!input || !out || out_cap < 3) return -1;
+ out[0] = '\0';
+
+ if (c_tools_append_char(out, out_cap, &used, '\'') != 0) return -1;
+ for (int i = 0; input[i]; i++) {
+ if (input[i] == '\'') {
+ if (c_tools_append_text(out, out_cap, &used, "'\\''") != 0) return -1;
+ } else {
+ if (c_tools_append_char(out, out_cap, &used, input[i]) != 0) return -1;
+ }
+ }
+ if (c_tools_append_char(out, out_cap, &used, '\'') != 0) return -1;
+ return 0;
+}
+
+static buffer_t *c_tools_current_path_buffer(ecex_t *ed) {
+ buffer_t *buffer = ecex_current_buffer(ed);
+ if (!buffer || !buffer->path || !buffer->path[0]) {
+ ecex_message(ed, "C tooling needs a file-backed buffer");
+ return 0;
+ }
+ return buffer;
+}
+
+static int c_tools_ecex_include_flags(char *out, int out_cap, int force) {
+ if (!out || out_cap <= 0) return -1;
+ out[0] = '\0';
+
+ char cwd[4096];
+ if (ecex_path_cwd(cwd, (size_t)sizeof(cwd)) != 0 || !cwd[0]) {
+ if (c_tools_copy_text(cwd, (int)sizeof(cwd), ".") != 0) return -1;
+ }
+
+ char include_dir[8192];
+ int used = 0;
+ include_dir[0] = '\0';
+ if (c_tools_append_text(include_dir, (int)sizeof(include_dir), &used, cwd) != 0) return -1;
+ if (c_tools_append_text(include_dir, (int)sizeof(include_dir), &used, "/include") != 0) return -1;
+
+ char header[8192];
+ used = 0;
+ header[0] = '\0';
+ if (c_tools_append_text(header, (int)sizeof(header), &used, include_dir) != 0) return -1;
+ if (c_tools_append_text(header, (int)sizeof(header), &used, "/ecex.h") != 0) return -1;
+
+ if (!force && !ecex_path_exists(header)) return 0;
+
+ char quoted[8192];
+ if (c_tools_shell_quote(include_dir, quoted, (int)sizeof(quoted)) != 0) return -1;
+
+ used = 0;
+ if (c_tools_append_text(out, out_cap, &used, " -I") != 0) return -1;
+ if (c_tools_append_text(out, out_cap, &used, quoted) != 0) return -1;
+ return 0;
+}
+
+static int c_tools_command_lsp_check(const char *quoted, char *out, int out_cap) {
+ int used = 0;
+ if (!quoted || !out || out_cap <= 0) return -1;
+ out[0] = '\0';
+ if (c_tools_append_text(out, out_cap, &used, "clangd --check=") != 0) return -1;
+ if (c_tools_append_text(out, out_cap, &used, quoted) != 0) return -1;
+ return 0;
+}
+
+static int c_tools_command_lint(const char *quoted,
+ const char *include_flags,
+ char *out,
+ int out_cap) {
+ int used = 0;
+ if (!quoted || !include_flags || !out || out_cap <= 0) return -1;
+ out[0] = '\0';
+ if (c_tools_append_text(out, out_cap, &used, "${CC:-clang} -fsyntax-only -Wall -Wextra") != 0) return -1;
+ if (c_tools_append_text(out, out_cap, &used, include_flags) != 0) return -1;
+ if (c_tools_append_char(out, out_cap, &used, ' ') != 0) return -1;
+ if (c_tools_append_text(out, out_cap, &used, quoted) != 0) return -1;
+ return 0;
+}
+
+static int cmd_c_lsp_check(ecex_t *ed) {
+ buffer_t *buffer = c_tools_current_path_buffer(ed);
+ if (!buffer) return -1;
+
+ char quoted[4096];
+ char command[8192];
+ if (c_tools_shell_quote(buffer->path, quoted, (int)sizeof(quoted)) != 0) {
+ ecex_message(ed, "Could not quote C buffer path");
+ return -1;
+ }
+ if (c_tools_command_lsp_check(quoted, command, (int)sizeof(command)) != 0) {
+ ecex_message(ed, "Could not build clangd command");
+ return -1;
+ }
+
+ return ecex_compile(ed, command);
+}
+
+static int cmd_c_lint_buffer(ecex_t *ed) {
+ buffer_t *buffer = c_tools_current_path_buffer(ed);
+ if (!buffer) return -1;
+
+ char quoted[4096];
+ char include_flags[8192];
+ char command[12288];
+ if (c_tools_shell_quote(buffer->path, quoted, (int)sizeof(quoted)) != 0) {
+ ecex_message(ed, "Could not quote C buffer path");
+ return -1;
+ }
+ if (c_tools_ecex_include_flags(include_flags, (int)sizeof(include_flags), 0) != 0) {
+ ecex_message(ed, "Could not build ecex include flags");
+ return -1;
+ }
+ if (c_tools_command_lint(quoted, include_flags, command, (int)sizeof(command)) != 0) {
+ ecex_message(ed, "Could not build C lint command");
+ return -1;
+ }
+
+ return ecex_compile(ed, command);
+}
+
+static int cmd_c_ecex_lint_buffer(ecex_t *ed) {
+ buffer_t *buffer = c_tools_current_path_buffer(ed);
+ if (!buffer) return -1;
+
+ char quoted[4096];
+ char include_flags[8192];
+ char command[12288];
+ if (c_tools_shell_quote(buffer->path, quoted, (int)sizeof(quoted)) != 0) {
+ ecex_message(ed, "Could not quote C buffer path");
+ return -1;
+ }
+ if (c_tools_ecex_include_flags(include_flags, (int)sizeof(include_flags), 1) != 0) {
+ ecex_message(ed, "Could not build ecex include flags");
+ return -1;
+ }
+ if (c_tools_command_lint(quoted, include_flags, command, (int)sizeof(command)) != 0) {
+ ecex_message(ed, "Could not build C lint command");
+ return -1;
+ }
+
+ return ecex_compile(ed, command);
+}
+
+static int cmd_c_tools_status(ecex_t *ed) {
+ buffer_t *buffer = ecex_create_interactive_buffer(ed, "*c-tools*");
+ if (!buffer) return -1;
+
+ buffer_clear(buffer);
+ buffer_append(buffer, "C tools\n\n");
+ buffer_append(buffer, "C-x c l runs clangd --check for the current file.\n");
+ buffer_append(buffer, "C-x c k runs ${CC:-clang} -fsyntax-only -Wall -Wextra, adding ./include when ecex.h exists.\n");
+ buffer_append(buffer, "C-x c e runs the same lint with ./include forced for ecex development.\n");
+ buffer_append(buffer, "Both commands use the file on disk; save first for exact diagnostics.\n");
+ buffer->modified = 0;
+ return ecex_switch_buffer(ed, "*c-tools*");
+}
+
+ECEX_PLUGIN_BEGIN(ecex_c_tools_plugin, C_TOOLS_PLUGIN_ID)
+ if (ecex_plugin_require_dependency(ed, C_TOOLS_PLUGIN_ID, "clang") != 0) return 0;
+ if (ecex_plugin_require_dependency(ed, C_TOOLS_PLUGIN_ID, "clangd") != 0) return 0;
+
+ ECEX_CONFIG_COMMAND("c-lsp-check", cmd_c_lsp_check);
+ ECEX_CONFIG_COMMAND("c-lint-buffer", cmd_c_lint_buffer);
+ ECEX_CONFIG_COMMAND("c-ecex-lint-buffer", cmd_c_ecex_lint_buffer);
+ ECEX_CONFIG_COMMAND("c-tools-status", cmd_c_tools_status);
+
+ ECEX_CONFIG_TRY(ecex_bind_mode_key(ed, "c-mode", "C-x c l", "c-lsp-check"));
+ ECEX_CONFIG_TRY(ecex_bind_mode_key(ed, "c-mode", "C-x c k", "c-lint-buffer"));
+ ECEX_CONFIG_TRY(ecex_bind_mode_key(ed, "c-mode", "C-x c e", "c-ecex-lint-buffer"));
+ ECEX_CONFIG_TRY(ecex_bind_mode_key(ed, "c-mode", "C-x c s", "c-tools-status"));
+
+ return 0;
+ECEX_PLUGIN_END
+
+#ifndef ECEX_NO_STANDALONE_CONFIG
+ECEX_CONFIG_BEGIN
+ ECEX_CONFIG_INCLUDE(ecex_c_tools_plugin);
+ECEX_CONFIG_END
+#endif
diff --git a/config/ecex_api_completion_plugin.c b/config/ecex_api_completion_plugin.c
new file mode 100644
index 0000000..dae5745
--- /dev/null
+++ b/config/ecex_api_completion_plugin.c
@@ -0,0 +1,806 @@
+#include "ecex.h"
+
+#include <string.h>
+
+#define ECEX_API_PLUGIN_ID "ecex-mode"
+#define ECEX_API_SYMBOLS_PROVIDER "ecex-mode-symbols"
+#define ECEX_API_ED_FIELDS_PROVIDER "ecex-mode-ed-fields"
+#define ECEX_API_SLOT_ENABLED "enabled"
+#if defined(__GNUC__) || defined(__clang__)
+#define ECEX_API_UNUSED __attribute__((unused))
+#else
+#define ECEX_API_UNUSED
+#endif
+#define ECEX_API_SYMBOL_WORDS \
+ "ecex_t\n" \
+ "buffer_t\n" \
+ "ecex_window_t\n" \
+ "ecex_draw_context_t\n" \
+ "ecex_plugin_t\n" \
+ "ecex_plugin_runtime_t\n" \
+ "ecex_command_fn\n" \
+ "ecex_completion_provider_fn\n" \
+ "ecex_file_handler_fn\n" \
+ "ECEX_CONFIG_BEGIN\n" \
+ "ECEX_CONFIG_END\n" \
+ "ECEX_PLUGIN_BEGIN\n" \
+ "ECEX_PLUGIN_END\n" \
+ "ECEX_CONFIG_TRY\n" \
+ "ECEX_CONFIG_COMMAND\n" \
+ "ECEX_CONFIG_BIND\n" \
+ "ECEX_CONFIG_MODE\n" \
+ "ECEX_CONFIG_MODE_BIND\n" \
+ "ECEX_CONFIG_INCLUDE\n" \
+ "ECEX_RGB8\n" \
+ "ECEX_ARRAY_COUNT\n" \
+ "ecex_new\n" \
+ "ecex_free\n" \
+ "ecex_config_alloc\n" \
+ "ecex_config_calloc\n" \
+ "ecex_config_free\n" \
+ "ecex_time_seconds\n" \
+ "ecex_log\n" \
+ "ecex_logf\n" \
+ "ecex_log_group_begin\n" \
+ "ecex_log_group_end\n" \
+ "ecex_log_int\n" \
+ "ecex_log_double\n" \
+ "ecex_log_ptr\n" \
+ "ecex_log_flush\n" \
+ "ecex_mem_zero\n" \
+ "ecex_i32_get\n" \
+ "ecex_i32_set\n" \
+ "ecex_prng_next_bounded\n" \
+ "ecex_random_bounded\n" \
+ "ecex_plugin_register\n" \
+ "ecex_plugin_require\n" \
+ "ecex_plugin_find\n" \
+ "ecex_plugin_id\n" \
+ "ecex_plugin_slot_alloc\n" \
+ "ecex_plugin_slot_get\n" \
+ "ecex_plugin_slot_free\n" \
+ "ecex_plugin_slot_set_export_flags\n" \
+ "ecex_plugin_slot_read_exported\n" \
+ "ecex_plugin_slot_i32_get_scalar\n" \
+ "ecex_plugin_slot_i32_get\n" \
+ "ecex_plugin_slot_i32_set\n" \
+ "ecex_plugin_slot_i32_get_2d\n" \
+ "ecex_plugin_slot_i32_set_2d\n" \
+ "ecex_plugin_slot_i32_set_scalar\n" \
+ "ecex_plugin_object_alloc\n" \
+ "ecex_plugin_object_calloc\n" \
+ "ecex_plugin_object_free\n" \
+ "ecex_plugin_object_valid\n" \
+ "ecex_plugin_object_i32_get\n" \
+ "ecex_plugin_object_i32_set\n" \
+ "ecex_plugin_object_ptr_get\n" \
+ "ecex_plugin_object_ptr_set\n" \
+ "ecex_plugin_text_set\n" \
+ "ecex_plugin_text_set_from_buffer_title\n" \
+ "ecex_plugin_text_free\n" \
+ "ecex_plugin_text_free_all\n" \
+ "ecex_plugin_text_get_drawable\n" \
+ "ecex_plugin_file_handler_register\n" \
+ "ecex_plugin_file_handlers_run\n" \
+ "ecex_buffer_text_len\n" \
+ "ecex_buffer_scroll_line_index\n" \
+ "ecex_buffer_line_count_int\n" \
+ "ecex_buffer_line_copy_text\n" \
+ "ecex_run_plugin_file_handlers\n" \
+ "ecex_reserve_buffers\n" \
+ "ecex_add_buffer\n" \
+ "ecex_create_buffer\n" \
+ "ecex_find_buffer\n" \
+ "ecex_switch_buffer\n" \
+ "ecex_current_buffer\n" \
+ "ecex_other_buffer\n" \
+ "ecex_current_window\n" \
+ "ecex_window_count\n" \
+ "ecex_sync_current_buffer\n" \
+ "ecex_split_window_vertically\n" \
+ "ecex_split_window_horizontally\n" \
+ "ecex_other_window\n" \
+ "ecex_previous_window\n" \
+ "ecex_delete_window\n" \
+ "ecex_delete_other_windows\n" \
+ "ecex_balance_windows\n" \
+ "ecex_next_buffer\n" \
+ "ecex_previous_buffer\n" \
+ "ecex_kill_buffer\n" \
+ "ecex_kill_buffer_force\n" \
+ "ecex_has_modified_buffers\n" \
+ "ecex_validate_bindings\n" \
+ "ecex_keep_jit_module\n" \
+ "ecex_set_config_path\n" \
+ "ecex_config_path\n" \
+ "ecex_reload_config\n" \
+ "ecex_config_register_commands\n" \
+ "ecex_config_bind_keys\n" \
+ "ecex_config_bind_mode_keys\n" \
+ "ecex_config_define_modes\n" \
+ "ecex_apply_theme\n" \
+ "ecex_register_command\n" \
+ "ecex_execute_command\n" \
+ "ecex_add_command_hook\n" \
+ "ecex_remove_command_hook\n" \
+ "ecex_add_prefix_hook\n" \
+ "ecex_remove_prefix_hook\n" \
+ "ecex_notify_prefix_hooks\n" \
+ "ecex_add_buffer_hook\n" \
+ "ecex_remove_buffer_hook\n" \
+ "ecex_notify_buffer_hooks\n" \
+ "ecex_message\n" \
+ "ecex_dependency_available\n" \
+ "ecex_plugin_require_dependency\n" \
+ "ecex_add_completion_provider\n" \
+ "ecex_add_word_completion_provider\n" \
+ "ecex_define_word_completion_provider\n" \
+ "ecex_completion_provider_add_word\n" \
+ "ecex_completion_provider_add_words\n" \
+ "ecex_completion_provider_set_detail\n" \
+ "ecex_add_clangd_completion_provider\n" \
+ "ecex_remove_completion_provider\n" \
+ "ecex_buffer_identifier_prefix\n" \
+ "ecex_complete_at_point\n" \
+ "ecex_set_clipboard_callbacks\n" \
+ "ecex_clipboard_get\n" \
+ "ecex_clipboard_set\n" \
+ "ecex_bind_key\n" \
+ "ecex_lookup_key\n" \
+ "ecex_define_major_mode\n" \
+ "ecex_major_mode_by_name\n" \
+ "ecex_major_mode_name\n" \
+ "ecex_buffer_set_major_mode\n" \
+ "ecex_buffer_set_major_mode_by_name\n" \
+ "ecex_buffer_major_mode_name\n" \
+ "ecex_bind_mode_key\n" \
+ "ecex_lookup_key_for_buffer\n" \
+ "ecex_key_sequence_has_prefix_for_buffer\n" \
+ "ecex_describe_key_prefix\n" \
+ "ecex_auto_set_major_mode\n" \
+ "ecex_list_commands\n" \
+ "ecex_list_buffers\n" \
+ "ecex_create_interactive_buffer\n" \
+ "ecex_interactive_append_line\n" \
+ "ecex_interactive_activate_current_line\n" \
+ "ecex_buffer_set_renderer\n" \
+ "ecex_buffer_clear_renderer\n" \
+ "ecex_buffer_has_renderer\n" \
+ "ecex_buffer_renderer_userdata\n" \
+ "ecex_buffer_set_mouse_handler\n" \
+ "ecex_buffer_clear_mouse_handler\n" \
+ "ecex_buffer_has_mouse_handler\n" \
+ "ecex_buffer_mouse_userdata\n" \
+ "ecex_buffer_set_animation\n" \
+ "ecex_buffer_set_animation_ms\n" \
+ "ecex_buffer_clear_animation\n" \
+ "ecex_buffer_is_animating\n" \
+ "ecex_buffer_animation_userdata\n" \
+ "ecex_tick_animations\n" \
+ "ecex_buffer_replace_text\n" \
+ "ecex_buffer_set_modified\n" \
+ "ecex_draw_set_color\n" \
+ "ecex_draw_rect\n" \
+ "ecex_draw_rect_outline\n" \
+ "ecex_draw_line\n" \
+ "ecex_draw_text\n" \
+ "ecex_draw_text_aligned\n" \
+ "ecex_draw_text_width\n" \
+ "ecex_draw_rgba\n" \
+ "ecex_draw_color_rgba8_i\n" \
+ "ecex_draw_rect_i\n" \
+ "ecex_draw_rect_outline_i\n" \
+ "ecex_draw_line_i\n" \
+ "ecex_draw_text_i\n" \
+ "ecex_draw_plugin_text_i\n" \
+ "ecex_draw_plugin_text_rect_i\n" \
+ "ecex_draw_markdown_canvas_i\n" \
+ "ecex_draw_markdown_text_i\n" \
+ "ecex_draw_markdown_canvas_auto_i\n" \
+ "ecex_draw_markdown_line_auto_i\n" \
+ "ecex_draw_label_i\n" \
+ "ecex_draw_stat_i\n" \
+ "ecex_draw_tetris_preview_i\n" \
+ "ecex_find_file\n" \
+ "ecex_save_current_buffer\n" \
+ "ecex_write_current_buffer\n" \
+ "ecex_compile\n" \
+ "ecex_grep\n" \
+ "ecex_rerun_compile\n" \
+ "ecex_rerun_grep\n" \
+ "ecex_next_interactive_action\n" \
+ "ecex_previous_interactive_action\n" \
+ "ecex_indent_line_to\n" \
+ "ecex_comment_region\n" \
+ "ecex_uncomment_region\n" \
+ "ecex_request_prompt\n" \
+ "ecex_clear_prompt_request\n" \
+ "ecex_complete_command\n" \
+ "ecex_set_font\n" \
+ "ecex_get_font_size\n" \
+ "ecex_set_font_size\n" \
+ "ecex_adjust_font_size\n" \
+ "ecex_set_bg_color\n" \
+ "ecex_set_fg_color\n" \
+ "ecex_set_status_bg_color\n" \
+ "ecex_set_status_fg_color\n" \
+ "ecex_set_status_border_color\n" \
+ "ecex_set_cursor_color\n" \
+ "ecex_set_region_bg_color\n" \
+ "ecex_set_minibuffer_bg_color\n" \
+ "ecex_set_minibuffer_fg_color\n" \
+ "ecex_set_completion_fg_color\n" \
+ "ecex_set_completion_enabled\n" \
+ "ecex_set_interactive_highlight_bg_color\n" \
+ "ecex_set_interactive_highlight_fg_color\n" \
+ "ecex_set_current_line_bg_color\n" \
+ "ecex_set_search_bg_color\n" \
+ "ecex_set_line_numbers_enabled\n" \
+ "ecex_set_current_line_enabled\n" \
+ "buffer_new\n" \
+ "buffer_free\n" \
+ "buffer_reserve\n" \
+ "buffer_clear\n" \
+ "buffer_set_text\n" \
+ "buffer_insert\n" \
+ "buffer_append\n" \
+ "buffer_prepend\n" \
+ "buffer_insert_at\n" \
+ "buffer_insert_char\n" \
+ "buffer_delete_range\n" \
+ "buffer_delete_selection\n" \
+ "buffer_replace_selection\n" \
+ "buffer_backspace\n" \
+ "buffer_delete_forward\n" \
+ "buffer_kill_line\n" \
+ "buffer_undo\n" \
+ "buffer_redo\n" \
+ "buffer_clear_undo\n" \
+ "buffer_set_point\n" \
+ "buffer_move_left\n" \
+ "buffer_move_right\n" \
+ "buffer_move_up\n" \
+ "buffer_move_down\n" \
+ "buffer_move_word_left\n" \
+ "buffer_move_word_right\n" \
+ "buffer_move_beginning_of_line\n" \
+ "buffer_move_end_of_line\n" \
+ "buffer_move_beginning_of_buffer\n" \
+ "buffer_move_end_of_buffer\n" \
+ "buffer_search_forward\n" \
+ "buffer_search_backward\n" \
+ "buffer_set_mark\n" \
+ "buffer_clear_mark\n" \
+ "buffer_has_selection\n" \
+ "buffer_selection_range\n" \
+ "buffer_line_start_at\n" \
+ "buffer_line_end_at\n" \
+ "buffer_current_line_start\n" \
+ "buffer_current_line_end\n" \
+ "buffer_current_column\n" \
+ "buffer_current_line_number\n" \
+ "buffer_line_count\n" \
+ "buffer_substring\n" \
+ "buffer_current_line_copy\n" \
+ "buffer_load_file\n" \
+ "buffer_save\n" \
+ "buffer_save_as\n" \
+ "buffer_set_interactive\n" \
+ "buffer_is_interactive\n" \
+ "buffer_clear_interactive_actions\n" \
+ "buffer_add_interactive_action\n" \
+ "buffer_interactive_action_at_line\n" \
+ "ecex_path_copy\n" \
+ "ecex_path_expand_user\n" \
+ "ecex_path_join\n" \
+ "ecex_path_dirname\n" \
+ "ecex_path_basename_dup\n" \
+ "ecex_path_normalize\n" \
+ "ecex_path_is_dir\n" \
+ "ecex_path_is_file\n" \
+ "ecex_path_exists\n" \
+ "ecex_path_file_size\n" \
+ "ecex_path_is_image\n" \
+ "ecex_path_is_previewable_image\n" \
+ "ecex_path_is_video\n" \
+ "ecex_path_is_media\n" \
+ "ecex_path_cwd\n" \
+ "ecex_media_open\n" \
+ "ecex_media_toggle_playback\n"
+#define ECEX_API_ED_FIELD_WORDS \
+ "buffers\n" \
+ "buffer_cap\n" \
+ "buffer_count\n" \
+ "current_buffer_index\n" \
+ "current_buffer\n" \
+ "previous_buffer\n" \
+ "windows\n" \
+ "window_cap\n" \
+ "window_count\n" \
+ "current_window_index\n" \
+ "jit_modules\n" \
+ "jit_module_cap\n" \
+ "jit_module_count\n" \
+ "commands\n" \
+ "command_cap\n" \
+ "command_count\n" \
+ "keybinds\n" \
+ "keybind_cap\n" \
+ "keybind_count\n" \
+ "mode_keybinds\n" \
+ "mode_keybind_cap\n" \
+ "mode_keybind_count\n" \
+ "command_hooks\n" \
+ "command_hook_cap\n" \
+ "command_hook_count\n" \
+ "prefix_hooks\n" \
+ "prefix_hook_cap\n" \
+ "prefix_hook_count\n" \
+ "buffer_hooks\n" \
+ "buffer_hook_cap\n" \
+ "buffer_hook_count\n" \
+ "completion_providers\n" \
+ "completion_provider_cap\n" \
+ "completion_provider_count\n" \
+ "major_modes\n" \
+ "major_mode_cap\n" \
+ "major_mode_count\n" \
+ "next_major_mode_id\n" \
+ "plugins\n" \
+ "last_eval_source\n" \
+ "last_eval_filename\n" \
+ "last_compile_command\n" \
+ "last_grep_command\n" \
+ "prompt_request\n" \
+ "prompt_message\n" \
+ "config_path\n" \
+ "message\n" \
+ "message_revision\n" \
+ "clipboard_get\n" \
+ "clipboard_set\n" \
+ "clipboard_userdata\n" \
+ "clipboard_text\n" \
+ "should_quit\n" \
+ "ui_revision\n" \
+ "font_revision\n" \
+ "theme\n"
+
+static const char *ecex_api_symbols[] ECEX_API_UNUSED = {
+ "ecex_t",
+ "buffer_t",
+ "ecex_window_t",
+ "ecex_draw_context_t",
+ "ecex_plugin_t",
+ "ecex_plugin_runtime_t",
+ "ecex_command_fn",
+ "ecex_completion_provider_fn",
+ "ecex_file_handler_fn",
+ "ECEX_CONFIG_BEGIN",
+ "ECEX_CONFIG_END",
+ "ECEX_PLUGIN_BEGIN",
+ "ECEX_PLUGIN_END",
+ "ECEX_CONFIG_TRY",
+ "ECEX_CONFIG_COMMAND",
+ "ECEX_CONFIG_BIND",
+ "ECEX_CONFIG_MODE",
+ "ECEX_CONFIG_MODE_BIND",
+ "ECEX_CONFIG_INCLUDE",
+ "ECEX_RGB8",
+ "ECEX_ARRAY_COUNT",
+ "ecex_new",
+ "ecex_free",
+ "ecex_config_alloc",
+ "ecex_config_calloc",
+ "ecex_config_free",
+ "ecex_time_seconds",
+ "ecex_log",
+ "ecex_logf",
+ "ecex_log_group_begin",
+ "ecex_log_group_end",
+ "ecex_log_int",
+ "ecex_log_double",
+ "ecex_log_ptr",
+ "ecex_log_flush",
+ "ecex_mem_zero",
+ "ecex_i32_get",
+ "ecex_i32_set",
+ "ecex_prng_next_bounded",
+ "ecex_random_bounded",
+ "ecex_plugin_register",
+ "ecex_plugin_require",
+ "ecex_plugin_find",
+ "ecex_plugin_id",
+ "ecex_plugin_slot_alloc",
+ "ecex_plugin_slot_get",
+ "ecex_plugin_slot_free",
+ "ecex_plugin_slot_set_export_flags",
+ "ecex_plugin_slot_read_exported",
+ "ecex_plugin_slot_i32_get_scalar",
+ "ecex_plugin_slot_i32_get",
+ "ecex_plugin_slot_i32_set",
+ "ecex_plugin_slot_i32_get_2d",
+ "ecex_plugin_slot_i32_set_2d",
+ "ecex_plugin_slot_i32_set_scalar",
+ "ecex_plugin_object_alloc",
+ "ecex_plugin_object_calloc",
+ "ecex_plugin_object_free",
+ "ecex_plugin_object_valid",
+ "ecex_plugin_object_i32_get",
+ "ecex_plugin_object_i32_set",
+ "ecex_plugin_object_ptr_get",
+ "ecex_plugin_object_ptr_set",
+ "ecex_plugin_text_set",
+ "ecex_plugin_text_set_from_buffer_title",
+ "ecex_plugin_text_free",
+ "ecex_plugin_text_free_all",
+ "ecex_plugin_text_get_drawable",
+ "ecex_plugin_file_handler_register",
+ "ecex_plugin_file_handlers_run",
+ "ecex_buffer_text_len",
+ "ecex_buffer_scroll_line_index",
+ "ecex_buffer_line_count_int",
+ "ecex_buffer_line_copy_text",
+ "ecex_run_plugin_file_handlers",
+ "ecex_reserve_buffers",
+ "ecex_add_buffer",
+ "ecex_create_buffer",
+ "ecex_find_buffer",
+ "ecex_switch_buffer",
+ "ecex_current_buffer",
+ "ecex_other_buffer",
+ "ecex_current_window",
+ "ecex_window_count",
+ "ecex_sync_current_buffer",
+ "ecex_split_window_vertically",
+ "ecex_split_window_horizontally",
+ "ecex_other_window",
+ "ecex_previous_window",
+ "ecex_delete_window",
+ "ecex_delete_other_windows",
+ "ecex_balance_windows",
+ "ecex_next_buffer",
+ "ecex_previous_buffer",
+ "ecex_kill_buffer",
+ "ecex_kill_buffer_force",
+ "ecex_has_modified_buffers",
+ "ecex_validate_bindings",
+ "ecex_keep_jit_module",
+ "ecex_set_config_path",
+ "ecex_config_path",
+ "ecex_reload_config",
+ "ecex_config_register_commands",
+ "ecex_config_bind_keys",
+ "ecex_config_bind_mode_keys",
+ "ecex_config_define_modes",
+ "ecex_apply_theme",
+ "ecex_register_command",
+ "ecex_execute_command",
+ "ecex_add_command_hook",
+ "ecex_remove_command_hook",
+ "ecex_add_prefix_hook",
+ "ecex_remove_prefix_hook",
+ "ecex_notify_prefix_hooks",
+ "ecex_message",
+ "ecex_add_completion_provider",
+ "ecex_remove_completion_provider",
+ "ecex_add_clangd_completion_provider",
+ "ecex_buffer_identifier_prefix",
+ "ecex_complete_at_point",
+ "ecex_set_clipboard_callbacks",
+ "ecex_clipboard_get",
+ "ecex_clipboard_set",
+ "ecex_bind_key",
+ "ecex_lookup_key",
+ "ecex_define_major_mode",
+ "ecex_major_mode_by_name",
+ "ecex_major_mode_name",
+ "ecex_buffer_set_major_mode",
+ "ecex_buffer_set_major_mode_by_name",
+ "ecex_buffer_major_mode_name",
+ "ecex_bind_mode_key",
+ "ecex_lookup_key_for_buffer",
+ "ecex_key_sequence_has_prefix_for_buffer",
+ "ecex_describe_key_prefix",
+ "ecex_auto_set_major_mode",
+ "ecex_list_commands",
+ "ecex_list_buffers",
+ "ecex_create_interactive_buffer",
+ "ecex_interactive_append_line",
+ "ecex_interactive_activate_current_line",
+ "ecex_buffer_set_renderer",
+ "ecex_buffer_clear_renderer",
+ "ecex_buffer_has_renderer",
+ "ecex_buffer_renderer_userdata",
+ "ecex_buffer_set_mouse_handler",
+ "ecex_buffer_clear_mouse_handler",
+ "ecex_buffer_has_mouse_handler",
+ "ecex_buffer_mouse_userdata",
+ "ecex_buffer_set_animation",
+ "ecex_buffer_set_animation_ms",
+ "ecex_buffer_clear_animation",
+ "ecex_buffer_is_animating",
+ "ecex_buffer_animation_userdata",
+ "ecex_tick_animations",
+ "ecex_buffer_replace_text",
+ "ecex_buffer_set_modified",
+ "ecex_draw_set_color",
+ "ecex_draw_rect",
+ "ecex_draw_rect_outline",
+ "ecex_draw_line",
+ "ecex_draw_text",
+ "ecex_draw_text_aligned",
+ "ecex_draw_text_width",
+ "ecex_draw_rgba",
+ "ecex_draw_color_rgba8_i",
+ "ecex_draw_rect_i",
+ "ecex_draw_rect_outline_i",
+ "ecex_draw_line_i",
+ "ecex_draw_text_i",
+ "ecex_draw_plugin_text_i",
+ "ecex_draw_plugin_text_rect_i",
+ "ecex_draw_markdown_canvas_i",
+ "ecex_draw_markdown_text_i",
+ "ecex_draw_markdown_canvas_auto_i",
+ "ecex_draw_markdown_line_auto_i",
+ "ecex_draw_label_i",
+ "ecex_draw_stat_i",
+ "ecex_draw_tetris_preview_i",
+ "ecex_find_file",
+ "ecex_save_current_buffer",
+ "ecex_write_current_buffer",
+ "ecex_compile",
+ "ecex_grep",
+ "ecex_rerun_compile",
+ "ecex_rerun_grep",
+ "ecex_next_interactive_action",
+ "ecex_previous_interactive_action",
+ "ecex_comment_region",
+ "ecex_uncomment_region",
+ "ecex_request_prompt",
+ "ecex_clear_prompt_request",
+ "ecex_complete_command",
+ "ecex_set_font",
+ "ecex_get_font_size",
+ "ecex_set_font_size",
+ "ecex_adjust_font_size",
+ "ecex_set_bg_color",
+ "ecex_set_fg_color",
+ "ecex_set_status_bg_color",
+ "ecex_set_status_fg_color",
+ "ecex_set_status_border_color",
+ "ecex_set_cursor_color",
+ "ecex_set_region_bg_color",
+ "ecex_set_minibuffer_bg_color",
+ "ecex_set_minibuffer_fg_color",
+ "ecex_set_completion_fg_color",
+ "ecex_set_completion_enabled",
+ "ecex_set_interactive_highlight_bg_color",
+ "ecex_set_interactive_highlight_fg_color",
+ "ecex_set_current_line_bg_color",
+ "ecex_set_search_bg_color",
+ "ecex_set_line_numbers_enabled",
+ "ecex_set_current_line_enabled",
+ "buffer_new",
+ "buffer_free",
+ "buffer_reserve",
+ "buffer_clear",
+ "buffer_set_text",
+ "buffer_insert",
+ "buffer_append",
+ "buffer_prepend",
+ "buffer_insert_at",
+ "buffer_insert_char",
+ "buffer_delete_range",
+ "buffer_delete_selection",
+ "buffer_replace_selection",
+ "buffer_backspace",
+ "buffer_delete_forward",
+ "buffer_kill_line",
+ "buffer_undo",
+ "buffer_redo",
+ "buffer_clear_undo",
+ "buffer_set_point",
+ "buffer_move_left",
+ "buffer_move_right",
+ "buffer_move_up",
+ "buffer_move_down",
+ "buffer_move_word_left",
+ "buffer_move_word_right",
+ "buffer_move_beginning_of_line",
+ "buffer_move_end_of_line",
+ "buffer_move_beginning_of_buffer",
+ "buffer_move_end_of_buffer",
+ "buffer_search_forward",
+ "buffer_search_backward",
+ "buffer_set_mark",
+ "buffer_clear_mark",
+ "buffer_has_selection",
+ "buffer_selection_range",
+ "buffer_line_start_at",
+ "buffer_line_end_at",
+ "buffer_current_line_start",
+ "buffer_current_line_end",
+ "buffer_current_column",
+ "buffer_current_line_number",
+ "buffer_line_count",
+ "buffer_substring",
+ "buffer_current_line_copy",
+ "buffer_load_file",
+ "buffer_save",
+ "buffer_save_as",
+ "buffer_set_interactive",
+ "buffer_is_interactive",
+ "buffer_clear_interactive_actions",
+ "buffer_add_interactive_action",
+ "buffer_interactive_action_at_line",
+ "ecex_path_copy",
+ "ecex_path_expand_user",
+ "ecex_path_join",
+ "ecex_path_dirname",
+ "ecex_path_basename_dup",
+ "ecex_path_normalize",
+ "ecex_path_is_dir",
+ "ecex_path_is_file",
+ "ecex_path_exists",
+ "ecex_path_file_size",
+ "ecex_path_is_image",
+ "ecex_path_is_previewable_image",
+ "ecex_path_is_video",
+ "ecex_path_is_media",
+ "ecex_path_cwd",
+ "ecex_media_open",
+ "ecex_media_toggle_playback"
+};
+
+static const char *ecex_ed_fields[] ECEX_API_UNUSED = {
+ "buffers",
+ "buffer_cap",
+ "buffer_count",
+ "current_buffer_index",
+ "current_buffer",
+ "previous_buffer",
+ "windows",
+ "window_cap",
+ "window_count",
+ "current_window_index",
+ "jit_modules",
+ "jit_module_cap",
+ "jit_module_count",
+ "commands",
+ "command_cap",
+ "command_count",
+ "keybinds",
+ "keybind_cap",
+ "keybind_count",
+ "mode_keybinds",
+ "mode_keybind_cap",
+ "mode_keybind_count",
+ "command_hooks",
+ "command_hook_cap",
+ "command_hook_count",
+ "prefix_hooks",
+ "prefix_hook_cap",
+ "prefix_hook_count",
+ "completion_providers",
+ "completion_provider_cap",
+ "completion_provider_count",
+ "major_modes",
+ "major_mode_cap",
+ "major_mode_count",
+ "next_major_mode_id",
+ "plugins",
+ "last_eval_source",
+ "last_eval_filename",
+ "last_compile_command",
+ "last_grep_command",
+ "prompt_request",
+ "prompt_message",
+ "config_path",
+ "message",
+ "message_revision",
+ "clipboard_get",
+ "clipboard_set",
+ "clipboard_userdata",
+ "clipboard_text",
+ "should_quit",
+ "ui_revision",
+ "font_revision",
+ "theme"
+};
+
+static int ecex_api_enabled(ecex_plugin_t *plugin) {
+ return ecex_plugin_slot_i32_get_scalar(plugin, ECEX_API_SLOT_ENABLED, 1) != 0;
+}
+
+static int ecex_api_register_providers(ecex_t *ed) {
+ if (ecex_define_word_completion_provider(ed,
+ ECEX_API_SYMBOLS_PROVIDER,
+ 0,
+ ECEX_COMPLETION_DEFAULT) != 0) {
+ return -1;
+ }
+
+ if (ecex_completion_provider_add_words(ed, ECEX_API_SYMBOLS_PROVIDER, ECEX_API_SYMBOL_WORDS) != 0) {
+ ecex_remove_completion_provider(ed, ECEX_API_SYMBOLS_PROVIDER);
+ return -1;
+ }
+ if (ecex_completion_provider_set_detail(ed, ECEX_API_SYMBOLS_PROVIDER, "ecex API symbol") != 0) {
+ ecex_remove_completion_provider(ed, ECEX_API_SYMBOLS_PROVIDER);
+ return -1;
+ }
+
+ if (ecex_define_word_completion_provider(ed,
+ ECEX_API_ED_FIELDS_PROVIDER,
+ 0,
+ ECEX_COMPLETION_ED_ARROW) != 0) {
+ ecex_remove_completion_provider(ed, ECEX_API_SYMBOLS_PROVIDER);
+ return -1;
+ }
+
+ if (ecex_completion_provider_add_words(ed, ECEX_API_ED_FIELDS_PROVIDER, ECEX_API_ED_FIELD_WORDS) != 0) {
+ ecex_remove_completion_provider(ed, ECEX_API_SYMBOLS_PROVIDER);
+ ecex_remove_completion_provider(ed, ECEX_API_ED_FIELDS_PROVIDER);
+ return -1;
+ }
+ if (ecex_completion_provider_set_detail(ed, ECEX_API_ED_FIELDS_PROVIDER, "ecex_t field") != 0) {
+ ecex_remove_completion_provider(ed, ECEX_API_SYMBOLS_PROVIDER);
+ ecex_remove_completion_provider(ed, ECEX_API_ED_FIELDS_PROVIDER);
+ return -1;
+ }
+
+ return 0;
+}
+
+static void ecex_api_remove_providers(ecex_t *ed) {
+ ecex_remove_completion_provider(ed, ECEX_API_SYMBOLS_PROVIDER);
+ ecex_remove_completion_provider(ed, ECEX_API_ED_FIELDS_PROVIDER);
+}
+
+static int cmd_ecex_api_complete(ecex_t *ed) {
+ return ecex_complete_at_point(ed);
+}
+
+static int cmd_ecex_mode(ecex_t *ed) {
+ buffer_t *buffer = ecex_current_buffer(ed);
+ if (!buffer) return -1;
+ return ecex_buffer_set_major_mode_by_name(ed, buffer, "ecex-mode");
+}
+
+static int cmd_ecex_api_completion_mode(ecex_t *ed) {
+ ecex_plugin_t *plugin = ecex_plugin_find(ed, ECEX_API_PLUGIN_ID);
+ if (!plugin) return -1;
+
+ int enabled = !ecex_api_enabled(plugin);
+ ecex_plugin_slot_i32_set_scalar(plugin, ECEX_API_SLOT_ENABLED, enabled);
+ if (enabled) {
+ if (ecex_api_register_providers(ed) != 0) return -1;
+ } else {
+ ecex_api_remove_providers(ed);
+ }
+ ecex_message(ed, enabled ? "ecex API completion enabled" : "ecex API completion disabled");
+ return 0;
+}
+
+ECEX_PLUGIN_BEGIN(ecex_api_completion_plugin, ECEX_API_PLUGIN_ID)
+ if (!ecex_define_major_mode(ed, "ecex-mode")) return -1;
+
+ if (ecex_plugin_slot_i32_get_scalar(plugin, ECEX_API_SLOT_ENABLED, -1) < 0) {
+ ecex_plugin_slot_i32_set_scalar(plugin, ECEX_API_SLOT_ENABLED, 1);
+ }
+
+ ECEX_CONFIG_COMMAND("ecex-mode", cmd_ecex_mode);
+ ECEX_CONFIG_COMMAND("ecex-mode-complete", cmd_ecex_api_complete);
+ ECEX_CONFIG_COMMAND("ecex-mode-completion-mode", cmd_ecex_api_completion_mode);
+ ECEX_CONFIG_COMMAND("ecex-api-complete", cmd_ecex_api_complete);
+ ECEX_CONFIG_COMMAND("ecex-api-completion-mode", cmd_ecex_api_completion_mode);
+ if (ecex_api_enabled(plugin)) {
+ ECEX_CONFIG_TRY(ecex_api_register_providers(ed));
+ }
+ return 0;
+ECEX_PLUGIN_END
+
+#ifndef ECEX_NO_STANDALONE_CONFIG
+ECEX_CONFIG_BEGIN
+ ECEX_CONFIG_INCLUDE(ecex_api_completion_plugin);
+ECEX_CONFIG_END
+#endif
diff --git a/config/ecexrc.c b/config/ecexrc.c
index 1d13f18..75f2470 100644
--- a/config/ecexrc.c
+++ b/config/ecexrc.c
@@ -5,6 +5,10 @@
#include "render_demo.c"
#include "tetris.c"
#include "markdown_plugin.c"
+#include "which_key_plugin.c"
+#include "c_mode_plugin.c"
+#include "ecex_api_completion_plugin.c"
+#include "c_tools_plugin.c"
#undef ECEX_NO_STANDALONE_CONFIG
#define RGB(r, g, b) ECEX_RGB8(r, g, b)
@@ -141,11 +145,22 @@ ECEX_CONFIG_BEGIN
ECEX_CONFIG_INCLUDE(ecex_render_demo_plugin);
ECEX_CONFIG_INCLUDE(ecex_tetris_plugin);
ECEX_CONFIG_INCLUDE(ecex_markdown_plugin);
+ ECEX_CONFIG_INCLUDE(ecex_which_key_plugin);
+ ECEX_CONFIG_INCLUDE(ecex_c_mode_plugin);
+ ECEX_CONFIG_INCLUDE(ecex_api_completion_plugin);
+ if (ecex_plugin_require_dependency(ed, "c-tools", "clang") == 0 &&
+ ecex_plugin_require_dependency(ed, "c-tools", "clangd") == 0) {
+ ECEX_CONFIG_INCLUDE(ecex_c_tools_plugin);
+ }
buffer_t *scratch = ecex_current_buffer(ed);
buffer_insert(scratch, "ecex config loaded !\n");
- buffer_insert(scratch, "Try F1, C-x C-f, C-x C-s, C-x d file browser, F2 commands, F3 buffers, F4 demo menu, C-x b.\n");
+ buffer_insert(scratch, "Try M-x, C-x C-f, C-x C-s, C-x d file browser, F2 commands, F3 buffers, F4 demo menu, C-x b.\n");
+ buffer_insert(scratch, "which-key is enabled: press C-x or M-g and the minibuffer will show continuations.\n");
+ buffer_insert(scratch, "TAB indents; in C buffers it smart-indents. C-TAB completes and cycles candidates.\n");
+ buffer_insert(scratch, "C completion asks clangd; c-mode highlights C syntax.\n");
+ buffer_insert(scratch, "When c-tools is loaded, C-x c l checks, C-x c k lints, C-x c e lints with ./include.\n");
buffer_insert(scratch, "Run M-x render-demo for a custom renderer demo, or M-x tetris for Tetris.\n");
buffer_insert(scratch, "Example line to edit then C-x C-e: ecex_set_font_size(ed, 28.0f);\n");
buffer_insert(scratch, "Example relative change: ecex_adjust_font_size(ed, 2.0f);\n\n");
diff --git a/config/markdown_plugin.c b/config/markdown_plugin.c
index ded0e1c..1026d31 100644
--- a/config/markdown_plugin.c
+++ b/config/markdown_plugin.c
@@ -3,9 +3,28 @@
#define MD_TEXT_TITLE 1
typedef struct md_state {
- ecex_t *ed;
+ ecex_plugin_t *plugin;
} md_state_t;
+static int md_line_is_fence(const char *line) {
+ const char *p = line;
+ if (!p) return 0;
+ while (*p == ' ' || *p == '\t') ++p;
+ return p[0] == '`' && p[1] == '`' && p[2] == '`';
+}
+
+static int md_in_code_at_line(buffer_t *buffer, int line) {
+ char scratch[256];
+ int in_code = 0;
+ int i;
+ if (!buffer || line <= 0) return 0;
+ for (i = 0; i < line; ++i) {
+ if (ecex_buffer_line_copy_text(buffer, i, scratch, (int)sizeof(scratch)) < 0) continue;
+ if (md_line_is_fence(scratch)) in_code = !in_code;
+ }
+ return in_code;
+}
+
static int md_render(ecex_t *ed, buffer_t *buffer, ecex_draw_context_t *ctx, void *userdata) {
md_state_t *state = (md_state_t *)userdata;
int draw_count;
@@ -19,31 +38,32 @@ static int md_render(ecex_t *ed, buffer_t *buffer, ecex_draw_context_t *ctx, voi
if (!ed || !buffer || !ctx || !state) return 0;
- draw_count = ecex_var_i32(ed, state, "draw_count", 0) + 1;
- ecex_var_i32_set_scalar(ed, state, "draw_count", draw_count);
+ draw_count = ecex_plugin_slot_i32_get_scalar(state->plugin, "draw_count", 0) + 1;
+ ecex_plugin_slot_i32_set_scalar(state->plugin, "draw_count", draw_count);
if (draw_count <= 2 || (draw_count % 120) == 0) {
ecex_log_int("markdown_plugin_draw: count=", draw_count);
}
- line_count = ecex_buffer_line_count_i(buffer);
+ line_count = ecex_buffer_line_count_int(buffer);
ecex_log_int("markdown_plugin_draw: line_count=", line_count);
if (line_count < 0) line_count = 0;
- ecex_text_set_buffer_title(ed, state, MD_TEXT_TITLE, buffer);
- ecex_draw_markdown_canvas_auto_i(ctx, state, MD_TEXT_TITLE);
+ ecex_plugin_text_set_from_buffer_title(state->plugin, MD_TEXT_TITLE, buffer);
+ ecex_draw_markdown_canvas_auto_i(ctx, state->plugin, MD_TEXT_TITLE);
- y = ecex_markdown_body_y_i(ctx);
- h = ecex_draw_context_height_i(ctx);
- line_h = ecex_draw_context_line_height_i(ctx);
- scroll = ecex_buffer_scroll_line(buffer);
+ y = ecex_markdown_body_y_px(ctx);
+ h = ecex_draw_context_height_px(ctx);
+ line_h = ecex_draw_context_line_height_px(ctx);
+ scroll = ecex_buffer_scroll_line_index(buffer);
if (scroll < 0) scroll = 0;
if (scroll > line_count) scroll = line_count;
+ in_code = md_in_code_at_line(buffer, scroll);
for (i = scroll; i < line_count && y < h - line_h; ++i) {
int packed;
int advance;
if (i < scroll + 4) ecex_log_int("markdown_plugin_draw: host line=", i);
- packed = ecex_markdown_draw_line_from_buffer_i(ctx, state, buffer, i, y, in_code);
+ packed = ecex_markdown_draw_buffer_line_i(ctx, state->plugin, buffer, i, y, in_code);
in_code = (packed & 0x10000) ? 1 : 0;
advance = packed & 0xffff;
if (advance <= 0) advance = line_h;
@@ -55,12 +75,9 @@ static int md_render(ecex_t *ed, buffer_t *buffer, ecex_draw_context_t *ctx, voi
static void md_free_state(void *userdata) {
md_state_t *state = (md_state_t *)userdata;
- ecex_t *ed;
if (!state) return;
- ed = state->ed;
- ecex_text_free_owner(ed, state);
- ecex_var_free_owner(ed, state);
- ecex_object_free(ed, state);
+ ecex_plugin_text_free_all(state->plugin);
+ ecex_plugin_object_free(state->plugin, state);
}
static int md_view_buffer(ecex_t *ed, buffer_t *buffer) {
@@ -68,12 +85,13 @@ static int md_view_buffer(ecex_t *ed, buffer_t *buffer) {
if (!ed || !buffer) return -1;
if (ecex_buffer_has_renderer(buffer)) ecex_buffer_clear_renderer(buffer);
- state = (md_state_t *)ecex_object_calloc(ed, 1, sizeof(*state));
+ ecex_plugin_t *plugin = ecex_plugin_find(ed, "markdown");
+ state = (md_state_t *)ecex_plugin_object_calloc(plugin, "state", 1, sizeof(*state));
if (!state) return -1;
- state->ed = ed;
+ state->plugin = plugin;
if (ecex_buffer_set_renderer(buffer, md_render, state, md_free_state, ECEX_RENDER_REPLACE_CONTENT) != 0) {
- ecex_object_free(ed, state);
+ ecex_plugin_object_free(plugin, state);
return -1;
}
ecex_buffer_set_major_mode_by_name(ed, buffer, "markdown-mode");
@@ -105,19 +123,19 @@ static int cmd_markdown_toggle(ecex_t *ed) {
return md_view_buffer(ed, buffer);
}
-int ecex_markdown_plugin(ecex_t *ed) {
+ECEX_PLUGIN_BEGIN(ecex_markdown_plugin, "markdown")
ECEX_CONFIG_MODE("markdown-mode");
ECEX_CONFIG_COMMAND("markdown-view", cmd_markdown_view);
ECEX_CONFIG_COMMAND("markdown-source", cmd_markdown_source);
ECEX_CONFIG_COMMAND("markdown-toggle", cmd_markdown_toggle);
ECEX_CONFIG_MODE_BIND("markdown-mode", "C-c C-c", "markdown-toggle");
ECEX_CONFIG_MODE_BIND("markdown-mode", "C-c C-s", "markdown-source");
- ecex_register_file_handler(ed, ".md", md_file_handler);
- ecex_register_file_handler(ed, ".markdown", md_file_handler);
- ecex_register_file_handler(ed, ".mdown", md_file_handler);
- ecex_register_file_handler(ed, ".mkd", md_file_handler);
+ ECEX_CONFIG_TRY(ecex_plugin_file_handler_register(plugin, ".md", md_file_handler));
+ ECEX_CONFIG_TRY(ecex_plugin_file_handler_register(plugin, ".markdown", md_file_handler));
+ ECEX_CONFIG_TRY(ecex_plugin_file_handler_register(plugin, ".mdown", md_file_handler));
+ ECEX_CONFIG_TRY(ecex_plugin_file_handler_register(plugin, ".mkd", md_file_handler));
return 0;
-}
+ECEX_PLUGIN_END
#ifndef ECEX_NO_STANDALONE_CONFIG
ECEX_CONFIG_BEGIN
diff --git a/config/render_demo.c b/config/render_demo.c
index 761b552..a0fb124 100644
--- a/config/render_demo.c
+++ b/config/render_demo.c
@@ -22,34 +22,33 @@
#define RENDER_DEMO_LABEL_CLICK 25
typedef struct render_demo_state {
- ecex_t *ed;
+ ecex_plugin_t *plugin;
int log_draw_count;
} render_demo_state_t;
-static int render_demo_get(ecex_t *ed, render_demo_state_t *s, const char *name, int fallback) {
- if (!ed || !s || !name) return fallback;
- return ecex_var_i32(ed, s, name, fallback);
+static int render_demo_get(render_demo_state_t *s, const char *name, int fallback) {
+ if (!s || !name) return fallback;
+ return ecex_plugin_slot_i32_get_scalar(s->plugin, name, fallback);
}
-static void render_demo_set(ecex_t *ed, render_demo_state_t *s, const char *name, int value) {
- if (!ed || !s || !name) return;
- ecex_var_i32_set_scalar(ed, s, name, value);
+static void render_demo_set(render_demo_state_t *s, const char *name, int value) {
+ if (!s || !name) return;
+ ecex_plugin_slot_i32_set_scalar(s->plugin, name, value);
}
-static void render_demo_reset(ecex_t *ed, render_demo_state_t *s) {
+static void render_demo_reset(render_demo_state_t *s) {
ecex_log_ptr("render_demo_reset: state=", s);
- if (!ed || !s) return;
+ if (!s) return;
- /* Store mutable demo state in the host variable registry. This mirrors the
- * Tetris plugin flow and avoids relying on CCDJIT struct-field writes for
- * values that must survive renderer, animation, and mouse callbacks.
+ /* Store mutable demo state in plugin slots. This avoids relying on CCDJIT
+ * struct-field writes for values shared by renderer, animation, and mouse callbacks.
* Values are fixed-point integers: 0..1000 represents the available travel
* range inside the demo frame. */
- render_demo_set(ed, s, RENDER_DEMO_VAR_X, 0);
- render_demo_set(ed, s, RENDER_DEMO_VAR_Y, 500);
- render_demo_set(ed, s, RENDER_DEMO_VAR_TARGET_X, 0);
- render_demo_set(ed, s, RENDER_DEMO_VAR_TARGET_Y, 500);
- render_demo_set(ed, s, RENDER_DEMO_VAR_MOVING, 0);
+ render_demo_set(s, RENDER_DEMO_VAR_X, 0);
+ render_demo_set(s, RENDER_DEMO_VAR_Y, 500);
+ render_demo_set(s, RENDER_DEMO_VAR_TARGET_X, 0);
+ render_demo_set(s, RENDER_DEMO_VAR_TARGET_Y, 500);
+ render_demo_set(s, RENDER_DEMO_VAR_MOVING, 0);
s->log_draw_count = 0;
}
@@ -93,27 +92,27 @@ static int render_demo_draw(ecex_t *ed, buffer_t *buffer, ecex_draw_context_t *c
if (cw < 1) cw = 1;
if (ch < 1) ch = 1;
- ecex_draw_color_rgba8(ctx, 26, 28, 36, 255);
+ ecex_draw_color_rgba8_i(ctx, 26, 28, 36, 255);
ecex_draw_rect_i(ctx, 0, 0, w, h);
- ecex_draw_color_rgba8(ctx, 48, 96, 180, 255);
+ ecex_draw_color_rgba8_i(ctx, 48, 96, 180, 255);
ecex_draw_rect_i(ctx, cx, cy, (cw * 55) / 100, line_h * 2);
- ecex_draw_color_rgba8(ctx, 242, 230, 191, 255);
+ ecex_draw_color_rgba8_i(ctx, 242, 230, 191, 255);
ecex_draw_rect_outline_i(ctx, cx, cy, cw, ch, 2);
ecex_draw_label_i(ctx, cx + 12, cy + 10, RENDER_DEMO_LABEL_TITLE);
- ecex_draw_color_rgba8(ctx, 229, 89, 64, 255);
+ ecex_draw_color_rgba8_i(ctx, 229, 89, 64, 255);
ecex_draw_line_i(ctx, cx, cy + ch, cx + cw, cy, 3);
- ecex_draw_color_rgba8(ctx, 160, 230, 180, 255);
+ ecex_draw_color_rgba8_i(ctx, 160, 230, 180, 255);
ecex_draw_label_i(ctx, cx + 12, cy + line_h * 3, RENDER_DEMO_LABEL_SUBTITLE);
ecex_draw_label_i(ctx, cx + 12, cy + line_h * 4, RENDER_DEMO_LABEL_SAFE_DRAW);
ecex_draw_label_i(ctx, cx + 12, cy + line_h * 5, RENDER_DEMO_LABEL_ANIM);
ecex_draw_label_i(ctx, cx + 12, cy + line_h * 6, RENDER_DEMO_LABEL_CLICK);
- x_milli = render_demo_get(ed, s, RENDER_DEMO_VAR_X, 0);
- y_milli = render_demo_get(ed, s, RENDER_DEMO_VAR_Y, 500);
+ x_milli = render_demo_get(s, RENDER_DEMO_VAR_X, 0);
+ y_milli = render_demo_get(s, RENDER_DEMO_VAR_Y, 500);
ecex_draw_stat_i(ctx, cx + 12, cy + line_h * 8, RENDER_DEMO_LABEL_POSITION, x_milli);
box = line_h * 2;
@@ -132,20 +131,20 @@ static int render_demo_draw(ecex_t *ed, buffer_t *buffer, ecex_draw_context_t *c
box_x = area_x + (travel * x_milli) / 1000;
box_y = area_y + (travel_y * y_milli) / 1000;
- render_demo_set(ed, s, RENDER_DEMO_VAR_BOX_X, box_x);
- render_demo_set(ed, s, RENDER_DEMO_VAR_BOX_Y, box_y);
- render_demo_set(ed, s, RENDER_DEMO_VAR_BOX_SIZE, box);
- render_demo_set(ed, s, RENDER_DEMO_VAR_TRAVEL_X, travel);
- render_demo_set(ed, s, RENDER_DEMO_VAR_TRAVEL_Y, travel_y);
- render_demo_set(ed, s, RENDER_DEMO_VAR_AREA_X, area_x);
- render_demo_set(ed, s, RENDER_DEMO_VAR_AREA_Y, area_y);
+ render_demo_set(s, RENDER_DEMO_VAR_BOX_X, box_x);
+ render_demo_set(s, RENDER_DEMO_VAR_BOX_Y, box_y);
+ render_demo_set(s, RENDER_DEMO_VAR_BOX_SIZE, box);
+ render_demo_set(s, RENDER_DEMO_VAR_TRAVEL_X, travel);
+ render_demo_set(s, RENDER_DEMO_VAR_TRAVEL_Y, travel_y);
+ render_demo_set(s, RENDER_DEMO_VAR_AREA_X, area_x);
+ render_demo_set(s, RENDER_DEMO_VAR_AREA_Y, area_y);
- ecex_draw_color_rgba8(ctx, 70, 76, 90, 255);
+ ecex_draw_color_rgba8_i(ctx, 70, 76, 90, 255);
ecex_draw_rect_outline_i(ctx, area_x, area_y, travel + box, travel_y + box, 1);
- ecex_draw_color_rgba8(ctx, 89, 242, 140, 255);
+ ecex_draw_color_rgba8_i(ctx, 89, 242, 140, 255);
ecex_draw_rect_i(ctx, box_x, box_y, box, box);
- ecex_draw_color_rgba8(ctx, 20, 24, 30, 255);
+ ecex_draw_color_rgba8_i(ctx, 20, 24, 30, 255);
ecex_draw_rect_outline_i(ctx, box_x, box_y, box, box, 2);
return 0;
@@ -185,20 +184,20 @@ static int render_demo_tick(ecex_t *ed, buffer_t *buffer, int now_ms, void *user
(void)buffer;
(void)now_ms;
if (!ed || !s) return 0;
- if (!render_demo_get(ed, s, RENDER_DEMO_VAR_MOVING, 0)) return 0;
+ if (!render_demo_get(s, RENDER_DEMO_VAR_MOVING, 0)) return 0;
- x = render_demo_get(ed, s, RENDER_DEMO_VAR_X, 0);
- y = render_demo_get(ed, s, RENDER_DEMO_VAR_Y, 500);
- target_x = render_demo_get(ed, s, RENDER_DEMO_VAR_TARGET_X, x);
- target_y = render_demo_get(ed, s, RENDER_DEMO_VAR_TARGET_Y, y);
+ x = render_demo_get(s, RENDER_DEMO_VAR_X, 0);
+ y = render_demo_get(s, RENDER_DEMO_VAR_Y, 500);
+ target_x = render_demo_get(s, RENDER_DEMO_VAR_TARGET_X, x);
+ target_y = render_demo_get(s, RENDER_DEMO_VAR_TARGET_Y, y);
next_x = render_demo_delta_step(x, target_x);
next_y = render_demo_delta_step(y, target_y);
- render_demo_set(ed, s, RENDER_DEMO_VAR_X, render_demo_clamp_milli(next_x));
- render_demo_set(ed, s, RENDER_DEMO_VAR_Y, render_demo_clamp_milli(next_y));
+ render_demo_set(s, RENDER_DEMO_VAR_X, render_demo_clamp_milli(next_x));
+ render_demo_set(s, RENDER_DEMO_VAR_Y, render_demo_clamp_milli(next_y));
if (next_x == target_x && next_y == target_y) {
- render_demo_set(ed, s, RENDER_DEMO_VAR_MOVING, 0);
+ render_demo_set(s, RENDER_DEMO_VAR_MOVING, 0);
ecex_log("render_demo_tick: target reached");
}
return 1;
@@ -218,11 +217,11 @@ static int render_demo_mouse(ecex_t *ed, buffer_t *buffer, int event, int x, int
if (!ed || !s || button != ECEX_MOUSE_BUTTON_LEFT) return 0;
if (event != ECEX_MOUSE_PRESS) return 0;
- box = render_demo_get(ed, s, RENDER_DEMO_VAR_BOX_SIZE, 0);
- area_x = render_demo_get(ed, s, RENDER_DEMO_VAR_AREA_X, 0);
- area_y = render_demo_get(ed, s, RENDER_DEMO_VAR_AREA_Y, 0);
- travel_x = render_demo_get(ed, s, RENDER_DEMO_VAR_TRAVEL_X, 0);
- travel_y = render_demo_get(ed, s, RENDER_DEMO_VAR_TRAVEL_Y, 0);
+ box = render_demo_get(s, RENDER_DEMO_VAR_BOX_SIZE, 0);
+ area_x = render_demo_get(s, RENDER_DEMO_VAR_AREA_X, 0);
+ area_y = render_demo_get(s, RENDER_DEMO_VAR_AREA_Y, 0);
+ travel_x = render_demo_get(s, RENDER_DEMO_VAR_TRAVEL_X, 0);
+ travel_y = render_demo_get(s, RENDER_DEMO_VAR_TRAVEL_Y, 0);
if (box <= 0) return 0;
if (x < area_x || x >= area_x + travel_x + box ||
@@ -232,12 +231,12 @@ static int render_demo_mouse(ecex_t *ed, buffer_t *buffer, int event, int x, int
nx = x - area_x - box / 2;
ny = y - area_y - box / 2;
- if (travel_x <= 0) render_demo_set(ed, s, RENDER_DEMO_VAR_TARGET_X, 0);
- else render_demo_set(ed, s, RENDER_DEMO_VAR_TARGET_X, render_demo_clamp_milli((nx * 1000) / travel_x));
- if (travel_y <= 0) render_demo_set(ed, s, RENDER_DEMO_VAR_TARGET_Y, 0);
- else render_demo_set(ed, s, RENDER_DEMO_VAR_TARGET_Y, render_demo_clamp_milli((ny * 1000) / travel_y));
+ if (travel_x <= 0) render_demo_set(s, RENDER_DEMO_VAR_TARGET_X, 0);
+ else render_demo_set(s, RENDER_DEMO_VAR_TARGET_X, render_demo_clamp_milli((nx * 1000) / travel_x));
+ if (travel_y <= 0) render_demo_set(s, RENDER_DEMO_VAR_TARGET_Y, 0);
+ else render_demo_set(s, RENDER_DEMO_VAR_TARGET_Y, render_demo_clamp_milli((ny * 1000) / travel_y));
- render_demo_set(ed, s, RENDER_DEMO_VAR_MOVING, 1);
+ render_demo_set(s, RENDER_DEMO_VAR_MOVING, 1);
ecex_log("render_demo_mouse: target set");
return 1;
}
@@ -245,8 +244,7 @@ static int render_demo_mouse(ecex_t *ed, buffer_t *buffer, int event, int x, int
static void render_demo_free_state(void *userdata) {
render_demo_state_t *s = (render_demo_state_t *)userdata;
if (!s) return;
- ecex_var_free_owner(s->ed, s);
- ecex_config_free(s);
+ ecex_plugin_object_free(s->plugin, s);
}
static int cmd_render_demo(ecex_t *ed) {
@@ -262,10 +260,11 @@ static int cmd_render_demo(ecex_t *ed) {
s = (render_demo_state_t *)ecex_buffer_renderer_userdata(buffer);
if (!s) {
- s = (render_demo_state_t *)ecex_config_calloc(1, sizeof(*s));
+ ecex_plugin_t *plugin = ecex_plugin_find(ed, "render-demo");
+ s = (render_demo_state_t *)ecex_plugin_object_calloc(plugin, "state", 1, sizeof(*s));
if (!s) return -1;
- s->ed = ed;
- render_demo_reset(ed, s);
+ s->plugin = plugin;
+ render_demo_reset(s);
if (ecex_buffer_set_renderer(buffer, render_demo_draw, s, render_demo_free_state, ECEX_RENDER_REPLACE_CONTENT) != 0) {
render_demo_free_state(s);
@@ -290,10 +289,11 @@ static int cmd_render_demo(ecex_t *ed) {
return ecex_switch_buffer(ed, RENDER_DEMO_BUF);
}
-int ecex_render_demo_plugin(ecex_t *ed) {
+ECEX_PLUGIN_BEGIN(ecex_render_demo_plugin, "render-demo")
+ (void)plugin;
ECEX_CONFIG_COMMAND("render-demo", cmd_render_demo);
return 0;
-}
+ECEX_PLUGIN_END
#ifndef ECEX_NO_STANDALONE_CONFIG
ECEX_CONFIG_BEGIN
diff --git a/config/tetris.c b/config/tetris.c
index 542963a..e86ab75 100644
--- a/config/tetris.c
+++ b/config/tetris.c
@@ -9,8 +9,7 @@
#define TETRIS_VAR_NEXT "next_piece"
typedef struct tetris_state {
- ecex_t *ed;
- int *board;
+ ecex_plugin_t *plugin;
int piece;
int rot;
int x;
@@ -34,28 +33,61 @@ static int tetris_idx(int x, int y) {
}
static int tetris_shape_cell(int piece, int rot, int col, int row) {
- return ecex_tetris_shape_cell(piece, rot, col, 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;
+
+ if (p == 0) {
+ if ((r & 1) == 0) return row == 1;
+ return col == 1;
+ }
+ if (p == 1) return (row == 1 || row == 2) && (col == 1 || col == 2);
+ 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);
+ }
+ 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));
+ }
+ 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));
+ }
+ 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);
+ }
+ 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 *tetris_board(ecex_t *ed, tetris_state_t *s) {
- if (!ed || !s) return 0;
- return (int *)ecex_var_get_or_alloc(ed, s, TETRIS_VAR_BOARD, (size_t)TETRIS_CELLS, sizeof(int));
+static int *tetris_board(tetris_state_t *s) {
+ if (!s) return 0;
+ return (int *)ecex_plugin_slot_alloc(s->plugin, TETRIS_VAR_BOARD, (size_t)TETRIS_CELLS, sizeof(int));
}
-static int tetris_next_piece(ecex_t *ed, tetris_state_t *s) {
- return ecex_var_i32(ed, s, TETRIS_VAR_NEXT, 0);
+static int tetris_next_piece(tetris_state_t *s) {
+ return ecex_plugin_slot_i32_get_scalar(s ? s->plugin : 0, TETRIS_VAR_NEXT, 0);
}
-static void tetris_set_next_piece(ecex_t *ed, tetris_state_t *s, int piece) {
+static void tetris_set_next_piece(tetris_state_t *s, int piece) {
if (piece < 0) piece = 0;
if (piece > 6) piece = piece % 7;
- ecex_var_i32_set_scalar(ed, s, TETRIS_VAR_NEXT, piece);
+ ecex_plugin_slot_i32_set_scalar(s ? s->plugin : 0, TETRIS_VAR_NEXT, piece);
}
-static void tetris_clear_board(ecex_t *ed, tetris_state_t *s) {
+static void tetris_clear_board(tetris_state_t *s) {
int *board;
if (!s) { ecex_log("tetris_clear_board: null state"); return; }
- board = tetris_board(ed, s);
+ board = tetris_board(s);
ecex_log_ptr("tetris_clear_board: board=", board);
ecex_mem_zero(board, (size_t)TETRIS_CELLS * sizeof(int));
ecex_log("tetris_clear_board: done");
@@ -71,11 +103,11 @@ static int tetris_pick(tetris_state_t *s) {
return piece;
}
-static int tetris_collides(ecex_t *ed, tetris_state_t *s, int piece, int rot, int px, int py) {
+static int tetris_collides(tetris_state_t *s, int piece, int rot, int px, int py) {
int r;
int c;
- int *board = tetris_board(ed, s);
+ int *board = tetris_board(s);
if (!s || !board) return 1;
for (r = 0; r < 4; ++r) {
for (c = 0; c < 4; ++c) {
@@ -93,44 +125,44 @@ static int tetris_collides(ecex_t *ed, tetris_state_t *s, int piece, int rot, in
return 0;
}
-static void tetris_spawn(ecex_t *ed, tetris_state_t *s) {
+static void tetris_spawn(tetris_state_t *s) {
int queued;
ecex_log_ptr("tetris_spawn: state=", s);
if (!s) return;
/* The sidebar must show the piece that will become active on the next
- * spawn. Keep that queued value in the host variable registry, then consume
+ * spawn. Keep that queued value in a plugin slot, then consume
* it here and immediately replace it with a freshly-picked preview. */
- queued = tetris_next_piece(ed, s);
+ queued = tetris_next_piece(s);
if (queued < 0 || queued > 6) queued = tetris_pick(s);
s->piece = queued;
- tetris_set_next_piece(ed, s, tetris_pick(s));
+ tetris_set_next_piece(s, tetris_pick(s));
ecex_log_int("tetris_spawn: current_piece=", s->piece);
- ecex_log_int("tetris_spawn: preview_piece=", tetris_next_piece(ed, s));
+ ecex_log_int("tetris_spawn: preview_piece=", tetris_next_piece(s));
s->rot = 0;
s->x = 3;
s->y = -1;
s->last_drop_ms = 0;
- if (tetris_collides(ed, s, s->piece, s->rot, s->x, s->y)) {
+ if (tetris_collides(s, s->piece, s->rot, s->x, s->y)) {
ecex_log("tetris_spawn: immediate collision -> game over");
s->game_over = 1;
}
}
-static void tetris_reset(ecex_t *ed, tetris_state_t *s) {
+static void tetris_reset(tetris_state_t *s) {
ecex_log_ptr("tetris_reset: state=", s);
if (!s) return;
- tetris_clear_board(ed, s);
+ tetris_clear_board(s);
ecex_log("tetris_reset: after clear");
/* Avoid calling libc/time-returning host functions from CCDJIT plugin code.
* Seed from stable host-owned addresses instead; good enough for a toy game
* and much safer for the tiny JIT ABI. */
- s->rng = 0x9e3779b9u ^ (unsigned int)(size_t)s ^ (unsigned int)(size_t)tetris_board(ed, s);
+ s->rng = 0x9e3779b9u ^ (unsigned int)(size_t)s ^ (unsigned int)(size_t)tetris_board(s);
ecex_log_int("tetris_reset: rng=", (int)s->rng);
s->score = 0;
s->lines = 0;
@@ -139,15 +171,15 @@ static void tetris_reset(ecex_t *ed, tetris_state_t *s) {
s->paused = 0;
s->last_drop_ms = 0;
s->drop_interval_ms = 650;
- tetris_set_next_piece(ed, s, tetris_pick(s));
+ tetris_set_next_piece(s, tetris_pick(s));
s->log_tick_count = 0;
s->log_draw_count = 0;
ecex_log("tetris_reset: spawning first piece");
- tetris_spawn(ed, s);
+ tetris_spawn(s);
ecex_log("tetris_reset: done");
}
-static void tetris_lock(ecex_t *ed, tetris_state_t *s) {
+static void tetris_lock(tetris_state_t *s) {
ecex_log_ptr("tetris_lock: state=", s);
int r;
int c;
@@ -156,7 +188,7 @@ static void tetris_lock(ecex_t *ed, tetris_state_t *s) {
int *board;
if (!s) return;
- board = tetris_board(ed, s);
+ board = tetris_board(s);
if (!board) return;
for (r = 0; r < 4; ++r) {
@@ -214,7 +246,7 @@ static void tetris_lock(ecex_t *ed, tetris_state_t *s) {
}
ecex_log_int("tetris_lock: cleared=", cleared);
- tetris_spawn(ed, s);
+ tetris_spawn(s);
}
@@ -244,11 +276,11 @@ static int tetris_top_out_if_hidden(tetris_state_t *s, const char *where) {
return 1;
}
-static int tetris_soft_drop(ecex_t *ed, tetris_state_t *s) {
+static int tetris_soft_drop(tetris_state_t *s) {
if (!s) { ecex_log("tetris_soft_drop: null state"); return 0; }
if (s->game_over || s->paused) return 0;
- if (!tetris_collides(ed, s, s->piece, s->rot, s->x, s->y + 1)) {
+ if (!tetris_collides(s, s->piece, s->rot, s->x, s->y + 1)) {
++s->y;
return 1;
}
@@ -256,12 +288,12 @@ static int tetris_soft_drop(ecex_t *ed, tetris_state_t *s) {
ecex_log_int("tetris_soft_drop: locking piece=", s->piece);
ecex_log_int("tetris_soft_drop: y=", s->y);
if (tetris_top_out_if_hidden(s, "tetris_soft_drop: hidden lock -> game over")) return 1;
- tetris_lock(ed, s);
+ tetris_lock(s);
return 0;
}
-static int tetris_move_horizontal(ecex_t *ed, tetris_state_t *s, int dx) {
+static int tetris_move_horizontal(tetris_state_t *s, int dx) {
int old_x;
int old_y;
int old_rot;
@@ -273,7 +305,7 @@ static int tetris_move_horizontal(ecex_t *ed, tetris_state_t *s, int dx) {
old_y = s->y;
old_rot = s->rot;
- if (!tetris_collides(ed, s, s->piece, s->rot, old_x + dx, old_y)) {
+ if (!tetris_collides(s, s->piece, s->rot, old_x + dx, old_y)) {
s->x = old_x + dx;
/* Horizontal movement must never vertical-kick the active piece.
* Keep these assignments explicit because plugin/JIT callback bugs are
@@ -296,18 +328,18 @@ static int tetris_move_horizontal(ecex_t *ed, tetris_state_t *s, int dx) {
return 0;
}
-static void tetris_hard_drop(ecex_t *ed, tetris_state_t *s) {
+static void tetris_hard_drop(tetris_state_t *s) {
int moved = 0;
if (!s || s->game_over || s->paused) return;
- while (!tetris_collides(ed, s, s->piece, s->rot, s->x, s->y + 1)) {
+ while (!tetris_collides(s, s->piece, s->rot, s->x, s->y + 1)) {
++s->y;
++moved;
}
s->score += moved * 2;
if (tetris_top_out_if_hidden(s, "tetris_hard_drop: hidden lock -> game over")) return;
- tetris_lock(ed, s);
+ tetris_lock(s);
}
static tetris_state_t *tetris_state_for_ed(ecex_t *ed) {
@@ -330,14 +362,14 @@ static int tetris_alpha8(int alpha) {
static void tetris_color(ecex_draw_context_t *ctx, int cell, int alpha) {
alpha = tetris_alpha8(alpha);
- if (cell == 1) ecex_draw_color_rgba8(ctx, 51, 191, 242, alpha);
- else if (cell == 2) ecex_draw_color_rgba8(ctx, 242, 217, 51, alpha);
- else if (cell == 3) ecex_draw_color_rgba8(ctx, 179, 89, 242, alpha);
- else if (cell == 4) ecex_draw_color_rgba8(ctx, 77, 217, 89, alpha);
- else if (cell == 5) ecex_draw_color_rgba8(ctx, 242, 64, 64, alpha);
- else if (cell == 6) ecex_draw_color_rgba8(ctx, 64, 102, 242, alpha);
- else if (cell == 7) ecex_draw_color_rgba8(ctx, 242, 140, 51, alpha);
- else ecex_draw_color_rgba8(ctx, 41, 41, 46, alpha);
+ if (cell == 1) ecex_draw_color_rgba8_i(ctx, 51, 191, 242, alpha);
+ else if (cell == 2) ecex_draw_color_rgba8_i(ctx, 242, 217, 51, alpha);
+ else if (cell == 3) ecex_draw_color_rgba8_i(ctx, 179, 89, 242, alpha);
+ else if (cell == 4) ecex_draw_color_rgba8_i(ctx, 77, 217, 89, alpha);
+ else if (cell == 5) ecex_draw_color_rgba8_i(ctx, 242, 64, 64, alpha);
+ else if (cell == 6) ecex_draw_color_rgba8_i(ctx, 64, 102, 242, alpha);
+ else if (cell == 7) ecex_draw_color_rgba8_i(ctx, 242, 140, 51, alpha);
+ else ecex_draw_color_rgba8_i(ctx, 41, 41, 46, alpha);
}
static void tetris_draw_piece(ecex_draw_context_t *ctx,
@@ -386,7 +418,7 @@ static int tetris_tick(ecex_t *ed, buffer_t *buffer, int now_ms, void *userdata)
if (!s->game_over && !s->paused && now_ms - s->last_drop_ms >= s->drop_interval_ms) {
ecex_log("tetris_tick: dropping piece");
- tetris_soft_drop(ed, s);
+ tetris_soft_drop(s);
s->last_drop_ms = now_ms;
return 1;
}
@@ -413,8 +445,8 @@ static int tetris_draw(ecex_t *ed, buffer_t *buffer, ecex_draw_context_t *ctx, v
if (!s) { ecex_log("tetris_draw: null state"); return 0; }
if (!ctx) { ecex_log("tetris_draw: null ctx"); return 0; }
- board = tetris_board(ed, s);
- if (!board) { ecex_log("tetris_draw: no registry board"); return 0; }
+ board = tetris_board(s);
+ if (!board) { ecex_log("tetris_draw: no plugin board slot"); return 0; }
s->log_draw_count += 1;
if (s->log_draw_count <= 8 || (s->log_draw_count % 60) == 0) {
ecex_log_int("tetris_draw: count=", s->log_draw_count);
@@ -423,7 +455,7 @@ static int tetris_draw(ecex_t *ed, buffer_t *buffer, ecex_draw_context_t *ctx, v
}
if (s->log_draw_count <= 3) ecex_log("tetris_draw: background");
- ecex_draw_color_rgba8(ctx, 20, 23, 28, 255);
+ ecex_draw_color_rgba8_i(ctx, 20, 23, 28, 255);
ecex_draw_rect_i(ctx, 0, 0, (int)ctx->w, (int)ctx->h);
max_board_w = ((int)ctx->content_w * 62) / 100;
@@ -438,9 +470,9 @@ static int tetris_draw(ecex_t *ed, buffer_t *buffer, ecex_draw_context_t *ctx, v
oy = (int)ctx->content_y + 12;
if (s->log_draw_count <= 3) ecex_log("tetris_draw: board frame");
- ecex_draw_color_rgba8(ctx, 8, 9, 12, 255);
+ ecex_draw_color_rgba8_i(ctx, 8, 9, 12, 255);
ecex_draw_rect_i(ctx, ox - 6, oy - 6, board_w + 12, board_h + 12);
- ecex_draw_color_rgba8(ctx, 97, 102, 115, 255);
+ ecex_draw_color_rgba8_i(ctx, 97, 102, 115, 255);
ecex_draw_rect_outline_i(ctx, ox - 6, oy - 6, board_w + 12, board_h + 12, 2);
if (s->log_draw_count <= 3) ecex_log("tetris_draw: cells");
@@ -459,7 +491,7 @@ static int tetris_draw(ecex_t *ed, buffer_t *buffer, ecex_draw_context_t *ctx, v
sy = oy;
if (s->log_draw_count <= 3) ecex_log("tetris_draw: sidebar");
- ecex_draw_color_rgba8(ctx, 235, 235, 214, 255);
+ ecex_draw_color_rgba8_i(ctx, 235, 235, 214, 255);
ecex_log("tetris_draw: sidebar title");
ecex_draw_label_i(ctx, sx, sy, 1);
sy += ((int)ctx->line_height * 16) / 10;
@@ -475,11 +507,11 @@ static int tetris_draw(ecex_t *ed, buffer_t *buffer, ecex_draw_context_t *ctx, v
ecex_draw_label_i(ctx, sx, sy, 5);
sy += ((int)ctx->line_height * 8) / 10;
- ecex_log_int("tetris_draw: preview_piece=", tetris_next_piece(ed, s));
- ecex_draw_tetris_preview_i(ctx, tetris_next_piece(ed, s), sx, sy, (cell * 72) / 100, 255);
+ ecex_log_int("tetris_draw: preview_piece=", tetris_next_piece(s));
+ ecex_draw_tetris_preview_i(ctx, tetris_next_piece(s), sx, sy, (cell * 72) / 100, 255);
sy += cell * 4;
- ecex_draw_color_rgba8(ctx, 184, 189, 199, 255);
+ ecex_draw_color_rgba8_i(ctx, 184, 189, 199, 255);
ecex_log("tetris_draw: sidebar help");
ecex_draw_label_i(ctx, sx, sy, 6); sy += (int)ctx->line_height;
ecex_draw_label_i(ctx, sx, sy, 7); sy += (int)ctx->line_height;
@@ -489,9 +521,9 @@ static int tetris_draw(ecex_t *ed, buffer_t *buffer, ecex_draw_context_t *ctx, v
ecex_draw_label_i(ctx, sx, sy, 11);
if (s->paused || s->game_over) {
- ecex_draw_color_rgba8(ctx, 0, 0, 0, 174);
+ ecex_draw_color_rgba8_i(ctx, 0, 0, 0, 174);
ecex_draw_rect_i(ctx, ox, oy + (board_h * 43) / 100, board_w, ((int)ctx->line_height * 22) / 10);
- ecex_draw_color_rgba8(ctx, 255, 235, 89, 255);
+ ecex_draw_color_rgba8_i(ctx, 255, 235, 89, 255);
ecex_draw_label_i(ctx, ox + cell, oy + (board_h * 43) / 100 + ((int)ctx->line_height * 55) / 100, s->game_over ? 12 : 13);
}
@@ -502,8 +534,7 @@ static int tetris_draw(ecex_t *ed, buffer_t *buffer, ecex_draw_context_t *ctx, v
static void tetris_free_state(void *userdata) {
tetris_state_t *s = (tetris_state_t *)userdata;
if (!s) return;
- ecex_var_free_owner(s->ed, s);
- ecex_config_free(s);
+ ecex_plugin_object_free(s->plugin, s);
}
static int cmd_tetris(ecex_t *ed) {
@@ -527,25 +558,25 @@ static int cmd_tetris(ecex_t *ed) {
ecex_log_ptr("cmd_tetris: existing state=", s);
if (!s) {
ecex_log_int("cmd_tetris: allocating state bytes=", (int)sizeof(*s));
- s = (tetris_state_t *)ecex_config_calloc(1, sizeof(*s));
+ ecex_plugin_t *plugin = ecex_plugin_find(ed, "tetris");
+ s = (tetris_state_t *)ecex_plugin_object_calloc(plugin, "state", 1, sizeof(*s));
ecex_log_ptr("cmd_tetris: allocated state=", s);
if (!s) { ecex_log("cmd_tetris: allocation failed"); return -1; }
- s->ed = ed;
- ecex_log_int("cmd_tetris: registry board bytes=", (int)((size_t)TETRIS_CELLS * sizeof(int)));
- ecex_log_ptr("cmd_tetris: registry board=", tetris_board(ed, s));
- if (!tetris_board(ed, s)) {
- ecex_log("cmd_tetris: registry board allocation failed");
- ecex_var_free_owner(ed, s);
- ecex_config_free(s);
+ s->plugin = plugin;
+ ecex_log_int("cmd_tetris: board slot bytes=", (int)((size_t)TETRIS_CELLS * sizeof(int)));
+ ecex_log_ptr("cmd_tetris: board slot=", tetris_board(s));
+ if (!tetris_board(s)) {
+ ecex_log("cmd_tetris: board slot allocation failed");
+ ecex_plugin_object_free(plugin, s);
return -1;
}
- tetris_reset(ed, s);
+ tetris_reset(s);
ecex_log("cmd_tetris: setting renderer");
if (ecex_buffer_set_renderer(buffer, tetris_draw, s, tetris_free_state, ECEX_RENDER_REPLACE_CONTENT) != 0) {
ecex_log("cmd_tetris: set_renderer failed");
- ecex_config_free(s);
+ ecex_plugin_object_free(plugin, s);
return -1;
}
@@ -577,19 +608,19 @@ static int cmd_tetris_new(ecex_t *ed) {
ecex_log("cmd_tetris_new: enter");
tetris_state_t *s = tetris_state_for_ed(ed);
if (!s) return cmd_tetris(ed);
- tetris_reset(ed, s);
+ tetris_reset(s);
return 0;
}
static int cmd_tetris_left(ecex_t *ed) {
ecex_log("cmd_tetris_left: enter");
- tetris_move_horizontal(ed, tetris_state_for_ed(ed), -1);
+ tetris_move_horizontal(tetris_state_for_ed(ed), -1);
return 0;
}
static int cmd_tetris_right(ecex_t *ed) {
ecex_log("cmd_tetris_right: enter");
- tetris_move_horizontal(ed, tetris_state_for_ed(ed), 1);
+ tetris_move_horizontal(tetris_state_for_ed(ed), 1);
return 0;
}
@@ -597,7 +628,7 @@ static int cmd_tetris_down(ecex_t *ed) {
ecex_log("cmd_tetris_down: enter");
tetris_state_t *s = tetris_state_for_ed(ed);
if (s) {
- if (tetris_soft_drop(ed, s)) s->score += 1;
+ if (tetris_soft_drop(s)) s->score += 1;
s->last_drop_ms = 0;
}
return 0;
@@ -611,12 +642,12 @@ static int cmd_tetris_rotate(ecex_t *ed) {
if (!s || s->game_over || s->paused) return 0;
nr = (s->rot + 1) & 3;
- if (!tetris_collides(ed, s, s->piece, nr, s->x, s->y)) {
+ if (!tetris_collides(s, s->piece, nr, s->x, s->y)) {
s->rot = nr;
- } else if (!tetris_collides(ed, s, s->piece, nr, s->x - 1, s->y)) {
+ } else if (!tetris_collides(s, s->piece, nr, s->x - 1, s->y)) {
--s->x;
s->rot = nr;
- } else if (!tetris_collides(ed, s, s->piece, nr, s->x + 1, s->y)) {
+ } else if (!tetris_collides(s, s->piece, nr, s->x + 1, s->y)) {
++s->x;
s->rot = nr;
}
@@ -626,7 +657,7 @@ static int cmd_tetris_rotate(ecex_t *ed) {
static int cmd_tetris_drop(ecex_t *ed) {
ecex_log("cmd_tetris_drop: enter");
- tetris_hard_drop(ed, tetris_state_for_ed(ed));
+ tetris_hard_drop(tetris_state_for_ed(ed));
return 0;
}
@@ -645,8 +676,9 @@ static int cmd_tetris_quit(ecex_t *ed) {
return ecex_execute_command(ed, "quit-window");
}
-int ecex_tetris_plugin(ecex_t *ed) {
+ECEX_PLUGIN_BEGIN(ecex_tetris_plugin, "tetris")
ecex_log("ecex_tetris_plugin: enter");
+ (void)plugin;
ECEX_CONFIG_MODE(TETRIS_MODE);
ECEX_CONFIG_COMMAND("tetris", cmd_tetris);
ECEX_CONFIG_COMMAND("tetris-new", cmd_tetris_new);
@@ -673,7 +705,7 @@ int ecex_tetris_plugin(ecex_t *ed) {
ecex_log("ecex_tetris_plugin: registered all commands and keybinds");
return 0;
-}
+ECEX_PLUGIN_END
#ifndef ECEX_NO_STANDALONE_CONFIG
ECEX_CONFIG_BEGIN
diff --git a/config/which_key_plugin.c b/config/which_key_plugin.c
new file mode 100644
index 0000000..8d4fb50
--- /dev/null
+++ b/config/which_key_plugin.c
@@ -0,0 +1,107 @@
+#include "ecex.h"
+
+#define WK_HOOK_NAME "which-key"
+#define WK_SLOT_ENABLED "enabled"
+
+typedef struct which_key_state {
+ ecex_plugin_t *plugin;
+ int showing;
+} which_key_state_t;
+
+static int which_key_enabled(ecex_plugin_t *plugin) {
+ return ecex_plugin_slot_i32_get_scalar(plugin, WK_SLOT_ENABLED, 1) != 0;
+}
+
+static int cmd_which_key_toggle(ecex_t *ed) {
+ ecex_plugin_t *plugin = ecex_plugin_find(ed, "which-key");
+ if (!plugin) return -1;
+
+ int enabled = !which_key_enabled(plugin);
+ ecex_plugin_slot_i32_set_scalar(plugin, WK_SLOT_ENABLED, enabled);
+ ecex_message(ed, enabled ? "which-key enabled" : "which-key disabled");
+ return 0;
+}
+
+static void which_key_prefix_hook(ecex_t *ed, const char *prefix, int event, void *userdata) {
+ which_key_state_t *state = (which_key_state_t *)userdata;
+ char line[1024];
+
+ if (!ed || !state || !state->plugin) return;
+ if (!which_key_enabled(state->plugin)) {
+ if (state->showing) ecex_message(ed, "");
+ state->showing = 0;
+ return;
+ }
+
+ if (event == ECEX_PREFIX_HOOK_CANCEL ||
+ event == ECEX_PREFIX_HOOK_FINISH ||
+ event == ECEX_PREFIX_HOOK_UNDEFINED) {
+ if (state->showing) ecex_message(ed, "");
+ state->showing = 0;
+ return;
+ }
+
+ if (event != ECEX_PREFIX_HOOK_BEGIN && event != ECEX_PREFIX_HOOK_UPDATE) {
+ return;
+ }
+
+ if (ecex_describe_key_prefix(ed, ecex_current_buffer(ed), prefix, line, sizeof(line), 24) >= 0) {
+ ecex_message(ed, line);
+ state->showing = 1;
+ }
+}
+
+static void which_key_command_hook(ecex_t *ed,
+ const char *command,
+ int event,
+ int result,
+ void *userdata) {
+ (void)command;
+ (void)result;
+
+ which_key_state_t *state = (which_key_state_t *)userdata;
+ if (event == ECEX_COMMAND_HOOK_AFTER && state && state->showing) {
+ ecex_plugin_t *plugin = state->plugin ? state->plugin : ecex_plugin_find(ed, "which-key");
+ if (plugin) {
+ ecex_message(ed, "");
+ state->showing = 0;
+ }
+ }
+}
+
+static void which_key_free(void *userdata) {
+ which_key_state_t *state = (which_key_state_t *)userdata;
+ if (!state) return;
+ ecex_plugin_object_free(state->plugin, state);
+}
+
+ECEX_PLUGIN_BEGIN(ecex_which_key_plugin, "which-key")
+ if (ecex_plugin_slot_i32_get_scalar(plugin, WK_SLOT_ENABLED, -1) < 0) {
+ ecex_plugin_slot_i32_set_scalar(plugin, WK_SLOT_ENABLED, 1);
+ }
+
+ ECEX_CONFIG_COMMAND("which-key-toggle", cmd_which_key_toggle);
+
+ which_key_state_t *state = ecex_plugin_object_calloc(plugin, "state", 1, sizeof(*state));
+ if (!state) return -1;
+ state->plugin = plugin;
+
+ if (ecex_add_command_hook(ed, WK_HOOK_NAME, which_key_command_hook, state, 0) != 0) {
+ ecex_plugin_object_free(plugin, state);
+ return -1;
+ }
+
+ if (ecex_add_prefix_hook(ed, WK_HOOK_NAME, which_key_prefix_hook, state, which_key_free) != 0) {
+ ecex_remove_command_hook(ed, WK_HOOK_NAME);
+ ecex_plugin_object_free(plugin, state);
+ return -1;
+ }
+
+ return 0;
+ECEX_PLUGIN_END
+
+#ifndef ECEX_NO_STANDALONE_CONFIG
+ECEX_CONFIG_BEGIN
+ ECEX_CONFIG_INCLUDE(ecex_which_key_plugin);
+ECEX_CONFIG_END
+#endif
diff --git a/docs/ccdjit-improvements.md b/docs/ccdjit-improvements.md
new file mode 100644
index 0000000..4b48589
--- /dev/null
+++ b/docs/ccdjit-improvements.md
@@ -0,0 +1,68 @@
+# CCDJIT improvements status for Ecex plugins
+
+Ecex now treats plugin state as a host-owned capability, but several plugin API
+constraints still exist because the JIT-to-host boundary is fragile in places.
+
+## Resolved in current CCDJIT
+
+- Multidimensional numeric arrays are usable from plugin code again.
+- Ecex added `ecex_plugin_slot_i32_get_2d` and
+ `ecex_plugin_slot_i32_set_2d` for host-owned board/grid state.
+- Higher-arity host calls are usable again; Ecex now exposes
+ `ecex_draw_plugin_text_rect_i` as a richer integer/text helper.
+- Normal allocation lowering is fixed. Ecex still requires durable plugin state
+ to live in plugin slots, plugin objects, or plugin text so cleanup and
+ cross-plugin access stay host-owned.
+- Ecex config loading now uses `ccdjit_compile_file_module` and looks up
+ `ecex_config_init` directly instead of appending a synthetic `main`.
+
+## ABI conformance tests
+
+Add a small CCDJIT test suite that calls host functions through generated code
+and covers:
+
+- integer and pointer arguments and return values;
+- float and double arguments and return values;
+- callbacks with six or more arguments;
+- structs and struct-field reads/writes;
+- arrays of string literals and `const char *` array indexing;
+- long-lived callback function pointers retained after compile-time setup.
+
+These should be standalone CCDJIT tests, not only Ecex integration tests.
+
+## Safe ABI profile
+
+Document or expose a safe host-call profile that embedders can target. Ecex
+currently favors integer-only plugin helpers, host-owned objects, and copy-based
+text because those paths are predictable.
+
+Static completion lists should use `ecex_define_word_completion_provider` plus
+`ecex_completion_provider_add_words`. That passes one string literal blob over
+the host boundary and avoids depending on JIT-side `const char *` array
+indexing.
+
+Runtime plugin callbacks should avoid variadic C library calls such as
+`snprintf`. Build strings with fixed-arity helpers or host APIs instead; the C
+tools plugin uses append helpers for this reason.
+
+## Diagnostics
+
+Improve diagnostics for:
+
+- unresolved host symbols;
+- unsafe implicit symbol resolution;
+- callback signatures that cannot be called reliably;
+- sandbox failures versus compile/type failures.
+
+## Module loading shape
+
+Ecex now uses CCDJIT's module-oriented compile path for config files. The
+remaining upstream opportunity is a first-class example/test for this embedder
+flow: compile a module, validate an init symbol, call it, retain callback code
+for later renderer/animation/mouse dispatch, and release it after host cleanup.
+
+## Capability registration
+
+Ecex has a large flat host symbol table. CCDJIT should provide examples or
+helpers for registering named capability groups so hosts can expose smaller,
+auditable APIs to plugin code.
diff --git a/docs/plugin-api.md b/docs/plugin-api.md
new file mode 100644
index 0000000..04851ed
--- /dev/null
+++ b/docs/plugin-api.md
@@ -0,0 +1,329 @@
+# Ecex Plugin API
+
+Ecex config/plugin files are C code compiled through CCDJIT and run in the
+editor process. Plugins are powerful, so the API boundary must stay explicit:
+long-lived plugin state belongs in the plugin registry, callbacks use
+host-owned objects, and cross-plugin access is read-only by explicit export.
+
+This document is the convention plugins must follow. Tetris, render-demo, and
+Markdown are the reference implementations.
+
+## Registry Convention
+
+Every plugin must register a stable string id before it allocates persistent
+state or registers callbacks:
+
+```c
+ECEX_PLUGIN_BEGIN(ecex_demo_plugin, "demo")
+ ECEX_CONFIG_COMMAND("demo", cmd_demo);
+ECEX_PLUGIN_END
+```
+
+`ECEX_PLUGIN_BEGIN` creates an `ecex_plugin_t *plugin` handle. That handle is
+the plugin's namespace in the registry. Do not use arbitrary owner pointers,
+global variables, raw malloc state, or host-specific addresses as namespaces for
+durable plugin state.
+
+Plugin ids must be stable, lowercase, and specific. Use names such as
+`markdown`, `tetris`, or `render-demo`. Changing an id changes the registry
+namespace, so treat ids as public API.
+
+## State Rules
+
+Use these storage paths:
+
+- Plugin slots: durable scalar/array state that must survive across commands,
+ renders, animation ticks, mouse callbacks, and cleanup.
+- Plugin objects: callback userdata structs passed to render, animation, mouse,
+ file-handler, or interactive callbacks.
+- Plugin text: arbitrary strings that the renderer will later resolve by
+ plugin/id.
+- Local stack variables: short-lived scratch values inside one callback only.
+
+Do not use these for durable plugin state:
+
+- `malloc`, `calloc`, or `free` directly.
+- `ecex_config_alloc`, `ecex_config_calloc`, or `ecex_config_free` for callback
+ userdata.
+- Raw editor/global static variables for mutable plugin state.
+- Host helpers created for one plugin, such as game rules or shape lookup, when
+ the plugin can own the logic itself.
+
+Current CCDJIT can lower normal allocation paths, but Ecex still treats direct
+allocation as scratch/setup memory. Anything referenced after plugin init
+returns must be in the plugin registry.
+
+## Plugin Slots
+
+Use slots for named persistent values:
+
+```c
+ecex_plugin_slot_i32_set_scalar(plugin, "score", 10);
+int score = ecex_plugin_slot_i32_get_scalar(plugin, "score", 0);
+
+int *board = ecex_plugin_slot_alloc(plugin, "board", 200, sizeof(int));
+ecex_plugin_slot_i32_set_2d(plugin, "board", 10, 3, 4, 7);
+int cell = ecex_plugin_slot_i32_get_2d(plugin, "board", 10, 3, 4, 0);
+```
+
+Rules:
+
+- Slot names are local to the plugin id.
+- Slots are private by default.
+- A plugin may mutate only its own slots.
+- Other plugins may read only explicitly exported slots.
+- Cross-plugin reads must copy into caller-owned storage.
+
+Export example:
+
+```c
+ecex_plugin_slot_set_export_flags(plugin, "score", ECEX_PLUGIN_I32, ECEX_PLUGIN_EXPORT_READ);
+```
+
+Read example:
+
+```c
+int score = 0;
+size_t len = 0;
+if (ecex_plugin_slot_read_exported(ed, "tetris", "score", &score, sizeof(score), &len) == 0) {
+ /* score copied from the exported slot */
+}
+```
+
+Never expose a mutable pointer to another plugin's slot.
+
+## Plugin Objects
+
+Use plugin objects for callback userdata:
+
+```c
+typedef struct demo_state {
+ ecex_plugin_t *plugin;
+} demo_state_t;
+
+demo_state_t *s = ecex_plugin_object_calloc(plugin, "state", 1, sizeof(*s));
+s->plugin = plugin;
+```
+
+Free the object from the callback destructor:
+
+```c
+static void demo_free(void *userdata) {
+ demo_state_t *s = (demo_state_t *)userdata;
+ if (!s) return;
+ ecex_plugin_object_free(s->plugin, s);
+}
+```
+
+Objects are tracked by the plugin runtime. Use
+`ecex_plugin_object_i32_get/set` and `ecex_plugin_object_ptr_get/set` when a
+plugin needs offset-based field access that avoids fragile JIT struct lowering.
+
+## Plugin Text
+
+Plugins must not pass arbitrary JIT-owned strings, stack buffers, or parser
+scratch memory into the real font renderer. Copy text into plugin text storage,
+then draw by plugin/id:
+
+```c
+ecex_plugin_text_set(plugin, 1, "hello", -1);
+ecex_draw_plugin_text_i(ctx, plugin, 1, x, y);
+ecex_draw_plugin_text_rect_i(ctx, plugin, 1, x, y, w, h, 8, 0x20242cff, 0xf4f1deff);
+```
+
+For buffer titles:
+
+```c
+ecex_plugin_text_set_from_buffer_title(plugin, 1, buffer);
+```
+
+Free text owned by a callback object during cleanup when appropriate:
+
+```c
+ecex_plugin_text_free_all(plugin);
+```
+
+## Rendered Plugin Callbacks
+
+Rendered plugins should use host-driven callbacks:
+
+```c
+ecex_buffer_set_renderer(buffer, draw_fn, state, free_fn, ECEX_RENDER_REPLACE_CONTENT);
+ecex_buffer_set_animation_ms(buffer, tick_fn, state, 0, 60);
+ecex_buffer_set_mouse_handler(buffer, mouse_fn, state, 0);
+```
+
+Use the millisecond animation API from plugins. Avoid the double-based animation
+API from CCDJIT plugin code.
+
+Use integer drawing helpers:
+
+- `ecex_draw_color_rgba8_i`
+- `ecex_draw_rect_i`
+- `ecex_draw_rect_outline_i`
+- `ecex_draw_line_i`
+- `ecex_draw_plugin_text_i`
+- `ecex_draw_plugin_text_rect_i`
+- `ecex_draw_label_i`
+- `ecex_draw_stat_i`
+
+The current CCDJIT ABI handles higher-arity host calls, so rectangle/text
+helpers can use richer signatures again. Prefer host-owned text ids over raw
+JIT string pointers for anything rendered after the current callback returns.
+
+## Hooks
+
+Plugins can register named hooks. Re-registering the same name replaces the old
+hook, which keeps config reloads from stacking duplicate callbacks:
+
+```c
+ecex_add_command_hook(ed, "demo", hook_fn, state, free_fn);
+ecex_add_prefix_hook(ed, "demo", prefix_fn, state, free_fn);
+ecex_add_buffer_hook(ed, "demo", buffer_fn, state, free_fn);
+```
+
+Command hooks receive `ECEX_COMMAND_HOOK_BEFORE` and
+`ECEX_COMMAND_HOOK_AFTER` around `ecex_execute_command`. Prefix hooks receive
+`ECEX_PREFIX_HOOK_BEGIN`, `UPDATE`, `CANCEL`, `FINISH`, and `UNDEFINED` for
+multi-key sequences such as `C-x`. Buffer hooks receive
+`ECEX_BUFFER_HOOK_CREATE`, `SWITCH`, `SAVE`, `KILL`, and `MODE_CHANGE`.
+
+Use `ecex_describe_key_prefix` and `ecex_message` for keymap helper plugins.
+`config/which_key_plugin.c` is the reference example: it displays the next
+available keys while a prefix sequence is active.
+
+Plugins that need external tools can check them before registering commands or
+providers:
+
+```c
+if (ecex_plugin_require_dependency(ed, "c-tools", "clangd") != 0) return 0;
+```
+
+## Completion Providers
+
+Plugins can register named completion providers. Providers receive the
+identifier prefix at point and return one candidate plus a score. Mode-specific
+providers pass a major-mode name; pass `0` for a global provider:
+
+```c
+ecex_add_completion_provider(ed, "demo-complete", "c-mode", provider_fn, state, free_fn);
+ecex_complete_at_point(ed);
+```
+
+For fixed word lists from JIT configs, prefer host-owned word providers:
+
+```c
+ecex_define_word_completion_provider(ed, "demo-words", "c-mode", ECEX_COMPLETION_DEFAULT);
+ecex_completion_provider_add_words(ed, "demo-words", "malloc\nfree\nsizeof\n");
+ecex_completion_provider_set_detail(ed, "demo-words", "C runtime symbol");
+```
+
+`ecex_completion_provider_add_words` accepts a whitespace-separated string
+literal, copies the words on the host, and avoids depending on JIT string-array
+indexing. Provider details are optional minibuffer labels shown while cycling.
+
+The built-in `indent-for-tab-command` is bound to `TAB`; `config/c_mode_plugin.c`
+defines `c-mode`, overrides `TAB` with `c-indent-line`, registers C-family file
+handlers, and adds clangd completion when `clangd` is installed. Clangd
+completions use label details when available, so functions display signatures
+and return types. `config/ecex_api_completion_plugin.c` is the `ecex-mode`
+plugin; it registers global ecex API completions and special-cases `ed->` field
+names using word completion providers.
+
+```c
+ecex_add_clangd_completion_provider(ed, "c-mode-clangd", "c-mode");
+```
+
+C-mode syntax highlighting is host-rendered for normal editable buffers once a
+buffer is assigned `c-mode`.
+
+`config/c_tools_plugin.c` registers C tooling commands and mode bindings:
+
+- `C-x c l` runs `clangd --check=<file>` for clangd/LSP diagnostics.
+- `C-x c k` runs `${CC:-clang} -fsyntax-only -Wall -Wextra <file>` as a
+ compiler linter, adding `./include` when this checkout has `include/ecex.h`.
+- `C-x c e` runs the linter with `./include` forced for ecex development.
+- `C-x c s` opens a small status/help buffer.
+
+## File Handlers
+
+File handlers are plugin-owned registrations:
+
+```c
+ECEX_CONFIG_TRY(ecex_plugin_file_handler_register(plugin, ".md", md_file_handler));
+```
+
+The handler callback may attach a renderer, set a major mode, or leave the
+buffer unchanged. Handler state must follow the same registry convention as all
+other plugin state.
+
+## Minimal Rendered Plugin
+
+```c
+#include "ecex.h"
+
+typedef struct demo_state {
+ ecex_plugin_t *plugin;
+} demo_state_t;
+
+static int demo_draw(ecex_t *ed, buffer_t *buffer, ecex_draw_context_t *ctx, void *userdata) {
+ (void)ed;
+ (void)buffer;
+ demo_state_t *s = (demo_state_t *)userdata;
+ if (!s || !ctx) return 0;
+
+ ecex_draw_color_rgba8_i(ctx, 20, 22, 28, 255);
+ ecex_draw_rect_i(ctx, 0, 0, (int)ctx->w, (int)ctx->h);
+ return 0;
+}
+
+static void demo_free(void *userdata) {
+ demo_state_t *s = (demo_state_t *)userdata;
+ if (!s) return;
+ ecex_plugin_object_free(s->plugin, s);
+}
+
+static int cmd_demo(ecex_t *ed) {
+ ecex_plugin_t *plugin = ecex_plugin_find(ed, "demo");
+ if (!plugin) return -1;
+
+ buffer_t *buffer = ecex_create_interactive_buffer(ed, "*demo*");
+ if (!buffer) return -1;
+
+ demo_state_t *state = ecex_plugin_object_calloc(plugin, "state", 1, sizeof(*state));
+ if (!state) return -1;
+ state->plugin = plugin;
+
+ if (ecex_buffer_set_renderer(buffer, demo_draw, state, demo_free, ECEX_RENDER_REPLACE_CONTENT) != 0) {
+ ecex_plugin_object_free(plugin, state);
+ return -1;
+ }
+
+ return ecex_switch_buffer(ed, "*demo*");
+}
+
+ECEX_PLUGIN_BEGIN(ecex_demo_plugin, "demo")
+ ECEX_CONFIG_COMMAND("demo", cmd_demo);
+ECEX_PLUGIN_END
+```
+
+## Cleanup Order
+
+Renderer, animation, mouse, file-handler, and command callbacks can be JIT
+function pointers. Ecex frees buffers before freeing JIT modules so callback
+destructors still point to live code. Plugin destructors should stay short:
+free plugin objects/text/slots through registry APIs and avoid complex editor
+mutation.
+
+## Logging
+
+Plugin logging is optional. Normal runs are quiet. Enable logs with:
+
+```sh
+ECEX_LOG=1
+ECEX_TRACE_CALLBACKS=1
+ECEX_TRACE_TETRIS=1
+```
+
+`ECEX_TRACE_CALLBACKS=1` also logs host callback entry/exit for renderer,
+animation, and mouse dispatch.
diff --git a/include/app.h b/include/app.h
index a420f00..9db977e 100644
--- a/include/app.h
+++ b/include/app.h
@@ -7,7 +7,8 @@
#include <GLFW/glfw3.h>
#include <stddef.h>
-#define ECEX_MINIBUFFER_SIZE 256
+#define ECEX_MINIBUFFER_SIZE 1024
+#define ECEX_MINIBUFFER_MAX_ROWS 6
#define ECEX_PREFIX_SIZE 128
typedef enum app_mode {
@@ -89,11 +90,15 @@ typedef struct app {
font_t font;
char font_path[4096];
+ unsigned long ui_revision_seen;
+ unsigned long font_revision_seen;
+ unsigned long message_revision_seen;
} app_t;
void app_init(app_t *app, ecex_t *ed);
void app_set_window(app_t *app, GLFWwindow *window);
void app_install_callbacks(app_t *app);
+void app_sync_editor_ui(app_t *app);
void app_message(app_t *app, const char *msg);
#endif
diff --git a/include/buffers.h b/include/buffers.h
index 148d718..8ffc5e9 100644
--- a/include/buffers.h
+++ b/include/buffers.h
@@ -5,6 +5,10 @@
#include <stddef.h>
+void ecex_logf(const char *fmt, ...);
+void ecex_log_group_begin(const char *message);
+void ecex_log_group_end(const char *message);
+
buffer_t *buffer_new(const char *name, const char *path, int read_only);
void buffer_free(buffer_t *buffer);
@@ -107,8 +111,8 @@ int ecex_tick_animations(ecex_t *ed, double now_seconds);
int ecex_buffer_replace_text(buffer_t *buffer, const char *text);
void ecex_buffer_set_modified(buffer_t *buffer, int modified);
int ecex_buffer_text_len(buffer_t *buffer);
-int ecex_buffer_scroll_line(buffer_t *buffer);
-int ecex_buffer_line_count_i(buffer_t *buffer);
-int ecex_buffer_line_copy(buffer_t *buffer, int line, char *out, int out_cap);
+int ecex_buffer_scroll_line_index(buffer_t *buffer);
+int ecex_buffer_line_count_int(buffer_t *buffer);
+int ecex_buffer_line_copy_text(buffer_t *buffer, int line, char *out, int out_cap);
#endif
diff --git a/include/ccdjit.h b/include/ccdjit.h
index f8a212b..7999125 100644
--- a/include/ccdjit.h
+++ b/include/ccdjit.h
@@ -33,26 +33,48 @@
* Function pointers returned by ccdjit_module_symbol() are invalid after
* ccdjit_module_free().
*
+ * Runtime policy:
+ * Module calls, symbol lookup, and emission intentionally consult the
+ * current context options, sandbox settings, resource limits, registered
+ * symbols, resolver callback, and loaded libraries. Machine code that
+ * has already been compiled may contain resolved external addresses; use
+ * a new module or function-handle replacement when changing symbol
+ * bindings must affect already compiled call sites.
+ *
* Threads:
- * Independent contexts may compile and run from different threads. Do
- * not mutate or use the same context or module concurrently from multiple
- * threads unless the host provides its own synchronization.
+ * Public API entry points are internally serialized, and module calls,
+ * symbol lookup, emission, and lazy compilation take a module lock. Raw
+ * function pointers returned by ccdjit_module_symbol() are still host
+ * pointers: do not free or replace their module while those pointers may
+ * run, and prefer typed handle/call helpers for dynamic bindings.
*
* Safety:
* Normal calls run in the host process. Set sandbox_execution to
* CCDJIT_SANDBOX_PROCESS to run ccdjit_module_call_main() in a child
- * process with optional resource, syscall, and filesystem limits.
+ * process with optional resource, syscall, and filesystem limits. Child
+ * crashes are reported through ccdjit_diagnostic.
*/
#include <stddef.h>
+#include <stdint.h>
#include <stdio.h>
#ifdef __cplusplus
extern "C" {
#endif
+#define CCDJIT_VERSION_MAJOR 1
+#define CCDJIT_VERSION_MINOR 0
+#define CCDJIT_VERSION_PATCH 0
+#define CCDJIT_VERSION_STRING "1.0.0"
+#define CCDJIT_API_ABI_VERSION 1
+#define CCDJITB2_FORMAT_VERSION 2
+#define CCDJITB2_FLAG_DEBUG_INFO 1u
+
typedef struct ccdjit_context ccdjit_context;
typedef struct ccdjit_module ccdjit_module;
+typedef struct ccdjit_function_handle ccdjit_function_handle;
+typedef struct ccdjit_execution_result ccdjit_execution_result;
typedef struct {
/* Host compiler or linker driver. NULL uses $CC, then cc. */
@@ -64,6 +86,12 @@ typedef struct {
/* Print the host linker command to stderr when nonzero. */
int verbose;
+
+ /* Wall-clock driver timeout. Zero uses the default. */
+ unsigned int timeout_ms;
+
+ /* Captured driver stdout/stderr cap. Zero uses the default. */
+ size_t output_limit_bytes;
} ccdjit_link_options;
/*
@@ -158,6 +186,9 @@ typedef struct {
*/
size_t sandbox_output_limit_bytes;
+ /* Emit and retain optional debug/source mapping metadata. */
+ int debug_info;
+
/*
* Preprocessor output byte cap. Zero uses the default currently 128
* MiB.
@@ -169,6 +200,18 @@ typedef struct {
/* Nested include cap. Zero uses the default. */
unsigned int preprocessor_include_depth_limit;
+
+ /*
+ * Optional compiler driver used for host system include and builtin
+ * macro probes. NULL auto-detects from $CC, then common cc drivers.
+ */
+ const char *preprocessor_compiler;
+
+ /* Wall-clock timeout for one compiler probe. Zero uses the default. */
+ unsigned int preprocessor_probe_timeout_ms;
+
+ /* Captured compiler-probe output cap. Zero uses the default. */
+ size_t preprocessor_probe_output_limit_bytes;
} ccdjit_options;
typedef struct {
@@ -189,18 +232,114 @@ typedef struct {
int line;
int column;
+ /* ccdjit_diagnostic_code plus optional code-specific detail. */
+ int code;
+ int subcode;
+
/* Raw child wait status or errno-style value for sandbox/API failures.
*/
int status;
/* Signal that terminated the sandbox child, or zero. */
int signal_number;
+
+ /* Faulting address for sandboxed crash diagnostics, when available. */
+ int has_fault_address;
+ uintptr_t fault_address;
} ccdjit_diagnostic;
-/* Create a compiler context. Passing NULL uses permissive CLI-like defaults.
+typedef enum {
+ CCDJIT_DIAG_NONE = 0,
+ CCDJIT_DIAG_LEXER = 1,
+ CCDJIT_DIAG_PARSER = 2,
+ CCDJIT_DIAG_PREPROCESSOR = 3,
+ CCDJIT_DIAG_TYPE = 4,
+ CCDJIT_DIAG_JIT = 5,
+ CCDJIT_DIAG_UNRESOLVED_SYMBOL = 6,
+ CCDJIT_DIAG_SANDBOX = 7,
+ CCDJIT_DIAG_SANDBOX_TIMEOUT = 8,
+ CCDJIT_DIAG_SANDBOX_FORBIDDEN_SYSCALL = 9,
+ CCDJIT_DIAG_SANDBOX_CRASH = 10,
+ CCDJIT_DIAG_INVALID_OPTIONS = 11,
+ CCDJIT_DIAG_INVALID_ARGUMENT = 12,
+ CCDJIT_DIAG_UNSUPPORTED_ABI = 13,
+ CCDJIT_DIAG_MALFORMED_BINARY = 14,
+ CCDJIT_DIAG_HOST_LINKER = 15,
+ CCDJIT_DIAG_OUT_OF_MEMORY = 16,
+} ccdjit_diagnostic_code;
+
+typedef enum {
+ CCDJIT_SYMBOL_UNKNOWN = 0,
+ CCDJIT_SYMBOL_FUNCTION = 1,
+ CCDJIT_SYMBOL_OBJECT = 2,
+} ccdjit_symbol_kind;
+
+typedef enum {
+ CCDJIT_TYPE_INT = 0,
+ CCDJIT_TYPE_CHAR = 1,
+ CCDJIT_TYPE_BOOL = 2,
+ CCDJIT_TYPE_VOID = 3,
+ CCDJIT_TYPE_FLOAT = 4,
+ CCDJIT_TYPE_DOUBLE = 5,
+ CCDJIT_TYPE_LONG = 6,
+ CCDJIT_TYPE_SHORT = 7,
+ CCDJIT_TYPE_STRUCT = 8,
+ CCDJIT_TYPE_ENUM = 9,
+ CCDJIT_TYPE_UNION = 10,
+ CCDJIT_TYPE_FLOAT128 = 11,
+} ccdjit_type_base;
+
+typedef struct {
+ int has_type;
+ int base;
+ int pointer_level;
+ int array_size;
+ int is_unsigned;
+ int is_complex;
+ int is_atomic;
+} ccdjit_type_info;
+
+typedef struct {
+ int kind;
+ int has_signature;
+ ccdjit_type_info type;
+ ccdjit_type_info return_type;
+ size_t parameter_count;
+ int is_variadic;
+ size_t size;
+} ccdjit_symbol_info;
+
+typedef struct {
+ int process;
+ int seccomp;
+ int landlock;
+} ccdjit_sandbox_capabilities;
+
+typedef struct {
+ const char *filename;
+ const char *function;
+ int line;
+ int column;
+ uintptr_t address;
+ size_t size;
+} ccdjit_debug_location;
+
+/*
+ * Create a compiler context. Passing NULL uses permissive CLI-like defaults.
+ * Invalid options return NULL; call ccdjit_context_last_error(NULL) for the
+ * thread-local creation diagnostic.
*/
ccdjit_context *ccdjit_context_new(const ccdjit_options *options);
+int ccdjit_version_major(void);
+int ccdjit_version_minor(void);
+int ccdjit_version_patch(void);
+const char *ccdjit_version_string(void);
+int ccdjit_api_abi_version(void);
+int ccdjit_binary_format_version(void);
+const char *ccdjit_git_commit(void);
+int ccdjit_sandbox_capabilities_query(ccdjit_sandbox_capabilities *out);
+
/* Free the context and all context-owned diagnostics/options/lists. */
void ccdjit_context_free(ccdjit_context *ctx);
@@ -239,6 +378,24 @@ int ccdjit_context_set_preprocessor_limits(ccdjit_context *ctx,
size_t output_limit_bytes, unsigned int macro_depth,
unsigned int include_depth);
+/* Select the compiler driver used by system include and builtin macro probes.
+ */
+int ccdjit_context_set_preprocessor_compiler(ccdjit_context *ctx,
+ const char *compiler);
+
+/* Update compiler-probe timeout/output limits. Passing zero keeps defaults. */
+int ccdjit_context_set_preprocessor_probe_limits(ccdjit_context *ctx,
+ unsigned int timeout_ms, size_t output_limit_bytes);
+
+/* Free this thread's cached compiler probe results and system include paths. */
+void ccdjit_preprocessor_free_probe_cache(void);
+
+/* Enable or disable executable memory for future compile/load/call paths. */
+int ccdjit_context_set_executable_memory(ccdjit_context *ctx, int allow);
+
+/* Enable or disable optional debug/source mapping metadata. */
+int ccdjit_context_set_debug_info(ccdjit_context *ctx, int enable);
+
/*
* Add a Landlock filesystem root for sandboxed execution.
*
@@ -249,13 +406,40 @@ int ccdjit_context_set_preprocessor_limits(ccdjit_context *ctx,
int ccdjit_context_add_sandbox_filesystem_root(ccdjit_context *ctx,
const char *path, int writable);
-/* Return the last error for this context. The pointer is context-owned. */
+/*
+ * Return the last error for this context. Passing NULL returns the
+ * thread-local diagnostic for context creation or NULL-context API failures.
+ * The pointer is context-owned or thread-local.
+ */
const ccdjit_diagnostic *ccdjit_context_last_error(ccdjit_context *ctx);
/* Return captured sandbox stdout/stderr from the last sandboxed main call. */
const char *ccdjit_context_last_stdout(ccdjit_context *ctx);
const char *ccdjit_context_last_stderr(ccdjit_context *ctx);
+/*
+ * Owned execution result snapshot for bindings and concurrent hosts.
+ *
+ * Result objects keep copies of the call status, integer result, diagnostic,
+ * captured stdout, and captured stderr. They are not invalidated by later
+ * calls on the same context.
+ */
+void ccdjit_execution_result_free(ccdjit_execution_result *result);
+int ccdjit_execution_result_status(const ccdjit_execution_result *result);
+int ccdjit_execution_result_has_value(const ccdjit_execution_result *result);
+int ccdjit_execution_result_value(const ccdjit_execution_result *result);
+const ccdjit_diagnostic *ccdjit_execution_result_diagnostic(
+ const ccdjit_execution_result *result);
+const char *ccdjit_execution_result_stdout(
+ const ccdjit_execution_result *result);
+const char *ccdjit_execution_result_stderr(
+ const ccdjit_execution_result *result);
+int ccdjit_execution_result_signal(const ccdjit_execution_result *result);
+int ccdjit_execution_result_has_fault_address(
+ const ccdjit_execution_result *result);
+uintptr_t ccdjit_execution_result_fault_address(
+ const ccdjit_execution_result *result);
+
/* Compile a source file into a live JIT module. */
int ccdjit_compile_file(ccdjit_context *ctx, const char *path,
ccdjit_module **out);
@@ -291,9 +475,75 @@ int ccdjit_eval_string(ccdjit_context *ctx, const char *source,
*/
void *ccdjit_module_symbol(ccdjit_module *module, const char *name);
+/*
+ * Inspect a module symbol without forcing the caller to cast blindly.
+ *
+ * JIT functions expose return type, parameter count, variadic flag, and
+ * per-parameter metadata through ccdjit_module_function_parameter_type().
+ * Object symbols expose their object type and size. Loaded CCDJITB2 binaries
+ * expose function/object kind and size but not full C signatures.
+ */
+int ccdjit_module_symbol_info(ccdjit_module *module, const char *name,
+ ccdjit_symbol_info *out);
+
+int ccdjit_module_function_parameter_type(ccdjit_module *module,
+ const char *name, size_t index, ccdjit_type_info *out);
+
+/*
+ * Return a callable function pointer only when the JIT signature exactly
+ * matches the expected return and parameter types.
+ */
+void *ccdjit_module_function_symbol_checked(ccdjit_module *module,
+ const char *name, const ccdjit_type_info *return_type,
+ const ccdjit_type_info *parameter_types, size_t parameter_count,
+ int allow_variadic);
+
+/* Map a live JIT instruction address to lightweight module-owned debug info. */
+int ccdjit_module_debug_location(ccdjit_module *module, const void *address,
+ ccdjit_debug_location *out);
+
/* Call module main(argc, argv). Honors CCDJIT_SANDBOX_PROCESS. */
int ccdjit_module_call_main(ccdjit_module *module, int argc, char **argv,
int *result_out);
+int ccdjit_module_call_main_result(ccdjit_module *module, int argc, char **argv,
+ ccdjit_execution_result **out);
+
+/*
+ * Call an exported JIT function with signature int name(int).
+ *
+ * Honors CCDJIT_SANDBOX_PROCESS like ccdjit_module_call_main(). Other
+ * signatures and binary modules are rejected with a diagnostic.
+ */
+int ccdjit_module_call_i32(ccdjit_module *module, const char *symbol, int arg,
+ int *result_out);
+int ccdjit_module_call_i32_result(ccdjit_module *module, const char *symbol,
+ int arg, ccdjit_execution_result **out);
+
+/*
+ * Compile and publish a named function behind a stable handle.
+ *
+ * The initial source and every replacement must define the same function
+ * signature. Calls through the handle do not expose raw function pointers, so
+ * replacements can wait for active callers and then free old generated code.
+ */
+int ccdjit_function_handle_new(ccdjit_context *ctx, const char *source,
+ const char *filename, const char *symbol, ccdjit_function_handle **out);
+
+/*
+ * Compile replacement source and atomically publish it on success.
+ *
+ * On compile failure or signature mismatch the old implementation remains
+ * active and the context diagnostic describes the failure.
+ */
+int ccdjit_function_handle_replace(ccdjit_function_handle *handle,
+ const char *source, const char *filename);
+
+/* Call a handle whose published function has signature int name(int). */
+int ccdjit_function_handle_call_i32(ccdjit_function_handle *handle, int arg,
+ int *result_out);
+
+/* Free a function handle and the currently published implementation. */
+void ccdjit_function_handle_free(ccdjit_function_handle *handle);
/* Emit textual assembly-like bytes for inspection. */
int ccdjit_module_emit_assembly(ccdjit_module *module, FILE *out);
diff --git a/include/ecex.h b/include/ecex.h
index efcbe5a..085c3d2 100644
--- a/include/ecex.h
+++ b/include/ecex.h
@@ -6,6 +6,7 @@
#include "eval.h"
#include "path.h"
#include "media.h"
+#include "plugin.h"
typedef struct ecex_config_command {
@@ -24,6 +25,9 @@ typedef struct ecex_config_mode_keybind {
const char *command;
} ecex_config_mode_keybind_t;
+#define ECEX_COMPLETION_DEFAULT 0
+#define ECEX_COMPLETION_ED_ARROW 1
+
ecex_t *ecex_new(void);
void ecex_free(ecex_t *ed);
@@ -35,61 +39,34 @@ void *ecex_config_calloc(size_t count, size_t size);
void ecex_config_free(void *ptr);
double ecex_time_seconds(void);
void ecex_log(const char *message);
+void ecex_logf(const char *fmt, ...);
+void ecex_log_group_begin(const char *message);
+void ecex_log_group_end(const char *message);
void ecex_log_int(const char *message, int value);
void ecex_log_double(const char *message, double value);
void ecex_log_ptr(const char *message, const void *ptr);
+void ecex_log_flush(void);
void ecex_mem_zero(void *ptr, size_t size);
int ecex_i32_get(const int *items, size_t index);
void ecex_i32_set(int *items, size_t index, int value);
int ecex_prng_next_bounded(unsigned int *state, int bound);
int ecex_random_bounded(int bound);
-int ecex_tetris_shape_cell(int piece, int rot, int col, int row);
-
-/* Host-owned variable registry for JIT plugins. Plugins can keep persistent
- * scalar values, dynamic arrays, or static host buffers behind a stable owner
- * pointer and a name instead of relying on fragile JIT struct/array storage. */
-void *ecex_var_get(ecex_t *ed, void *owner, const char *name);
-void *ecex_var_get_or_alloc(ecex_t *ed, void *owner, const char *name, size_t count, size_t elem_size);
-int ecex_var_bind_static(ecex_t *ed, void *owner, const char *name, void *data, size_t count, size_t elem_size);
-int ecex_var_free(ecex_t *ed, void *owner, const char *name);
-int ecex_var_free_owner(ecex_t *ed, void *owner);
-int ecex_var_i32_get(ecex_t *ed, void *owner, const char *name, size_t index, int fallback);
-int ecex_var_i32_set(ecex_t *ed, void *owner, const char *name, size_t index, int value);
-int ecex_var_i32(ecex_t *ed, void *owner, const char *name, int fallback);
-int ecex_var_i32_set_scalar(ecex_t *ed, void *owner, const char *name, int value);
-
-/* Host-owned object allocator for plugin state. Returned pointers are tracked
- * by the editor and can be freed through the API. Prefer this for callback
- * userdata over plugin/local allocations when the object has callback lifetime.
- */
-void *ecex_object_alloc(ecex_t *ed, size_t size);
-void *ecex_object_calloc(ecex_t *ed, size_t count, size_t size);
-int ecex_object_free(ecex_t *ed, void *object);
-int ecex_object_valid(ecex_t *ed, void *object);
-int ecex_object_i32_get(ecex_t *ed, void *object, size_t byte_offset, int fallback);
-int ecex_object_i32_set(ecex_t *ed, void *object, size_t byte_offset, int value);
-void *ecex_object_ptr_get(ecex_t *ed, void *object, size_t byte_offset);
-int ecex_object_ptr_set(ecex_t *ed, void *object, size_t byte_offset, void *value);
-
-/* Host-owned text registry for plugins that need to render arbitrary text.
- * Plugins copy text into host storage, then draw by owner/id. This avoids
- * passing JIT-owned literals or stack strings into the normal text renderer. */
-int ecex_text_set(ecex_t *ed, void *owner, int id, const char *text, int len);
-int ecex_text_set_buffer_title(ecex_t *ed, void *owner, int id, buffer_t *buffer);
-int ecex_text_free(ecex_t *ed, void *owner, int id);
-int ecex_text_free_owner(ecex_t *ed, void *owner);
+
+/* The plugin runtime owns long-lived JIT/plugin state. Plugins must register a
+ * stable id and then allocate slots, objects, text, and file handlers through
+ * the returned opaque handle. Slots are private unless explicitly exported,
+ * and cross-plugin reads are copy-only. */
int ecex_buffer_text_len(buffer_t *buffer);
-int ecex_buffer_scroll_line(buffer_t *buffer);
-int ecex_buffer_line_count_i(buffer_t *buffer);
-int ecex_buffer_line_copy(buffer_t *buffer, int line, char *out, int out_cap);
-int ecex_markdown_draw_line_from_buffer_i(ecex_draw_context_t *ctx, void *owner, buffer_t *buffer, int line, int y, int in_code);
-int ecex_markdown_body_y_i(ecex_draw_context_t *ctx);
-int ecex_draw_context_height_i(ecex_draw_context_t *ctx);
-int ecex_draw_context_line_height_i(ecex_draw_context_t *ctx);
+int ecex_buffer_scroll_line_index(buffer_t *buffer);
+int ecex_buffer_line_count_int(buffer_t *buffer);
+int ecex_buffer_line_copy_text(buffer_t *buffer, int line, char *out, int out_cap);
+int ecex_markdown_draw_buffer_line_i(ecex_draw_context_t *ctx, void *owner, buffer_t *buffer, int line, int y, int in_code);
+int ecex_markdown_body_y_px(ecex_draw_context_t *ctx);
+int ecex_draw_context_height_px(ecex_draw_context_t *ctx);
+int ecex_draw_context_line_height_px(ecex_draw_context_t *ctx);
-int ecex_register_file_handler(ecex_t *ed, const char *extension, ecex_file_handler_fn fn);
-int ecex_run_file_handlers(ecex_t *ed, buffer_t *buffer);
+int ecex_run_plugin_file_handlers(ecex_t *ed, buffer_t *buffer);
int ecex_reserve_buffers(ecex_t *ed, size_t needed);
int ecex_add_buffer(ecex_t *ed, buffer_t *buffer);
@@ -137,6 +114,52 @@ int ecex_apply_theme(ecex_t *ed, const ecex_theme_t *theme);
int ecex_register_command(ecex_t *ed, const char *name, ecex_command_fn fn);
int ecex_execute_command(ecex_t *ed, const char *name);
+int ecex_add_command_hook(ecex_t *ed,
+ const char *name,
+ ecex_command_hook_fn fn,
+ void *userdata,
+ ecex_hook_free_fn free_fn);
+int ecex_remove_command_hook(ecex_t *ed, const char *name);
+int ecex_add_prefix_hook(ecex_t *ed,
+ const char *name,
+ ecex_prefix_hook_fn fn,
+ void *userdata,
+ ecex_hook_free_fn free_fn);
+int ecex_remove_prefix_hook(ecex_t *ed, const char *name);
+void ecex_notify_prefix_hooks(ecex_t *ed, const char *prefix, int event);
+int ecex_add_buffer_hook(ecex_t *ed,
+ const char *name,
+ ecex_buffer_hook_fn fn,
+ void *userdata,
+ ecex_hook_free_fn free_fn);
+int ecex_remove_buffer_hook(ecex_t *ed, const char *name);
+void ecex_notify_buffer_hooks(ecex_t *ed, buffer_t *buffer, int event);
+void ecex_message(ecex_t *ed, const char *message);
+int ecex_dependency_available(const char *program);
+int ecex_plugin_require_dependency(ecex_t *ed, const char *plugin_id, const char *program);
+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);
+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);
+int ecex_define_word_completion_provider(ecex_t *ed,
+ const char *name,
+ const char *mode_name,
+ int flags);
+int ecex_completion_provider_add_word(ecex_t *ed, const char *name, const char *word);
+int ecex_completion_provider_add_words(ecex_t *ed, const char *name, const char *words);
+int ecex_completion_provider_set_detail(ecex_t *ed, const char *name, const char *detail);
+int ecex_add_clangd_completion_provider(ecex_t *ed, const char *name, const char *mode_name);
+int ecex_remove_completion_provider(ecex_t *ed, const char *name);
+int ecex_buffer_identifier_prefix(buffer_t *buffer, char *out, size_t out_size);
+int ecex_complete_at_point(ecex_t *ed);
void ecex_set_clipboard_callbacks(ecex_t *ed,
ecex_clipboard_get_fn get_fn,
ecex_clipboard_set_fn set_fn,
@@ -156,6 +179,12 @@ const char *ecex_buffer_major_mode_name(ecex_t *ed, buffer_t *buffer);
int ecex_bind_mode_key(ecex_t *ed, const char *mode_name, const char *key, const char *command);
const char *ecex_lookup_key_for_buffer(ecex_t *ed, buffer_t *buffer, const char *key);
int ecex_key_sequence_has_prefix_for_buffer(ecex_t *ed, buffer_t *buffer, const char *prefix);
+int ecex_describe_key_prefix(ecex_t *ed,
+ buffer_t *buffer,
+ const char *prefix,
+ char *out,
+ size_t out_size,
+ size_t max_items);
int ecex_auto_set_major_mode(ecex_t *ed, buffer_t *buffer);
int ecex_list_commands(ecex_t *ed);
@@ -244,12 +273,13 @@ void ecex_draw_rgba(ecex_draw_context_t *ctx,
/* CCDJIT-safe integer drawing wrappers. Prefer these from plugins when the
* JIT ABI is known to be fragile with float/double arguments. Colors are
* 0..255 RGBA and coordinates are integer pixels local to the buffer window. */
-void ecex_draw_color_rgba8(ecex_draw_context_t *ctx, int r, int g, int b, int a);
+void ecex_draw_color_rgba8_i(ecex_draw_context_t *ctx, int r, int g, int b, int a);
void ecex_draw_rect_i(ecex_draw_context_t *ctx, int x, int y, int w, int h);
void ecex_draw_rect_outline_i(ecex_draw_context_t *ctx, int x, int y, int w, int h, int thickness);
void ecex_draw_line_i(ecex_draw_context_t *ctx, int x1, int y1, int x2, int y2, int thickness);
void ecex_draw_text_i(ecex_draw_context_t *ctx, int x, int y, const char *text);
-void ecex_draw_text_id_i(ecex_draw_context_t *ctx, void *owner, int id, int x, int y);
+void ecex_draw_plugin_text_i(ecex_draw_context_t *ctx, void *owner, int id, int x, int y);
+void ecex_draw_plugin_text_rect_i(ecex_draw_context_t *ctx, void *owner, int id, int x, int y, int w, int h, int padding, unsigned int bg_rgba, unsigned int fg_rgba);
void ecex_draw_markdown_canvas_i(ecex_draw_context_t *ctx, void *owner, int title_id, int x, int y, int w, int line_h);
void ecex_draw_markdown_text_i(ecex_draw_context_t *ctx, void *owner, int text_id, int x, int y, int w, int line_h, int style);
@@ -273,6 +303,7 @@ int ecex_rerun_compile(ecex_t *ed);
int ecex_rerun_grep(ecex_t *ed);
int ecex_next_interactive_action(ecex_t *ed);
int ecex_previous_interactive_action(ecex_t *ed);
+int ecex_indent_line_to(buffer_t *buffer, int target_cols);
int ecex_comment_region(ecex_t *ed);
int ecex_uncomment_region(ecex_t *ed);
void ecex_request_prompt(ecex_t *ed, ecex_prompt_request_t request, const char *message);
@@ -320,7 +351,9 @@ void ecex_set_current_line_enabled(ecex_t *ed, int enabled);
#define ECEX_RGB8(r, g, b) ((float)(r) / 255.0f), ((float)(g) / 255.0f), ((float)(b) / 255.0f)
#define ECEX_CONFIG_BEGIN int ecex_config_init(ecex_t *ed) {
#define ECEX_CONFIG_END return 0; }
-#define ECEX_PLUGIN_BEGIN(name) int name(ecex_t *ed) {
+#define ECEX_PLUGIN_BEGIN(name, id) int name(ecex_t *ed) { \
+ ecex_plugin_t *plugin = ecex_plugin_require(ed, (id), ECEX_PLUGIN_API_VERSION); \
+ if (!plugin) return -1;
#define ECEX_PLUGIN_END return 0; }
#define ECEX_CONFIG_TRY(expr) \
do { \
diff --git a/include/libccdjit.a b/include/libccdjit.a
index 3045696..d062f38 100644
--- a/include/libccdjit.a
+++ b/include/libccdjit.a
Binary files differ
diff --git a/include/libccdjit.so b/include/libccdjit.so
index 40456db..c3d9203 100755
--- a/include/libccdjit.so
+++ b/include/libccdjit.so
Binary files differ
diff --git a/include/plugin.h b/include/plugin.h
new file mode 100644
index 0000000..50aad33
--- /dev/null
+++ b/include/plugin.h
@@ -0,0 +1,110 @@
+#ifndef ECEX_PLUGIN_H
+#define ECEX_PLUGIN_H
+
+#include "types.h"
+
+#include <stddef.h>
+#include <stdint.h>
+
+#define ECEX_PLUGIN_API_VERSION 1
+#define ECEX_PLUGIN_EXPORT_READ 1
+
+typedef struct ecex_plugin ecex_plugin_t;
+typedef struct ecex_plugin_runtime ecex_plugin_runtime_t;
+
+typedef enum ecex_plugin_value_type {
+ ECEX_PLUGIN_BYTES = 0,
+ ECEX_PLUGIN_I32 = 1,
+ ECEX_PLUGIN_U32 = 2,
+ ECEX_PLUGIN_I64 = 3,
+ ECEX_PLUGIN_PTR = 4,
+} ecex_plugin_value_type_t;
+
+ecex_plugin_runtime_t *ecex_plugin_runtime_new(void);
+void ecex_plugin_runtime_free(ecex_plugin_runtime_t *runtime);
+
+ecex_plugin_t *ecex_plugin_register(ecex_t *ed, const char *id, int api_version);
+ecex_plugin_t *ecex_plugin_require(ecex_t *ed, const char *id, int api_version);
+ecex_plugin_t *ecex_plugin_find(ecex_t *ed, const char *id);
+const char *ecex_plugin_id(ecex_plugin_t *plugin);
+
+/* Plugin slots are named, plugin-private arrays unless explicitly exported. */
+void *ecex_plugin_slot_alloc(ecex_plugin_t *plugin,
+ const char *name,
+ size_t count,
+ size_t elem_size);
+void *ecex_plugin_slot_get(ecex_plugin_t *plugin, const char *name);
+int ecex_plugin_slot_free(ecex_plugin_t *plugin, const char *name);
+int ecex_plugin_slot_set_export_flags(ecex_plugin_t *plugin,
+ const char *name,
+ int type,
+ int flags);
+int ecex_plugin_slot_read_exported(ecex_t *ed,
+ const char *plugin_id,
+ const char *name,
+ void *out,
+ size_t out_cap,
+ size_t *out_len);
+
+int ecex_plugin_slot_i32_get_scalar(ecex_plugin_t *plugin, const char *name, int fallback);
+int ecex_plugin_slot_i32_get(ecex_plugin_t *plugin,
+ const char *name,
+ size_t index,
+ int fallback);
+int ecex_plugin_slot_i32_set(ecex_plugin_t *plugin,
+ const char *name,
+ size_t index,
+ int value);
+int ecex_plugin_slot_i32_get_2d(ecex_plugin_t *plugin,
+ const char *name,
+ size_t width,
+ size_t x,
+ size_t y,
+ int fallback);
+int ecex_plugin_slot_i32_set_2d(ecex_plugin_t *plugin,
+ const char *name,
+ size_t width,
+ size_t x,
+ size_t y,
+ int value);
+int ecex_plugin_slot_i32_set_scalar(ecex_plugin_t *plugin,
+ const char *name,
+ int value);
+
+/* Plugin objects are tracked allocations intended for callback userdata. */
+void *ecex_plugin_object_alloc(ecex_plugin_t *plugin, const char *name, size_t size);
+void *ecex_plugin_object_calloc(ecex_plugin_t *plugin,
+ const char *name,
+ size_t count,
+ size_t size);
+int ecex_plugin_object_free(ecex_plugin_t *plugin, void *object);
+int ecex_plugin_object_valid(ecex_plugin_t *plugin, void *object);
+int ecex_plugin_object_i32_get(ecex_plugin_t *plugin,
+ void *object,
+ size_t byte_offset,
+ int fallback);
+int ecex_plugin_object_i32_set(ecex_plugin_t *plugin,
+ void *object,
+ size_t byte_offset,
+ int value);
+void *ecex_plugin_object_ptr_get(ecex_plugin_t *plugin,
+ void *object,
+ size_t byte_offset);
+int ecex_plugin_object_ptr_set(ecex_plugin_t *plugin,
+ void *object,
+ size_t byte_offset,
+ void *value);
+
+/* Plugin text stores host-owned strings for later draw-time lookup. */
+int ecex_plugin_text_set(ecex_plugin_t *plugin, int id, const char *text, int len);
+int ecex_plugin_text_set_from_buffer_title(ecex_plugin_t *plugin, int id, buffer_t *buffer);
+int ecex_plugin_text_free(ecex_plugin_t *plugin, int id);
+int ecex_plugin_text_free_all(ecex_plugin_t *plugin);
+const char *ecex_plugin_text_get_drawable(ecex_t *ed, void *owner, int id);
+
+int ecex_plugin_file_handler_register(ecex_plugin_t *plugin,
+ const char *extension,
+ ecex_file_handler_fn fn);
+int ecex_plugin_file_handlers_run(ecex_t *ed, buffer_t *buffer);
+
+#endif
diff --git a/include/types.h b/include/types.h
index 800c3d6..2b63ad3 100644
--- a/include/types.h
+++ b/include/types.h
@@ -7,6 +7,7 @@ typedef struct ecex ecex_t;
typedef struct buffer buffer_t;
typedef struct ecex_window ecex_window_t;
typedef struct ecex_draw_context ecex_draw_context_t;
+typedef struct ecex_plugin_runtime ecex_plugin_runtime_t;
typedef int (*ecex_command_fn)(ecex_t *ed);
typedef int (*ecex_interactive_line_fn)(ecex_t *ed, buffer_t *buffer, size_t line, const char *payload, void *userdata);
@@ -18,6 +19,37 @@ typedef int (*ecex_buffer_tick_ms_fn)(ecex_t *ed, buffer_t *buffer, int now_ms,
typedef int (*ecex_buffer_mouse_fn)(ecex_t *ed, buffer_t *buffer, int event, int x, int y, int button, void *userdata);
typedef int (*ecex_file_handler_fn)(ecex_t *ed, buffer_t *buffer);
typedef void (*ecex_buffer_userdata_free_fn)(void *userdata);
+typedef void (*ecex_hook_free_fn)(void *userdata);
+typedef void (*ecex_command_hook_fn)(ecex_t *ed, const char *command, int event, int result, void *userdata);
+typedef void (*ecex_prefix_hook_fn)(ecex_t *ed, const char *prefix, int event, void *userdata);
+typedef void (*ecex_buffer_hook_fn)(ecex_t *ed, buffer_t *buffer, int event, void *userdata);
+typedef int (*ecex_completion_provider_fn)(ecex_t *ed,
+ buffer_t *buffer,
+ const char *prefix,
+ char *out,
+ size_t out_size,
+ void *userdata);
+
+typedef enum ecex_command_hook_event {
+ ECEX_COMMAND_HOOK_BEFORE = 1,
+ ECEX_COMMAND_HOOK_AFTER = 2,
+} ecex_command_hook_event_t;
+
+typedef enum ecex_prefix_hook_event {
+ ECEX_PREFIX_HOOK_BEGIN = 1,
+ ECEX_PREFIX_HOOK_UPDATE = 2,
+ ECEX_PREFIX_HOOK_CANCEL = 3,
+ ECEX_PREFIX_HOOK_FINISH = 4,
+ ECEX_PREFIX_HOOK_UNDEFINED = 5,
+} ecex_prefix_hook_event_t;
+
+typedef enum ecex_buffer_hook_event {
+ ECEX_BUFFER_HOOK_CREATE = 1,
+ ECEX_BUFFER_HOOK_SWITCH = 2,
+ ECEX_BUFFER_HOOK_SAVE = 3,
+ ECEX_BUFFER_HOOK_KILL = 4,
+ ECEX_BUFFER_HOOK_MODE_CHANGE = 5,
+} ecex_buffer_hook_event_t;
typedef enum ecex_prompt_request {
ECEX_PROMPT_NONE = 0,
@@ -154,6 +186,7 @@ struct buffer {
void *media_pipe;
double media_last_frame_time;
int media_playing;
+ int media_audio_pid;
char media_status[256];
ecex_buffer_render_fn render_fn;
@@ -198,42 +231,43 @@ typedef struct ecex_mode_keybind {
char *command;
} ecex_mode_keybind_t;
-typedef struct ecex_major_mode {
- int id;
+typedef struct ecex_command_hook {
char *name;
-} ecex_major_mode_t;
+ ecex_command_hook_fn fn;
+ void *userdata;
+ ecex_hook_free_fn free_fn;
+} ecex_command_hook_t;
-typedef enum ecex_var_kind {
- ECEX_VAR_BYTES = 0,
- ECEX_VAR_I32 = 1,
-} ecex_var_kind_t;
+typedef struct ecex_prefix_hook {
+ char *name;
+ ecex_prefix_hook_fn fn;
+ void *userdata;
+ ecex_hook_free_fn free_fn;
+} ecex_prefix_hook_t;
-typedef struct ecex_var_entry {
- void *owner;
+typedef struct ecex_buffer_hook {
char *name;
- void *data;
- size_t elem_size;
- size_t count;
- int kind;
- int dynamic;
-} ecex_var_entry_t;
-
-typedef struct ecex_text_entry {
- void *owner;
- int id;
- char *text;
- size_t len;
-} ecex_text_entry_t;
+ ecex_buffer_hook_fn fn;
+ void *userdata;
+ ecex_hook_free_fn free_fn;
+} ecex_buffer_hook_t;
-typedef struct ecex_file_handler {
- char *extension;
- ecex_file_handler_fn fn;
-} ecex_file_handler_t;
+typedef struct ecex_completion_provider {
+ char *name;
+ int mode;
+ ecex_completion_provider_fn fn;
+ void *userdata;
+ ecex_hook_free_fn free_fn;
+ char *detail;
+ char **words;
+ size_t word_count;
+ int flags;
+} ecex_completion_provider_t;
-typedef struct ecex_object_entry {
- void *ptr;
- size_t size;
-} ecex_object_entry_t;
+typedef struct ecex_major_mode {
+ int id;
+ char *name;
+} ecex_major_mode_t;
struct ecex_window {
buffer_t *buffer;
@@ -273,26 +307,34 @@ struct ecex {
size_t mode_keybind_cap;
size_t mode_keybind_count;
+ ecex_command_hook_t *command_hooks;
+ size_t command_hook_cap;
+ size_t command_hook_count;
+
+ ecex_prefix_hook_t *prefix_hooks;
+ size_t prefix_hook_cap;
+ size_t prefix_hook_count;
+
+ ecex_buffer_hook_t *buffer_hooks;
+ size_t buffer_hook_cap;
+ size_t buffer_hook_count;
+
+ ecex_completion_provider_t *completion_providers;
+ size_t completion_provider_cap;
+ size_t completion_provider_count;
+ int completion_cycle_active;
+ buffer_t *completion_cycle_buffer;
+ size_t completion_cycle_start;
+ size_t completion_cycle_index;
+ char completion_cycle_prefix[256];
+ char completion_cycle_current[256];
+
ecex_major_mode_t *major_modes;
size_t major_mode_cap;
size_t major_mode_count;
int next_major_mode_id;
- ecex_var_entry_t *vars;
- size_t var_cap;
- size_t var_count;
-
- ecex_text_entry_t *texts;
- size_t text_cap;
- size_t text_count;
-
- ecex_file_handler_t *file_handlers;
- size_t file_handler_cap;
- size_t file_handler_count;
-
- ecex_object_entry_t *objects;
- size_t object_cap;
- size_t object_count;
+ ecex_plugin_runtime_t *plugins;
char *last_eval_source;
char *last_eval_filename;
@@ -305,13 +347,19 @@ struct ecex {
char prompt_message[128];
char *config_path;
+ char message[1024];
+ unsigned long message_revision;
ecex_clipboard_get_fn clipboard_get;
ecex_clipboard_set_fn clipboard_set;
void *clipboard_userdata;
+ char *clipboard_text;
int should_quit;
+ unsigned long ui_revision;
+ unsigned long font_revision;
+
ecex_theme_t theme;
};
diff --git a/src/app.c b/src/app.c
index 9a38e40..b9840de 100644
--- a/src/app.c
+++ b/src/app.c
@@ -770,6 +770,36 @@ static void app_reload_font_if_needed(app_t *app) {
app->dirty = 1;
}
+void app_sync_editor_ui(app_t *app) {
+ if (!app || !app->ed) return;
+
+ const char *path = app->ed->theme.font_path && app->ed->theme.font_path[0]
+ ? app->ed->theme.font_path
+ : app->font_path;
+ int font_path_mismatch =
+ path && path[0] &&
+ (!app->font_path[0] || strcmp(app->font_path, path) != 0);
+ int font_size_mismatch = app->font.size_px != app->ed->theme.font_size;
+
+ if (app->font_revision_seen != app->ed->font_revision ||
+ font_path_mismatch ||
+ font_size_mismatch) {
+ app_reload_font_if_needed(app);
+ app->font_revision_seen = app->ed->font_revision;
+ }
+
+ if (app->ui_revision_seen != app->ed->ui_revision) {
+ app->ui_revision_seen = app->ed->ui_revision;
+ app->dirty = 1;
+ }
+
+ if (app->message_revision_seen != app->ed->message_revision) {
+ app->message_revision_seen = app->ed->message_revision;
+ snprintf(app->message, sizeof(app->message), "%s", app->ed->message);
+ app->dirty = 1;
+ }
+}
+
static int app_handle_prompt_request(app_t *app) {
if (!app || !app->ed) return 0;
@@ -797,7 +827,7 @@ static int app_execute_command(app_t *app, const char *command) {
int result = ecex_execute_command(app->ed, command);
if (result == 0) {
- app_reload_font_if_needed(app);
+ app_sync_editor_ui(app);
app_handle_prompt_request(app);
}
@@ -915,9 +945,7 @@ static void app_submit_prompt(app_t *app) {
break;
}
- if (result == 0) {
- app_reload_font_if_needed(app);
- }
+ if (result == 0) app_sync_editor_ui(app);
char msg[ECEX_MINIBUFFER_SIZE];
if (result == 0) {
@@ -989,12 +1017,15 @@ static void app_enter_prefix(app_t *app, const char *first_key) {
app->prefix_len = strlen(app->prefix);
app->message[0] = '\0';
+ ecex_notify_prefix_hooks(app->ed, app->prefix, ECEX_PREFIX_HOOK_BEGIN);
+ app_sync_editor_ui(app);
app->dirty = 1;
}
static void app_cancel_prefix(app_t *app) {
if (!app) return;
+ ecex_notify_prefix_hooks(app->ed, app->prefix, ECEX_PREFIX_HOOK_CANCEL);
app->mode = APP_MODE_EDIT;
app->prefix[0] = '\0';
app->prefix_len = 0;
@@ -1032,6 +1063,8 @@ static void app_finish_prefix(app_t *app) {
const char *command = ecex_lookup_key_for_buffer(app->ed, ecex_current_buffer(app->ed), app->prefix);
if (command) {
+ ecex_notify_prefix_hooks(app->ed, app->prefix, ECEX_PREFIX_HOOK_FINISH);
+ app_sync_editor_ui(app);
if (app_execute_command(app, command) == 0) {
if (app->mode == APP_MODE_EDIT) {
char msg[ECEX_MINIBUFFER_SIZE];
@@ -1059,6 +1092,8 @@ static void app_finish_prefix(app_t *app) {
}
if (key_sequence_has_prefix(app, app->prefix)) {
+ ecex_notify_prefix_hooks(app->ed, app->prefix, ECEX_PREFIX_HOOK_UPDATE);
+ app_sync_editor_ui(app);
app->dirty = 1;
return;
}
@@ -1066,6 +1101,7 @@ static void app_finish_prefix(app_t *app) {
char msg[ECEX_MINIBUFFER_SIZE];
snprintf(msg, sizeof(msg), "Undefined key: %s", app->prefix);
+ ecex_notify_prefix_hooks(app->ed, app->prefix, ECEX_PREFIX_HOOK_UNDEFINED);
app->mode = APP_MODE_EDIT;
app->prefix[0] = '\0';
app->prefix_len = 0;
@@ -1091,7 +1127,24 @@ static float app_status_height(app_t *app) {
return line_h + pad_y * 2.0f;
}
-static float app_minibuffer_height(app_t *app) { return app_status_height(app); }
+static size_t app_minibuffer_row_count(app_t *app) {
+ if (!app || app->mode != APP_MODE_PREFIX || !app->message[0]) return 1;
+
+ size_t rows = 1;
+ for (const char *p = app->message; *p; ++p) {
+ if (*p != '\n') continue;
+ rows++;
+ if (rows >= ECEX_MINIBUFFER_MAX_ROWS) return ECEX_MINIBUFFER_MAX_ROWS;
+ }
+ return rows;
+}
+
+static float app_minibuffer_height(app_t *app) {
+ float line_h = app->font.line_height > 1.0f ? app->font.line_height : app->font.size_px * 1.2f;
+ float pad_y = app->font.size_px * 0.35f;
+ if (pad_y < 4.0f) pad_y = 4.0f;
+ return line_h * (float)app_minibuffer_row_count(app) + pad_y * 2.0f;
+}
static int app_minibuffer_visible(app_t *app) {
return app->mode == APP_MODE_MX || app->mode == APP_MODE_PREFIX ||
@@ -1182,6 +1235,29 @@ static int app_window_rect_at(app_t *app,
return 0;
}
+static int app_page_scroll_rendered_buffer(app_t *app, int direction) {
+ if (!app || !app->ed || direction == 0) return 0;
+
+ ecex_window_t *win = ecex_current_window(app->ed);
+ buffer_t *buf = win && win->buffer ? win->buffer : ecex_current_buffer(app->ed);
+ if (!buf || !buf->render_fn || !(buf->render_flags & ECEX_RENDER_REPLACE_CONTENT)) {
+ return 0;
+ }
+
+ size_t step = 1;
+
+ if (direction > 0) {
+ size_t line_count = buffer_line_count(buf);
+ size_t max_scroll = line_count > 0 ? line_count - 1 : 0;
+ buf->scroll_line = buf->scroll_line + step > max_scroll ? max_scroll : buf->scroll_line + step;
+ } else {
+ buf->scroll_line = buf->scroll_line > step ? buf->scroll_line - step : 0;
+ }
+
+ app->dirty = 1;
+ return 1;
+}
+
static int app_dispatch_buffer_mouse(app_t *app, int event, double px, double py, int button) {
if (!app || !app->ed) return 0;
@@ -1221,9 +1297,8 @@ static int app_dispatch_buffer_mouse(app_t *app, int event, double px, double py
if (local_y < -32768) local_y = -32768;
if (app_trace_callbacks_enabled()) {
- fprintf(stderr, "ecex-log: mouse_callback buffer=%p fn=%p event=%d x=%d y=%d button=%d userdata=%p\n",
- (void *)buf, (void *)buf->mouse_fn, event, local_x, local_y, button, buf->mouse_userdata);
- fflush(stderr);
+ ecex_logf("mouse_callback buffer=%p fn=%p event=%d x=%d y=%d button=%d userdata=%p",
+ (void *)buf, (void *)buf->mouse_fn, event, local_x, local_y, button, buf->mouse_userdata);
}
int result = buf->mouse_fn(app->ed, buf, event, local_x, local_y, button, buf->mouse_userdata);
@@ -1265,9 +1340,8 @@ static void mouse_button_callback(GLFWwindow *window, int button, int action, in
app->mouse_capture_button = ebutton;
app->mouse_capture_active = 1;
if (app_trace_callbacks_enabled()) {
- fprintf(stderr, "ecex-log: mouse_capture_start buffer=%p button=%d\n",
- (void *)app->mouse_capture_buffer, ebutton);
- fflush(stderr);
+ ecex_logf("mouse_capture_start buffer=%p button=%d",
+ (void *)app->mouse_capture_buffer, ebutton);
}
}
return;
@@ -1277,9 +1351,8 @@ static void mouse_button_callback(GLFWwindow *window, int button, int action, in
app_dispatch_buffer_mouse(app, ECEX_MOUSE_RELEASE, x, y, ebutton);
if (!app->mouse_capture_active || app->mouse_capture_button == ebutton) {
if (app_trace_callbacks_enabled() && app->mouse_capture_active) {
- fprintf(stderr, "ecex-log: mouse_capture_end buffer=%p button=%d\n",
- (void *)app->mouse_capture_buffer, ebutton);
- fflush(stderr);
+ ecex_logf("mouse_capture_end buffer=%p button=%d",
+ (void *)app->mouse_capture_buffer, ebutton);
}
app->mouse_capture_active = 0;
app->mouse_capture_buffer = NULL;
@@ -1303,7 +1376,25 @@ static void scroll_callback(GLFWwindow *window, double xoffset, double yoffset)
(void)xoffset;
app_t *app = glfwGetWindowUserPointer(window);
if (!app || !app->ed) return;
- buffer_t *buf = ecex_current_buffer(app->ed);
+
+ glfwGetFramebufferSize(window, &app->width, &app->height);
+
+ double x = 0.0;
+ double y = 0.0;
+ glfwGetCursorPos(window, &x, &y);
+
+ buffer_t *buf = NULL;
+ size_t wi = 0;
+ if (app_window_rect_at(app, x, y, &wi, NULL, NULL, NULL, NULL) &&
+ wi < app->ed->window_count) {
+ buf = app->ed->windows[wi].buffer;
+ }
+
+ if (!buf) {
+ ecex_window_t *win = ecex_current_window(app->ed);
+ buf = win && win->buffer ? win->buffer : ecex_current_buffer(app->ed);
+ }
+
if (!buf) return;
if (yoffset < 0.0) buf->scroll_line += 3;
else if (yoffset > 0.0) buf->scroll_line = buf->scroll_line > 3 ? buf->scroll_line - 3 : 0;
@@ -1407,8 +1498,47 @@ static int key_event_to_string(int key,
strncat(prefix, "M-", sizeof(prefix) - strlen(prefix) - 1);
}
+ if (mods & GLFW_MOD_SHIFT) {
+ if (base[0] >= 'a' && base[0] <= 'z' && base[1] == '\0') {
+ if (mods & (GLFW_MOD_CONTROL | GLFW_MOD_ALT | GLFW_MOD_SUPER)) {
+ strncat(prefix, "S-", sizeof(prefix) - strlen(prefix) - 1);
+ } else {
+ base[0] = (char)(base[0] - 'a' + 'A');
+ }
+ } else if (base[1] == '\0') {
+ switch (base[0]) {
+ case '`': base[0] = '~'; break;
+ case '1': base[0] = '!'; break;
+ case '2': base[0] = '@'; break;
+ case '3': base[0] = '#'; break;
+ case '4': base[0] = '$'; break;
+ case '5': base[0] = '%'; break;
+ case '6': base[0] = '^'; break;
+ case '7': base[0] = '&'; break;
+ case '8': base[0] = '*'; break;
+ case '9': base[0] = '('; break;
+ case '0': base[0] = ')'; break;
+ case '-': base[0] = '_'; break;
+ case '=': base[0] = '+'; break;
+ case '[': base[0] = '{'; break;
+ case ']': base[0] = '}'; break;
+ case '\\': base[0] = '|'; break;
+ case ';': base[0] = ':'; break;
+ case '\'': base[0] = '"'; break;
+ case ',': base[0] = '<'; break;
+ case '.': base[0] = '>'; break;
+ case '/': base[0] = '?'; break;
+ default:
+ strncat(prefix, "S-", sizeof(prefix) - strlen(prefix) - 1);
+ break;
+ }
+ } else {
+ strncat(prefix, "S-", sizeof(prefix) - strlen(prefix) - 1);
+ }
+ }
+
if (mods & GLFW_MOD_SUPER) {
- strncat(prefix, "S-", sizeof(prefix) - strlen(prefix) - 1);
+ strncat(prefix, "Super-", sizeof(prefix) - strlen(prefix) - 1);
}
snprintf(out, out_size, "%s%s", prefix, base);
@@ -1440,6 +1570,19 @@ static int key_produces_text(int key, int scancode) {
return name && name[0] && name[1] == '\0';
}
+static int is_modifier_key(int key) {
+ return key == GLFW_KEY_LEFT_SHIFT ||
+ key == GLFW_KEY_RIGHT_SHIFT ||
+ key == GLFW_KEY_LEFT_CONTROL ||
+ key == GLFW_KEY_RIGHT_CONTROL ||
+ key == GLFW_KEY_LEFT_ALT ||
+ key == GLFW_KEY_RIGHT_ALT ||
+ key == GLFW_KEY_LEFT_SUPER ||
+ key == GLFW_KEY_RIGHT_SUPER ||
+ key == GLFW_KEY_CAPS_LOCK ||
+ key == GLFW_KEY_NUM_LOCK;
+}
+
static void try_execute_keybind(app_t *app, const char *key_name) {
const char *command = ecex_lookup_key_for_buffer(app->ed, ecex_current_buffer(app->ed), key_name);
if (!command) return;
@@ -1541,13 +1684,46 @@ static void key_callback(GLFWwindow *window,
app->current_mods = mods;
- if (app->mode == APP_MODE_EDIT && key == GLFW_KEY_F1) {
+ if (is_modifier_key(key)) {
+ return;
+ }
+
+ if ((mods & GLFW_MOD_CONTROL) &&
+ !(mods & (GLFW_MOD_ALT | GLFW_MOD_SHIFT | GLFW_MOD_SUPER)) &&
+ is_layout_char_key(key, scancode, 'g')) {
+ if (app->mode == APP_MODE_MX) {
+ app_cancel_mx(app);
+ } else if (app->mode == APP_MODE_PROMPT) {
+ app_cancel_prompt(app);
+ } else if (app->mode == APP_MODE_ISEARCH) {
+ app_cancel_isearch(app);
+ } else if (app->mode == APP_MODE_PREFIX) {
+ app_cancel_prefix(app);
+ } else {
+ buffer_t *buf = ecex_current_buffer(app->ed);
+ if (buf) buffer_clear_mark(buf);
+ app_message(app, "Quit");
+ }
+ if (key_produces_text(key, scancode)) {
+ app->suppress_next_char = 1;
+ }
+ return;
+ }
+
+ if (app->mode == APP_MODE_EDIT &&
+ (mods & GLFW_MOD_ALT) &&
+ !(mods & (GLFW_MOD_CONTROL | GLFW_MOD_SHIFT | GLFW_MOD_SUPER)) &&
+ is_layout_char_key(key, scancode, 'x')) {
+ if (key_produces_text(key, scancode)) {
+ app->suppress_next_char = 1;
+ }
app_enter_mx(app);
return;
}
if (app->mode == APP_MODE_EDIT &&
(mods & GLFW_MOD_ALT) &&
+ !(mods & (GLFW_MOD_CONTROL | GLFW_MOD_SHIFT | GLFW_MOD_SUPER)) &&
is_layout_char_key(key, scancode, 'g')) {
app_enter_prefix(app, "M-g");
return;
@@ -1555,6 +1731,7 @@ static void key_callback(GLFWwindow *window,
if (app->mode == APP_MODE_EDIT &&
(mods & GLFW_MOD_CONTROL) &&
+ !(mods & (GLFW_MOD_ALT | GLFW_MOD_SHIFT | GLFW_MOD_SUPER)) &&
is_layout_char_key(key, scancode, 'x')) {
app_enter_prefix(app, "C-x");
return;
@@ -1713,11 +1890,15 @@ static void key_callback(GLFWwindow *window,
}
}
- if (key == GLFW_KEY_ESCAPE) {
- glfwSetWindowShouldClose(window, GLFW_TRUE);
- glfwPostEmptyEvent();
- return;
+ if (key == GLFW_KEY_PAGE_DOWN) {
+ if (app_page_scroll_rendered_buffer(app, 1)) return;
+ }
+
+ if (key == GLFW_KEY_PAGE_UP) {
+ if (app_page_scroll_rendered_buffer(app, -1)) return;
}
+
+ if (key == GLFW_KEY_ESCAPE) return;
}
void app_init(app_t *app, ecex_t *ed) {
diff --git a/src/buffers.c b/src/buffers.c
index a932712..1e42bfc 100644
--- a/src/buffers.c
+++ b/src/buffers.c
@@ -719,9 +719,8 @@ int ecex_buffer_set_renderer(buffer_t *buffer,
ecex_buffer_userdata_free_fn free_fn,
int flags) {
if (ecex_trace_callbacks_enabled()) {
- fprintf(stderr, "ecex-log: buffer_set_renderer buffer=%p fn=%p userdata=%p free=%p flags=%d\n",
- (void *)buffer, (void *)fn, userdata, (void *)free_fn, flags);
- fflush(stderr);
+ ecex_logf("buffer_set_renderer buffer=%p fn=%p userdata=%p free=%p flags=%d",
+ (void *)buffer, (void *)fn, userdata, (void *)free_fn, flags);
}
if (!buffer) return ECEX_ERR;
if (buffer->render_userdata_free && buffer->render_userdata && buffer->render_userdata != userdata) {
@@ -736,10 +735,9 @@ int ecex_buffer_set_renderer(buffer_t *buffer,
int ecex_buffer_clear_renderer(buffer_t *buffer) {
if (ecex_trace_callbacks_enabled()) {
- fprintf(stderr, "ecex-log: buffer_clear_renderer buffer=%p userdata=%p free=%p\n",
- (void *)buffer, buffer ? buffer->render_userdata : NULL,
- buffer ? (void *)buffer->render_userdata_free : NULL);
- fflush(stderr);
+ ecex_logf("buffer_clear_renderer buffer=%p userdata=%p free=%p",
+ (void *)buffer, buffer ? buffer->render_userdata : NULL,
+ buffer ? (void *)buffer->render_userdata_free : NULL);
}
if (!buffer) return ECEX_ERR;
if (buffer->render_userdata_free && buffer->render_userdata) {
@@ -766,9 +764,8 @@ int ecex_buffer_set_mouse_handler(buffer_t *buffer,
void *userdata,
ecex_buffer_userdata_free_fn free_fn) {
if (ecex_trace_callbacks_enabled()) {
- fprintf(stderr, "ecex-log: buffer_set_mouse_handler buffer=%p fn=%p userdata=%p free=%p\n",
- (void *)buffer, (void *)fn, userdata, (void *)free_fn);
- fflush(stderr);
+ ecex_logf("buffer_set_mouse_handler buffer=%p fn=%p userdata=%p free=%p",
+ (void *)buffer, (void *)fn, userdata, (void *)free_fn);
}
if (!buffer || !fn) return ECEX_ERR;
if (buffer->mouse_userdata_free && buffer->mouse_userdata && buffer->mouse_userdata != userdata) {
@@ -782,10 +779,9 @@ int ecex_buffer_set_mouse_handler(buffer_t *buffer,
int ecex_buffer_clear_mouse_handler(buffer_t *buffer) {
if (ecex_trace_callbacks_enabled()) {
- fprintf(stderr, "ecex-log: buffer_clear_mouse_handler buffer=%p userdata=%p free=%p\n",
- (void *)buffer, buffer ? buffer->mouse_userdata : NULL,
- buffer ? (void *)buffer->mouse_userdata_free : NULL);
- fflush(stderr);
+ ecex_logf("buffer_clear_mouse_handler buffer=%p userdata=%p free=%p",
+ (void *)buffer, buffer ? buffer->mouse_userdata : NULL,
+ buffer ? (void *)buffer->mouse_userdata_free : NULL);
}
if (!buffer) return ECEX_ERR;
if (buffer->mouse_userdata_free && buffer->mouse_userdata) {
@@ -811,9 +807,8 @@ int ecex_buffer_set_animation(buffer_t *buffer,
ecex_buffer_userdata_free_fn free_fn,
double fps) {
if (ecex_trace_callbacks_enabled()) {
- fprintf(stderr, "ecex-log: buffer_set_animation buffer=%p fn=%p userdata=%p free=%p fps=%.3f\n",
- (void *)buffer, (void *)fn, userdata, (void *)free_fn, fps);
- fflush(stderr);
+ ecex_logf("buffer_set_animation buffer=%p fn=%p userdata=%p free=%p fps=%.3f",
+ (void *)buffer, (void *)fn, userdata, (void *)free_fn, fps);
}
if (!buffer || !fn) return ECEX_ERR;
@@ -841,9 +836,8 @@ int ecex_buffer_set_animation_ms(buffer_t *buffer,
ecex_buffer_userdata_free_fn free_fn,
int fps) {
if (ecex_trace_callbacks_enabled()) {
- fprintf(stderr, "ecex-log: buffer_set_animation_ms buffer=%p fn=%p userdata=%p free=%p fps=%d\n",
- (void *)buffer, (void *)fn, userdata, (void *)free_fn, fps);
- fflush(stderr);
+ ecex_logf("buffer_set_animation_ms buffer=%p fn=%p userdata=%p free=%p fps=%d",
+ (void *)buffer, (void *)fn, userdata, (void *)free_fn, fps);
}
if (!buffer || !fn) return ECEX_ERR;
@@ -867,10 +861,9 @@ int ecex_buffer_set_animation_ms(buffer_t *buffer,
int ecex_buffer_clear_animation(buffer_t *buffer) {
if (ecex_trace_callbacks_enabled()) {
- fprintf(stderr, "ecex-log: buffer_clear_animation buffer=%p userdata=%p free=%p\n",
- (void *)buffer, buffer ? buffer->tick_userdata : NULL,
- buffer ? (void *)buffer->tick_userdata_free : NULL);
- fflush(stderr);
+ ecex_logf("buffer_clear_animation buffer=%p userdata=%p free=%p",
+ (void *)buffer, buffer ? buffer->tick_userdata : NULL,
+ buffer ? (void *)buffer->tick_userdata_free : NULL);
}
if (!buffer) return ECEX_ERR;
if (buffer->tick_userdata_free && buffer->tick_userdata) {
@@ -899,25 +892,37 @@ int ecex_tick_animations(ecex_t *ed, double now_seconds) {
if (!ed) return 0;
int dirty = 0;
+ int frame_group_open = 0;
+ int trace_callbacks = ecex_trace_callbacks_enabled();
for (size_t i = 0; i < ed->buffer_count; i++) {
buffer_t *buffer = ed->buffers[i];
if (!buffer || !buffer->tick_enabled) continue;
if (buffer->tick_uses_ms) {
if (!buffer->tick_ms_fn) {
- if (ecex_trace_callbacks_enabled()) {
- fprintf(stderr, "ecex-log: animation_tick_skip_ms_null buffer=%p userdata=%p\n",
- (void *)buffer, buffer->tick_userdata);
- fflush(stderr);
+ if (trace_callbacks) {
+ if (!frame_group_open) {
+ char frame_msg[128];
+ snprintf(frame_msg, sizeof(frame_msg), "animation frame start now=%.6f", now_seconds);
+ ecex_log_group_begin(frame_msg);
+ frame_group_open = 1;
+ }
+ ecex_logf("animation_tick_skip_ms_null buffer=%p userdata=%p",
+ (void *)buffer, buffer->tick_userdata);
}
buffer->tick_enabled = 0;
continue;
}
} else {
if (!buffer->tick_fn) {
- if (ecex_trace_callbacks_enabled()) {
- fprintf(stderr, "ecex-log: animation_tick_skip_null buffer=%p userdata=%p\n",
- (void *)buffer, buffer->tick_userdata);
- fflush(stderr);
+ if (trace_callbacks) {
+ if (!frame_group_open) {
+ char frame_msg[128];
+ snprintf(frame_msg, sizeof(frame_msg), "animation frame start now=%.6f", now_seconds);
+ ecex_log_group_begin(frame_msg);
+ frame_group_open = 1;
+ }
+ ecex_logf("animation_tick_skip_null buffer=%p userdata=%p",
+ (void *)buffer, buffer->tick_userdata);
}
buffer->tick_enabled = 0;
continue;
@@ -935,30 +940,48 @@ int ecex_tick_animations(ecex_t *ed, double now_seconds) {
buffer->tick_last_time = now_seconds;
int tick_dirty = 0;
+ if (!frame_group_open) {
+ char frame_msg[128];
+ snprintf(frame_msg, sizeof(frame_msg), "animation frame start now=%.6f", now_seconds);
+ ecex_log_group_begin(frame_msg);
+ frame_group_open = 1;
+ }
if (buffer->tick_uses_ms) {
int now_ms = (int)(now_seconds * 1000.0);
- if (ecex_trace_callbacks_enabled()) {
- fprintf(stderr, "ecex-log: animation_tick_ms_enter buffer=%p fn=%p userdata=%p now_ms=%d\n",
- (void *)buffer, (void *)buffer->tick_ms_fn, buffer->tick_userdata, now_ms);
- fflush(stderr);
+ if (trace_callbacks) {
+ char tick_msg[256];
+ snprintf(tick_msg,
+ sizeof(tick_msg),
+ "animation tick ms start buffer=%p fn=%p userdata=%p now_ms=%d",
+ (void *)buffer,
+ (void *)buffer->tick_ms_fn,
+ buffer->tick_userdata,
+ now_ms);
+ ecex_log_group_begin(tick_msg);
}
tick_dirty = buffer->tick_ms_fn(ed, buffer, now_ms, buffer->tick_userdata);
- if (ecex_trace_callbacks_enabled()) {
- fprintf(stderr, "ecex-log: animation_tick_ms_leave buffer=%p result=%d\n",
- (void *)buffer, tick_dirty);
- fflush(stderr);
+ if (trace_callbacks) {
+ char tick_msg[128];
+ snprintf(tick_msg, sizeof(tick_msg), "animation tick ms end buffer=%p result=%d", (void *)buffer, tick_dirty);
+ ecex_log_group_end(tick_msg);
}
} else {
- if (ecex_trace_callbacks_enabled()) {
- fprintf(stderr, "ecex-log: animation_tick_enter buffer=%p fn=%p userdata=%p now=%.6f\n",
- (void *)buffer, (void *)buffer->tick_fn, buffer->tick_userdata, now_seconds);
- fflush(stderr);
+ if (trace_callbacks) {
+ char tick_msg[256];
+ snprintf(tick_msg,
+ sizeof(tick_msg),
+ "animation tick start buffer=%p fn=%p userdata=%p now=%.6f",
+ (void *)buffer,
+ (void *)buffer->tick_fn,
+ buffer->tick_userdata,
+ now_seconds);
+ ecex_log_group_begin(tick_msg);
}
tick_dirty = buffer->tick_fn(ed, buffer, now_seconds, buffer->tick_userdata);
- if (ecex_trace_callbacks_enabled()) {
- fprintf(stderr, "ecex-log: animation_tick_leave buffer=%p result=%d\n",
- (void *)buffer, tick_dirty);
- fflush(stderr);
+ if (trace_callbacks) {
+ char tick_msg[128];
+ snprintf(tick_msg, sizeof(tick_msg), "animation tick end buffer=%p result=%d", (void *)buffer, tick_dirty);
+ ecex_log_group_end(tick_msg);
}
}
if (tick_dirty != 0) {
@@ -966,6 +989,10 @@ int ecex_tick_animations(ecex_t *ed, double now_seconds) {
}
}
+ if (frame_group_open) {
+ ecex_log_group_end(dirty ? "animation frame end dirty=1" : "animation frame end dirty=0");
+ }
+
return dirty;
}
@@ -983,17 +1010,17 @@ int ecex_buffer_text_len(buffer_t *buffer) {
return buffer->len > (size_t)2147483647 ? 2147483647 : (int)buffer->len;
}
-int ecex_buffer_scroll_line(buffer_t *buffer) {
+int ecex_buffer_scroll_line_index(buffer_t *buffer) {
if (!buffer) return 0;
return buffer->scroll_line > (size_t)2147483647 ? 2147483647 : (int)buffer->scroll_line;
}
-int ecex_buffer_line_count_i(buffer_t *buffer) {
+int ecex_buffer_line_count_int(buffer_t *buffer) {
size_t n = buffer_line_count(buffer);
return n > (size_t)2147483647 ? 2147483647 : (int)n;
}
-int ecex_buffer_line_copy(buffer_t *buffer, int line, char *out, int out_cap) {
+int ecex_buffer_line_copy_text(buffer_t *buffer, int line, char *out, int out_cap) {
size_t current = 0;
size_t pos = 0;
size_t start;
diff --git a/src/config.c b/src/config.c
index 1b0b5ed..e9bba5d 100644
--- a/src/config.c
+++ b/src/config.c
@@ -5,7 +5,6 @@
#include "common.h"
#include "ecex.h"
#include "eval.h"
-#include "util.h"
#include "path.h"
#include <stdio.h>
@@ -65,46 +64,56 @@ static const host_symbol_t host_symbols[] = {
HOST_SYMBOL(ecex_config_free),
HOST_SYMBOL(ecex_time_seconds),
HOST_SYMBOL(ecex_log),
+ HOST_SYMBOL(ecex_logf),
+ HOST_SYMBOL(ecex_log_group_begin),
+ HOST_SYMBOL(ecex_log_group_end),
HOST_SYMBOL(ecex_log_int),
HOST_SYMBOL(ecex_log_double),
HOST_SYMBOL(ecex_log_ptr),
+ HOST_SYMBOL(ecex_log_flush),
HOST_SYMBOL(ecex_mem_zero),
HOST_SYMBOL(ecex_i32_get),
HOST_SYMBOL(ecex_i32_set),
HOST_SYMBOL(ecex_prng_next_bounded),
HOST_SYMBOL(ecex_random_bounded),
- HOST_SYMBOL(ecex_tetris_shape_cell),
- HOST_SYMBOL(ecex_var_get),
- HOST_SYMBOL(ecex_var_get_or_alloc),
- HOST_SYMBOL(ecex_var_bind_static),
- HOST_SYMBOL(ecex_var_free),
- HOST_SYMBOL(ecex_var_free_owner),
- HOST_SYMBOL(ecex_var_i32_get),
- HOST_SYMBOL(ecex_var_i32_set),
- HOST_SYMBOL(ecex_var_i32),
- HOST_SYMBOL(ecex_var_i32_set_scalar),
- HOST_SYMBOL(ecex_object_alloc),
- HOST_SYMBOL(ecex_object_calloc),
- HOST_SYMBOL(ecex_object_free),
- HOST_SYMBOL(ecex_object_valid),
- HOST_SYMBOL(ecex_object_i32_get),
- HOST_SYMBOL(ecex_object_i32_set),
- HOST_SYMBOL(ecex_object_ptr_get),
- HOST_SYMBOL(ecex_object_ptr_set),
- HOST_SYMBOL(ecex_text_set),
- HOST_SYMBOL(ecex_text_set_buffer_title),
- HOST_SYMBOL(ecex_text_free),
- HOST_SYMBOL(ecex_text_free_owner),
+ HOST_SYMBOL(ecex_plugin_register),
+ HOST_SYMBOL(ecex_plugin_require),
+ HOST_SYMBOL(ecex_plugin_find),
+ HOST_SYMBOL(ecex_plugin_id),
+ HOST_SYMBOL(ecex_plugin_slot_alloc),
+ HOST_SYMBOL(ecex_plugin_slot_get),
+ HOST_SYMBOL(ecex_plugin_slot_free),
+ HOST_SYMBOL(ecex_plugin_slot_set_export_flags),
+ HOST_SYMBOL(ecex_plugin_slot_read_exported),
+ HOST_SYMBOL(ecex_plugin_slot_i32_get),
+ HOST_SYMBOL(ecex_plugin_slot_i32_set),
+ HOST_SYMBOL(ecex_plugin_slot_i32_get_2d),
+ HOST_SYMBOL(ecex_plugin_slot_i32_set_2d),
+ HOST_SYMBOL(ecex_plugin_slot_i32_get_scalar),
+ HOST_SYMBOL(ecex_plugin_slot_i32_set_scalar),
+ HOST_SYMBOL(ecex_plugin_object_alloc),
+ HOST_SYMBOL(ecex_plugin_object_calloc),
+ HOST_SYMBOL(ecex_plugin_object_free),
+ HOST_SYMBOL(ecex_plugin_object_valid),
+ HOST_SYMBOL(ecex_plugin_object_i32_get),
+ HOST_SYMBOL(ecex_plugin_object_i32_set),
+ HOST_SYMBOL(ecex_plugin_object_ptr_get),
+ HOST_SYMBOL(ecex_plugin_object_ptr_set),
+ HOST_SYMBOL(ecex_plugin_text_set),
+ HOST_SYMBOL(ecex_plugin_text_set_from_buffer_title),
+ HOST_SYMBOL(ecex_plugin_text_free),
+ HOST_SYMBOL(ecex_plugin_text_free_all),
+ HOST_SYMBOL(ecex_plugin_text_get_drawable),
HOST_SYMBOL(ecex_buffer_text_len),
- HOST_SYMBOL(ecex_buffer_scroll_line),
- HOST_SYMBOL(ecex_buffer_line_count_i),
- HOST_SYMBOL(ecex_buffer_line_copy),
- HOST_SYMBOL(ecex_markdown_draw_line_from_buffer_i),
- HOST_SYMBOL(ecex_markdown_body_y_i),
- HOST_SYMBOL(ecex_draw_context_height_i),
- HOST_SYMBOL(ecex_draw_context_line_height_i),
- HOST_SYMBOL(ecex_register_file_handler),
- HOST_SYMBOL(ecex_run_file_handlers),
+ HOST_SYMBOL(ecex_buffer_scroll_line_index),
+ HOST_SYMBOL(ecex_buffer_line_count_int),
+ HOST_SYMBOL(ecex_buffer_line_copy_text),
+ HOST_SYMBOL(ecex_markdown_draw_buffer_line_i),
+ HOST_SYMBOL(ecex_markdown_body_y_px),
+ HOST_SYMBOL(ecex_draw_context_height_px),
+ HOST_SYMBOL(ecex_draw_context_line_height_px),
+ HOST_SYMBOL(ecex_plugin_file_handler_register),
+ HOST_SYMBOL(ecex_plugin_file_handlers_run),
HOST_SYMBOL(ecex_config_register_commands),
HOST_SYMBOL(ecex_config_bind_keys),
@@ -113,6 +122,27 @@ static const host_symbol_t host_symbols[] = {
HOST_SYMBOL(ecex_apply_theme),
HOST_SYMBOL(ecex_register_command),
HOST_SYMBOL(ecex_execute_command),
+ HOST_SYMBOL(ecex_add_command_hook),
+ HOST_SYMBOL(ecex_remove_command_hook),
+ HOST_SYMBOL(ecex_add_prefix_hook),
+ HOST_SYMBOL(ecex_remove_prefix_hook),
+ HOST_SYMBOL(ecex_notify_prefix_hooks),
+ HOST_SYMBOL(ecex_add_buffer_hook),
+ HOST_SYMBOL(ecex_remove_buffer_hook),
+ HOST_SYMBOL(ecex_notify_buffer_hooks),
+ HOST_SYMBOL(ecex_message),
+ HOST_SYMBOL(ecex_dependency_available),
+ HOST_SYMBOL(ecex_plugin_require_dependency),
+ HOST_SYMBOL(ecex_add_completion_provider),
+ HOST_SYMBOL(ecex_add_word_completion_provider),
+ HOST_SYMBOL(ecex_define_word_completion_provider),
+ HOST_SYMBOL(ecex_completion_provider_add_word),
+ HOST_SYMBOL(ecex_completion_provider_add_words),
+ HOST_SYMBOL(ecex_completion_provider_set_detail),
+ HOST_SYMBOL(ecex_add_clangd_completion_provider),
+ HOST_SYMBOL(ecex_remove_completion_provider),
+ HOST_SYMBOL(ecex_buffer_identifier_prefix),
+ HOST_SYMBOL(ecex_complete_at_point),
HOST_SYMBOL(ecex_bind_key),
HOST_SYMBOL(ecex_lookup_key),
HOST_SYMBOL(ecex_define_major_mode),
@@ -124,6 +154,7 @@ static const host_symbol_t host_symbols[] = {
HOST_SYMBOL(ecex_bind_mode_key),
HOST_SYMBOL(ecex_lookup_key_for_buffer),
HOST_SYMBOL(ecex_key_sequence_has_prefix_for_buffer),
+ HOST_SYMBOL(ecex_describe_key_prefix),
HOST_SYMBOL(ecex_auto_set_major_mode),
HOST_SYMBOL(ecex_list_commands),
HOST_SYMBOL(ecex_list_buffers),
@@ -153,12 +184,13 @@ static const host_symbol_t host_symbols[] = {
HOST_SYMBOL(ecex_draw_text),
HOST_SYMBOL(ecex_draw_text_aligned),
HOST_SYMBOL(ecex_draw_text_width),
- HOST_SYMBOL(ecex_draw_color_rgba8),
+ HOST_SYMBOL(ecex_draw_color_rgba8_i),
HOST_SYMBOL(ecex_draw_rect_i),
HOST_SYMBOL(ecex_draw_rect_outline_i),
HOST_SYMBOL(ecex_draw_line_i),
HOST_SYMBOL(ecex_draw_text_i),
- HOST_SYMBOL(ecex_draw_text_id_i),
+ HOST_SYMBOL(ecex_draw_plugin_text_i),
+ HOST_SYMBOL(ecex_draw_plugin_text_rect_i),
HOST_SYMBOL(ecex_draw_markdown_canvas_i),
HOST_SYMBOL(ecex_draw_markdown_text_i),
HOST_SYMBOL(ecex_draw_markdown_canvas_auto_i),
@@ -170,6 +202,15 @@ static const host_symbol_t host_symbols[] = {
HOST_SYMBOL(ecex_find_file),
HOST_SYMBOL(ecex_save_current_buffer),
HOST_SYMBOL(ecex_write_current_buffer),
+ HOST_SYMBOL(ecex_compile),
+ HOST_SYMBOL(ecex_grep),
+ HOST_SYMBOL(ecex_rerun_compile),
+ HOST_SYMBOL(ecex_rerun_grep),
+ HOST_SYMBOL(ecex_next_interactive_action),
+ HOST_SYMBOL(ecex_previous_interactive_action),
+ HOST_SYMBOL(ecex_indent_line_to),
+ HOST_SYMBOL(ecex_comment_region),
+ HOST_SYMBOL(ecex_uncomment_region),
HOST_SYMBOL(ecex_request_prompt),
HOST_SYMBOL(ecex_clear_prompt_request),
HOST_SYMBOL(ecex_complete_command),
@@ -308,32 +349,48 @@ int ecex_add_ccdjit_include_paths(ccdjit_context *ctx) {
return ECEX_OK;
}
-static char *make_config_source_with_forced_main(const char *path) {
- size_t source_size = 0;
- char *source = ecex_read_entire_file(path, &source_size);
- if (!source) return NULL;
-
- const char *forced_main =
- "\n"
- "\n"
- "int main(int argc, char **argv) {\n"
- " (void)argc;\n"
- " (void)argv;\n"
- " return ecex_config_init((ecex_t *)0);\n"
- "}\n";
-
- size_t forced_main_len = strlen(forced_main);
- char *combined = malloc(source_size + forced_main_len + 1);
- if (!combined) {
- free(source);
- return NULL;
- }
+static int ccdjit_type_is_int(ccdjit_type_info type) {
+ return type.has_type &&
+ type.base == CCDJIT_TYPE_INT &&
+ type.pointer_level == 0;
+}
+
+static int ccdjit_type_is_pointer(ccdjit_type_info type) {
+ return type.has_type && type.pointer_level == 1;
+}
- memcpy(combined, source, source_size);
- memcpy(combined + source_size, forced_main, forced_main_len + 1);
+static ecex_config_init_fn ecex_config_init_symbol(ccdjit_context *ctx,
+ ccdjit_module *module,
+ const char *path) {
+ void *symbol = ccdjit_module_symbol(module, "ecex_config_init");
+ if (!symbol) return NULL;
+
+ ccdjit_symbol_info info;
+ memset(&info, 0, sizeof(info));
+ if (ccdjit_module_symbol_info(module, "ecex_config_init", &info) == 0 &&
+ info.has_signature) {
+ ccdjit_type_info param;
+ memset(&param, 0, sizeof(param));
+
+ if (info.kind != CCDJIT_SYMBOL_FUNCTION ||
+ !ccdjit_type_is_int(info.return_type) ||
+ info.parameter_count != 1 ||
+ info.is_variadic ||
+ ccdjit_module_function_parameter_type(module, "ecex_config_init", 0, &param) != 0 ||
+ !ccdjit_type_is_pointer(param)) {
+ fprintf(stderr,
+ "ecex: invalid config signature in %s; expected int ecex_config_init(ecex_t *ed)\n",
+ path ? path : "<config>");
+ return NULL;
+ }
+ } else if (ctx) {
+ const ccdjit_diagnostic *diag = ccdjit_context_last_error(ctx);
+ if (diag && diag->code != CCDJIT_DIAG_NONE) {
+ ecex_print_ccdjit_error(ctx);
+ }
+ }
- free(source);
- return combined;
+ return (ecex_config_init_fn)symbol;
}
int ecex_load_c_config(ecex_t *ed, const char *path) {
@@ -350,26 +407,15 @@ int ecex_load_c_config(ecex_t *ed, const char *path) {
return ECEX_ERR;
}
- char *source = make_config_source_with_forced_main(path);
- if (!source) {
- fprintf(stderr, "ecex: failed to read or prepare config: %s\n", path);
- ccdjit_context_free(ctx);
- return ECEX_ERR;
- }
-
ccdjit_module *module = NULL;
- if (ccdjit_compile_string(ctx, source, path, &module) != 0) {
+ if (ccdjit_compile_file_module(ctx, path, &module) != 0) {
fprintf(stderr, "ecex: failed to compile config: %s\n", path);
ecex_print_ccdjit_error(ctx);
- free(source);
ccdjit_context_free(ctx);
return ECEX_ERR;
}
- free(source);
-
- ecex_config_init_fn init =
- (ecex_config_init_fn)ccdjit_module_symbol(module, "ecex_config_init");
+ ecex_config_init_fn init = ecex_config_init_symbol(ctx, module, path);
if (!init) {
fprintf(stderr, "ecex: config missing function: ecex_config_init\n");
@@ -382,7 +428,12 @@ int ecex_load_c_config(ecex_t *ed, const char *path) {
int result = init(ed);
if (result != 0) {
fprintf(stderr, "ecex: config init failed with code: %d\n", result);
- ccdjit_module_free(module);
+ /* Config init may have registered hooks/renderers before returning an
+ * error. Keep the module alive so rollback can call any JIT-owned
+ * destructors safely. */
+ if (ecex_keep_jit_module(ed, module) != ECEX_OK) {
+ fprintf(stderr, "ecex: failed to retain failed config module; leaving it mapped for cleanup safety\n");
+ }
ccdjit_context_free(ctx);
return ECEX_ERR;
}
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); } }
diff --git a/src/eval.c b/src/eval.c
index e5f0e44..34259eb 100644
--- a/src/eval.c
+++ b/src/eval.c
@@ -15,6 +15,9 @@
#include <unistd.h>
static ecex_t *g_eval_editor = NULL;
+static unsigned long g_eval_serial = 0;
+
+typedef int (*ecex_eval_entry_fn)(int argc, char **argv);
static buffer_t *eval_output_buffer(ecex_t *ed) {
if (!ed) return NULL;
@@ -207,15 +210,148 @@ static int source_has_main(const char *source) {
strstr(source, "main (") != NULL;
}
-static char *make_eval_source(const char *source, int wrap_as_statements) {
+static int eval_trace_enabled(void) {
+ const char *log = getenv("ECEX_LOG");
+ const char *trace_eval = getenv("ECEX_TRACE_EVAL");
+ return (log && log[0] && log[0] != '0') ||
+ (trace_eval && trace_eval[0] && trace_eval[0] != '0');
+}
+
+static size_t eval_source_line_count(const char *source, size_t source_len) {
+ if (!source || source_len == 0) return 0;
+
+ size_t lines = 1;
+ for (size_t i = 0; i < source_len; i++) {
+ if (source[i] == '\n' && i + 1 < source_len) lines++;
+ }
+ return lines;
+}
+
+static size_t eval_append_escaped_char(char *out, size_t len, size_t cap, unsigned char c) {
+ static const char hex[] = "0123456789abcdef";
+ if (!out || cap == 0 || len >= cap) return len;
+
+ if (c == '\t') {
+ for (int i = 0; i < 4 && len + 1 < cap; i++) out[len++] = ' ';
+ } else if (c == '\\') {
+ if (len + 2 < cap) {
+ out[len++] = '\\';
+ out[len++] = '\\';
+ }
+ } else if (c < 32 || c == 127) {
+ if (len + 4 < cap) {
+ out[len++] = '\\';
+ out[len++] = 'x';
+ out[len++] = hex[(c >> 4) & 0x0f];
+ out[len++] = hex[c & 0x0f];
+ }
+ } else {
+ if (len + 1 < cap) out[len++] = (char)c;
+ }
+
+ out[len < cap ? len : cap - 1] = '\0';
+ return len;
+}
+
+static void eval_log_source_line(size_t line_no, const char *line, size_t line_len) {
+ size_t pos = 0;
+ int continued = 0;
+
+ if (line_len == 0) {
+ ecex_logf("%4lu |", (unsigned long)line_no);
+ return;
+ }
+
+ while (pos < line_len) {
+ char out[1024];
+ size_t len = 0;
+ int prefix_len = snprintf(out,
+ sizeof(out),
+ "%4lu %c ",
+ (unsigned long)line_no,
+ continued ? '>' : '|');
+ if (prefix_len < 0) return;
+ if ((size_t)prefix_len >= sizeof(out)) prefix_len = (int)sizeof(out) - 1;
+ len = (size_t)prefix_len;
+
+ size_t before = pos;
+ while (pos < line_len && len + 8 < sizeof(out)) {
+ len = eval_append_escaped_char(out, len, sizeof(out), (unsigned char)line[pos]);
+ pos++;
+ }
+
+ if (pos == before) {
+ len = eval_append_escaped_char(out, len, sizeof(out), (unsigned char)line[pos]);
+ pos++;
+ }
+
+ ecex_log(out);
+ continued = 1;
+ }
+}
+
+static void eval_log_source(const char *source,
+ const char *filename,
+ int wrap_source,
+ unsigned long serial) {
+ if (!source || !eval_trace_enabled()) return;
+
+ size_t source_len = strlen(source);
+ size_t line_count = eval_source_line_count(source, source_len);
+ char header[768];
+ snprintf(header,
+ sizeof(header),
+ "eval source start serial=%lu file=%s mode=%s bytes=%lu lines=%lu",
+ serial,
+ filename ? filename : "<eval>",
+ wrap_source ? "wrapped-statements" : "translation-unit",
+ (unsigned long)source_len,
+ (unsigned long)line_count);
+
+ ecex_log_group_begin(header);
+
+ if (source_len == 0) {
+ ecex_log("(empty)");
+ } else {
+ size_t line_start = 0;
+ size_t line_no = 1;
+ for (size_t i = 0; i <= source_len; i++) {
+ if (source[i] != '\n' && source[i] != '\0') continue;
+ if (source[i] == '\0' && line_start == source_len && source_len > 0 && source[source_len - 1] == '\n') {
+ break;
+ }
+
+ size_t line_len = i - line_start;
+ if (line_len > 0 && source[line_start + line_len - 1] == '\r') line_len--;
+ eval_log_source_line(line_no, source + line_start, line_len);
+
+ if (source[i] == '\0') break;
+ line_start = i + 1;
+ line_no++;
+ }
+ }
+
+ ecex_log_group_end("eval source end");
+}
+
+static char *make_eval_source(const char *source, int wrap_as_statements, unsigned long serial) {
if (!source) return NULL;
- const char *prefix =
+ /* Keep eval-defined command/helper symbols private. Successful eval
+ * modules stay loaded, and default-visible ELF symbols can otherwise be
+ * interposed by the first eval that defined the same names. */
+ char prefix[512];
+ int prefix_len = snprintf(prefix,
+ sizeof(prefix),
"#include \"ecex.h\"\n"
"#include \"buffers.h\"\n"
"extern ecex_t *__ecex_eval_editor;\n"
"#define ECEX_ED (__ecex_eval_editor)\n"
- "\n";
+ "#define main __ecex_eval_user_main_%lu\n"
+ "#pragma GCC visibility push(hidden)\n"
+ "\n",
+ serial);
+ if (prefix_len < 0 || (size_t)prefix_len >= sizeof(prefix)) return NULL;
const char *wrapper_open =
"int main(int argc, char **argv) {\n"
@@ -231,16 +367,35 @@ static char *make_eval_source(const char *source, int wrap_as_statements) {
int do_wrap = wrap_as_statements || !source_has_main(source);
- size_t prefix_len = strlen(prefix);
size_t source_len = strlen(source);
size_t open_len = do_wrap ? strlen(wrapper_open) : 0;
size_t close_len = do_wrap ? strlen(wrapper_close) : 0;
-
- char *combined = malloc(prefix_len + open_len + source_len + close_len + 2);
+ char entry_wrapper[384];
+ int entry_wrapper_len = 0;
+ const char *suffix =
+ "\n"
+ "#pragma GCC visibility pop\n"
+ "#undef main\n";
+
+ entry_wrapper_len = snprintf(entry_wrapper,
+ sizeof(entry_wrapper),
+ "int ecex_eval_entry(int argc, char **argv) {\n"
+ " return ((int (*)(int, char **))__ecex_eval_user_main_%lu)(argc, argv);\n"
+ "}\n"
+ "int main(int argc, char **argv) {\n"
+ " return ecex_eval_entry(argc, argv);\n"
+ "}\n",
+ serial);
+ if (entry_wrapper_len < 0 || (size_t)entry_wrapper_len >= sizeof(entry_wrapper)) return NULL;
+
+ size_t suffix_len = strlen(suffix);
+
+ char *combined = malloc((size_t)prefix_len + open_len + source_len + close_len +
+ suffix_len + (size_t)entry_wrapper_len + 2);
if (!combined) return NULL;
char *p = combined;
- memcpy(p, prefix, prefix_len);
+ memcpy(p, prefix, (size_t)prefix_len);
p += prefix_len;
if (do_wrap) {
@@ -259,6 +414,12 @@ static char *make_eval_source(const char *source, int wrap_as_statements) {
p += close_len;
}
+ memcpy(p, suffix, suffix_len);
+ p += suffix_len;
+
+ memcpy(p, entry_wrapper, (size_t)entry_wrapper_len);
+ p += entry_wrapper_len;
+
*p = '\0';
return combined;
}
@@ -305,7 +466,11 @@ int ecex_eval_source(ecex_t *ed,
buffer_append(out, filename ? filename : "<eval>");
buffer_append(out, "\n\n");
- char *eval_source = make_eval_source(source, wrap_as_statements);
+ unsigned long eval_serial = ++g_eval_serial;
+ int wrap_source = wrap_as_statements || !source_has_main(source);
+ eval_log_source(source, filename, wrap_source, eval_serial);
+
+ char *eval_source = make_eval_source(source, wrap_as_statements, eval_serial);
if (!eval_source) {
buffer_append(out, "failed to allocate eval source\n");
ecex_switch_buffer(ed, "*eval-output*");
@@ -349,15 +514,49 @@ int ecex_eval_source(ecex_t *ed,
return ECEX_ERR;
}
+ char entry_name[128];
+ snprintf(entry_name, sizeof(entry_name), "ecex_eval_entry");
+ ecex_eval_entry_fn entry = (ecex_eval_entry_fn)ccdjit_module_symbol(module, entry_name);
+ if (!entry) {
+ buffer_append(out, "failed to resolve eval entry: ");
+ buffer_append(out, entry_name);
+ buffer_append(out, "\n");
+ ccdjit_module_free(module);
+ g_eval_editor = NULL;
+ ccdjit_context_free(ctx);
+ free(eval_source);
+ ecex_switch_buffer(ed, "*eval-output*");
+ return ECEX_ERR;
+ }
+
int result = 0;
eval_capture_t capture;
int capture_enabled = (eval_capture_begin(&capture, out) == ECEX_OK);
- int runtime_status = ccdjit_module_call_main(module, 0, NULL, &result);
+ int runtime_status = 0;
+ result = entry(0, NULL);
if (capture_enabled) {
eval_capture_finish(&capture, out);
}
+ /*
+ * Eval code can mutate public editor fields directly, bypassing the
+ * setter functions that normally bump UI revisions. Treat a completed
+ * eval as a possible UI mutation so the app layer always gets a repaint
+ * opportunity.
+ */
+ ed->ui_revision++;
+
+ if (eval_trace_enabled()) {
+ ecex_logf("eval result serial=%lu entry=%p result=%d font_size=%.3f font_revision=%lu ui_revision=%lu",
+ eval_serial,
+ (void *)entry,
+ result,
+ (double)ecex_get_font_size(ed),
+ ed->font_revision,
+ ed->ui_revision);
+ }
+
if (runtime_status != 0) {
buffer_append(out, "runtime failed\n\n");
eval_append_diag(out, ctx);
diff --git a/src/log.c b/src/log.c
new file mode 100644
index 0000000..7dec534
--- /dev/null
+++ b/src/log.c
@@ -0,0 +1,345 @@
+#include "ecex.h"
+
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <unistd.h>
+
+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_EVAL") ||
+ ecex_env_enabled("ECEX_TRACE_CALLBACKS") ||
+ ecex_env_enabled("ECEX_TRACE_TETRIS");
+}
+
+static char ecex_last_log_line[1024];
+static unsigned long ecex_last_log_count = 0;
+static int ecex_last_log_depth = 0;
+static int ecex_log_group_depth = 0;
+static int ecex_frame_group_open = 0;
+static int ecex_frame_group_depth = 0;
+static char ecex_frame_group_start[1024];
+static char ecex_frame_repeat_start[1024];
+static char ecex_frame_repeat_end[1024];
+static unsigned long ecex_frame_repeat_count = 0;
+
+static size_t ecex_strn_copy(char *out, size_t out_cap, const char *in) {
+ size_t i = 0;
+ if (!out || out_cap == 0) return 0;
+ if (!in) in = "(null)";
+ while (i + 1 < out_cap && in[i]) {
+ out[i] = in[i];
+ ++i;
+ }
+ out[i] = '\0';
+ return i;
+}
+
+static int ecex_streq(const char *a, const char *b) {
+ size_t i = 0;
+ if (!a || !b) return 0;
+ while (a[i] && b[i]) {
+ if (a[i] != b[i]) return 0;
+ ++i;
+ }
+ return a[i] == b[i];
+}
+
+static int ecex_starts_with(const char *text, const char *prefix) {
+ size_t i = 0;
+ if (!text || !prefix) return 0;
+ while (prefix[i]) {
+ if (text[i] != prefix[i]) return 0;
+ ++i;
+ }
+ return 1;
+}
+
+static size_t ecex_append_char(char *out, size_t len, size_t cap, char c) {
+ if (len + 1 < cap) {
+ out[len++] = c;
+ out[len] = '\0';
+ }
+ return len;
+}
+
+static size_t ecex_append_cstr(char *out, size_t len, size_t cap, const char *text) {
+ size_t i = 0;
+ if (!text) text = "(null)";
+ while (text[i] && len + 1 < cap) {
+ out[len++] = text[i++];
+ }
+ if (cap) out[len < cap ? len : cap - 1] = '\0';
+ return len;
+}
+
+static size_t ecex_append_ulong(char *out, size_t len, size_t cap, unsigned long value) {
+ char tmp[32];
+ size_t n = 0;
+ if (value == 0) return ecex_append_char(out, len, cap, '0');
+ while (value && n < sizeof(tmp)) {
+ tmp[n++] = (char)('0' + (value % 10));
+ value /= 10;
+ }
+ while (n > 0) len = ecex_append_char(out, len, cap, tmp[--n]);
+ return len;
+}
+
+static size_t ecex_append_int(char *out, size_t len, size_t cap, int value) {
+ unsigned int mag;
+ if (value < 0) {
+ len = ecex_append_char(out, len, cap, '-');
+ mag = (unsigned int)(-(value + 1)) + 1u;
+ } else {
+ mag = (unsigned int)value;
+ }
+ return ecex_append_ulong(out, len, cap, (unsigned long)mag);
+}
+
+static size_t ecex_append_ptr(char *out, size_t len, size_t cap, const void *ptr) {
+ uintptr_t value = (uintptr_t)ptr;
+ char tmp[sizeof(uintptr_t) * 2];
+ size_t n = 0;
+ len = ecex_append_cstr(out, len, cap, "0x");
+ if (value == 0) return ecex_append_char(out, len, cap, '0');
+ while (value && n < sizeof(tmp)) {
+ int digit = (int)(value & 0xfu);
+ tmp[n++] = (char)(digit < 10 ? '0' + digit : 'a' + digit - 10);
+ value >>= 4;
+ }
+ while (n > 0) len = ecex_append_char(out, len, cap, tmp[--n]);
+ return len;
+}
+
+static size_t ecex_append_double(char *out, size_t len, size_t cap, double value) {
+ long whole;
+ unsigned long frac;
+ int i;
+ if (value < 0.0) {
+ len = ecex_append_char(out, len, cap, '-');
+ value = -value;
+ }
+ whole = (long)value;
+ frac = (unsigned long)((value - (double)whole) * 1000000.0 + 0.5);
+ len = ecex_append_ulong(out, len, cap, (unsigned long)whole);
+ len = ecex_append_char(out, len, cap, '.');
+ for (i = 100000; i > 0; i /= 10) {
+ len = ecex_append_char(out, len, cap, (char)('0' + (frac / (unsigned long)i) % 10));
+ }
+ return len;
+}
+
+static void ecex_write_all(const char *text, size_t len) {
+ while (len > 0) {
+ ssize_t written = write(STDERR_FILENO, text, len);
+ if (written <= 0) return;
+ text += written;
+ len -= (size_t)written;
+ }
+}
+
+static void ecex_log_emit_raw_depth(const char *line, int depth) {
+ size_t len = 0;
+ int i;
+ while (line && line[len]) ++len;
+ ecex_write_all("ecex-log: ", 10);
+ for (i = 0; i < depth; i++) {
+ ecex_write_all(" ", 2);
+ }
+ ecex_write_all(line ? line : "(null)", line ? len : 6);
+ ecex_write_all("\n", 1);
+}
+
+static void ecex_log_emit_raw_counted(const char *line, int depth, unsigned long count) {
+ char counted[1200];
+ if (count > 1) {
+ snprintf(counted, sizeof(counted), "%s [%lu]", line ? line : "(null)", count);
+ ecex_log_emit_raw_depth(counted, depth);
+ } else {
+ ecex_log_emit_raw_depth(line, depth);
+ }
+}
+
+static void ecex_log_flush_frame_repeats(void) {
+ if (ecex_frame_repeat_count == 0) return;
+ ecex_log_emit_raw_counted(ecex_frame_repeat_start, 0, ecex_frame_repeat_count);
+ ecex_log_emit_raw_counted(ecex_frame_repeat_end, 0, ecex_frame_repeat_count);
+ ecex_frame_repeat_count = 0;
+ ecex_frame_repeat_start[0] = '\0';
+ ecex_frame_repeat_end[0] = '\0';
+}
+
+static void ecex_log_flush_open_frame_group(void) {
+ if (!ecex_frame_group_open) return;
+ ecex_log_emit_raw_depth(ecex_frame_group_start, ecex_frame_group_depth);
+ ecex_frame_group_open = 0;
+ ecex_frame_group_start[0] = '\0';
+ ecex_frame_group_depth = 0;
+}
+
+static int ecex_log_is_frame_start(const char *line) {
+ return ecex_starts_with(line, "frame start ");
+}
+
+static int ecex_log_is_frame_end(const char *line) {
+ return ecex_streq(line, "frame end");
+}
+
+static void ecex_log_flush_repeat(void) {
+ char line[64];
+ size_t len = 0;
+ if (ecex_last_log_count > 1) {
+ len = ecex_append_cstr(line, len, sizeof(line), "... [");
+ len = ecex_append_ulong(line, len, sizeof(line), ecex_last_log_count);
+ len = ecex_append_char(line, len, sizeof(line), ']');
+ ecex_log_emit_raw_depth(line, ecex_last_log_depth);
+ }
+ ecex_last_log_count = 0;
+ ecex_last_log_line[0] = '\0';
+ ecex_last_log_depth = 0;
+}
+
+void ecex_log_flush(void) {
+ ecex_log_flush_open_frame_group();
+ ecex_log_flush_frame_repeats();
+ ecex_log_flush_repeat();
+}
+
+static void ecex_log_write_line_depth(const char *line, int depth) {
+ char safe_line[1024];
+ if (depth < 0) depth = 0;
+ ecex_strn_copy(safe_line, sizeof(safe_line), line);
+
+ ecex_log_flush_open_frame_group();
+ ecex_log_flush_frame_repeats();
+
+ if (ecex_last_log_count > 0 &&
+ ecex_last_log_depth == depth &&
+ ecex_streq(ecex_last_log_line, safe_line)) {
+ ecex_last_log_count++;
+ return;
+ }
+
+ ecex_log_flush_repeat();
+ ecex_strn_copy(ecex_last_log_line, sizeof(ecex_last_log_line), safe_line);
+ ecex_last_log_depth = depth;
+ ecex_last_log_count = 1;
+ ecex_log_emit_raw_depth(safe_line, depth);
+}
+
+static void ecex_log_write_line(const char *line) {
+ ecex_log_write_line_depth(line, ecex_log_group_depth);
+}
+
+void ecex_log(const char *message) {
+ if (!ecex_log_enabled()) return;
+ ecex_log_write_line(message ? message : "(null)");
+}
+
+void ecex_logf(const char *fmt, ...) {
+ char line[1024];
+ va_list ap;
+ if (!ecex_log_enabled()) return;
+ if (!fmt) {
+ ecex_log_write_line("(null)");
+ return;
+ }
+ va_start(ap, fmt);
+ vsnprintf(line, sizeof(line), fmt, ap);
+ va_end(ap);
+ ecex_log_write_line(line);
+}
+
+void ecex_log_group_begin(const char *message) {
+ if (!ecex_log_enabled()) return;
+ if (ecex_log_group_depth == 0 && ecex_log_is_frame_start(message)) {
+ ecex_log_flush_repeat();
+ if (ecex_frame_repeat_count > 0 &&
+ !ecex_streq(ecex_frame_repeat_start, message ? message : "group start")) {
+ ecex_log_flush_frame_repeats();
+ }
+ ecex_strn_copy(ecex_frame_group_start,
+ sizeof(ecex_frame_group_start),
+ message ? message : "group start");
+ ecex_frame_group_depth = ecex_log_group_depth;
+ ecex_frame_group_open = 1;
+ ecex_log_group_depth++;
+ return;
+ }
+
+ ecex_log_flush_open_frame_group();
+ ecex_log_flush_frame_repeats();
+ ecex_log_flush_repeat();
+ ecex_log_write_line_depth(message ? message : "group start", ecex_log_group_depth);
+ ecex_log_group_depth++;
+}
+
+void ecex_log_group_end(const char *message) {
+ if (!ecex_log_enabled()) return;
+ ecex_log_flush_repeat();
+ if (ecex_log_group_depth > 0) ecex_log_group_depth--;
+
+ if (ecex_frame_group_open &&
+ ecex_log_group_depth == ecex_frame_group_depth &&
+ ecex_log_is_frame_end(message)) {
+ const char *end = message ? message : "group end";
+ if (ecex_frame_repeat_count > 0 &&
+ ecex_streq(ecex_frame_repeat_start, ecex_frame_group_start) &&
+ ecex_streq(ecex_frame_repeat_end, end)) {
+ ecex_frame_repeat_count++;
+ } else {
+ ecex_log_flush_frame_repeats();
+ ecex_strn_copy(ecex_frame_repeat_start,
+ sizeof(ecex_frame_repeat_start),
+ ecex_frame_group_start);
+ ecex_strn_copy(ecex_frame_repeat_end,
+ sizeof(ecex_frame_repeat_end),
+ end);
+ ecex_frame_repeat_count = 1;
+ }
+ ecex_frame_group_open = 0;
+ ecex_frame_group_start[0] = '\0';
+ ecex_frame_group_depth = 0;
+ return;
+ }
+
+ ecex_log_flush_open_frame_group();
+ ecex_log_flush_frame_repeats();
+ ecex_log_write_line_depth(message ? message : "group end", ecex_log_group_depth);
+}
+
+void ecex_log_int(const char *message, int value) {
+ char line[1024];
+ size_t len = 0;
+ if (!ecex_log_enabled()) return;
+ len = ecex_append_cstr(line, len, sizeof(line), message ? message : "");
+ ecex_append_int(line, len, sizeof(line), value);
+ ecex_log_write_line(line);
+}
+
+void ecex_log_double(const char *message, double value) {
+ char line[1024];
+ size_t len = 0;
+ if (!ecex_log_enabled()) return;
+ len = ecex_append_cstr(line, len, sizeof(line), message ? message : "");
+ ecex_append_double(line, len, sizeof(line), value);
+ ecex_log_write_line(line);
+}
+
+void ecex_log_ptr(const char *message, const void *ptr) {
+ char line[1024];
+ size_t len = 0;
+ if (!ecex_log_enabled()) return;
+ len = ecex_append_cstr(line, len, sizeof(line), message ? message : "");
+ ecex_append_ptr(line, len, sizeof(line), ptr);
+ ecex_log_write_line(line);
+}
diff --git a/src/main.c b/src/main.c
index 94a08fa..191a159 100644
--- a/src/main.c
+++ b/src/main.c
@@ -17,9 +17,8 @@ static void print_usage(const char *argv0) {
fprintf(stderr, " %s [--config path/to/ecexrc.c] [--font path/to/font.ttf]\n", argv0);
fprintf(stderr, "\n");
fprintf(stderr, "keys:\n");
- fprintf(stderr, " F1 opens M-x\n");
+ fprintf(stderr, " M-x opens the command prompt\n");
fprintf(stderr, " Tab completes M-x command names\n");
- fprintf(stderr, " Alt+x opens M-x when the OS/window-manager allows it\n");
fprintf(stderr, " C-x is reserved for prefix maps\n");
fprintf(stderr, "\n");
fprintf(stderr, "env:\n");
@@ -141,15 +140,18 @@ int main(int argc, char **argv) {
}
snprintf(app.font_path, sizeof(app.font_path), "%s", font_path);
+ app.font_revision_seen = ed->font_revision;
+ app.ui_revision_seen = ed->ui_revision;
app_install_callbacks(&app);
- app_message(&app, "F1 for M-x. Tab completes commands.");
+ app_message(&app, "M-x for commands. Tab completes.");
while (!glfwWindowShouldClose(window) && !ed->should_quit) {
double now = glfwGetTime();
if (ecex_media_tick(ed, now) || ecex_tick_animations(ed, now)) {
app.dirty = 1;
}
+ app_sync_editor_ui(&app);
if (app.dirty) {
render(&app);
diff --git a/src/media.c b/src/media.c
index 6c7a989..4e14a96 100644
--- a/src/media.c
+++ b/src/media.c
@@ -10,11 +10,70 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
+#include <signal.h>
+#include <sys/wait.h>
#include <unistd.h>
extern FILE *popen(const char *command, const char *type);
extern int pclose(FILE *stream);
extern int mkstemp(char *template);
+extern int kill(pid_t pid, int sig);
+
+static int media_audio_enabled(void) {
+ const char *enabled = getenv("ECEX_MEDIA_AUDIO");
+ return !enabled || !enabled[0] || enabled[0] != '0';
+}
+
+static void media_audio_stop(buffer_t *buffer) {
+ if (!buffer || buffer->media_audio_pid <= 0) return;
+
+ pid_t pid = (pid_t)buffer->media_audio_pid;
+ kill(pid, SIGTERM);
+
+ if (waitpid(pid, NULL, WNOHANG) != pid) {
+ kill(pid, SIGKILL);
+ waitpid(pid, NULL, 0);
+ }
+ buffer->media_audio_pid = 0;
+}
+
+static void media_audio_reap(buffer_t *buffer) {
+ if (!buffer || buffer->media_audio_pid <= 0) return;
+ if (waitpid((pid_t)buffer->media_audio_pid, NULL, WNOHANG) > 0) {
+ buffer->media_audio_pid = 0;
+ }
+}
+
+static int media_audio_start(buffer_t *buffer) {
+ if (!buffer || !buffer->media_path || !media_audio_enabled()) return ECEX_ERR;
+ if (buffer->media_audio_pid > 0) return ECEX_OK;
+
+ const char *player = getenv("ECEX_FFPLAY");
+ if (!player || !player[0]) player = "ffplay";
+
+ pid_t pid = fork();
+ if (pid < 0) return ECEX_ERR;
+
+ if (pid == 0) {
+ execlp(player,
+ player,
+ "-nodisp",
+ "-autoexit",
+ "-loglevel",
+ "quiet",
+ buffer->media_path,
+ (char *)NULL);
+ _exit(127);
+ }
+
+ buffer->media_audio_pid = (int)pid;
+ return ECEX_OK;
+}
+
+static void media_audio_set_paused(buffer_t *buffer, int paused) {
+ if (!buffer || buffer->media_audio_pid <= 0) return;
+ kill((pid_t)buffer->media_audio_pid, paused ? SIGSTOP : SIGCONT);
+}
static char *shell_quote(const char *path) {
if (!path) return NULL;
@@ -220,6 +279,7 @@ static void buffer_set_media_text(buffer_t *buffer, const char *path, int video,
void ecex_media_buffer_clear(buffer_t *buffer) {
if (!buffer) return;
+ media_audio_stop(buffer);
if (buffer->media_pipe) {
pclose((FILE *)buffer->media_pipe);
buffer->media_pipe = NULL;
@@ -236,6 +296,7 @@ void ecex_media_buffer_clear(buffer_t *buffer) {
buffer->media_texture_height = 0;
buffer->media_last_frame_time = 0.0;
buffer->media_playing = 0;
+ buffer->media_audio_pid = 0;
buffer->media_status[0] = '\0';
}
@@ -307,7 +368,11 @@ int ecex_media_load_into_buffer(ecex_t *ed, const char *path, buffer_t *buffer)
if (video) {
buffer->media_pipe = pipe;
buffer->media_playing = 1;
- snprintf(buffer->media_status, sizeof(buffer->media_status), "Playing at decoded 60fps stream");
+ if (media_audio_start(buffer) == ECEX_OK) {
+ snprintf(buffer->media_status, sizeof(buffer->media_status), "Playing decoded 60fps stream with ffplay audio");
+ } else {
+ snprintf(buffer->media_status, sizeof(buffer->media_status), "Playing decoded 60fps stream; audio unavailable");
+ }
} else {
pclose(pipe);
snprintf(buffer->media_status, sizeof(buffer->media_status), "Image decoded via ffmpeg");
@@ -345,6 +410,11 @@ int ecex_media_toggle_playback(ecex_t *ed) {
buffer_t *buffer = ecex_current_buffer(ed);
if (!buffer || buffer->media_kind != ECEX_MEDIA_VIDEO) return ECEX_ERR;
buffer->media_playing = !buffer->media_playing;
+ if (buffer->media_playing && buffer->media_audio_pid <= 0) {
+ media_audio_start(buffer);
+ } else {
+ media_audio_set_paused(buffer, !buffer->media_playing);
+ }
snprintf(buffer->media_status,
sizeof(buffer->media_status),
"%s",
@@ -358,6 +428,7 @@ int ecex_media_tick(ecex_t *ed, double now_seconds) {
for (size_t i = 0; i < ed->buffer_count; i++) {
buffer_t *buffer = ed->buffers[i];
if (!buffer || buffer->media_kind != ECEX_MEDIA_VIDEO || !buffer->media_pipe || !buffer->media_playing) continue;
+ media_audio_reap(buffer);
if (buffer->media_last_frame_time > 0.0 && now_seconds - buffer->media_last_frame_time < (1.0 / 60.0)) continue;
int w = 0, h = 0;
@@ -370,6 +441,7 @@ int ecex_media_tick(ecex_t *ed, double now_seconds) {
pclose((FILE *)buffer->media_pipe);
buffer->media_pipe = NULL;
buffer->media_playing = 0;
+ media_audio_stop(buffer);
snprintf(buffer->media_status, sizeof(buffer->media_status), "Playback finished");
dirty = 1;
}
diff --git a/src/plugin.c b/src/plugin.c
new file mode 100644
index 0000000..ac849d1
--- /dev/null
+++ b/src/plugin.c
@@ -0,0 +1,613 @@
+#include "plugin.h"
+
+#include "common.h"
+#include "ecex.h"
+#include "util.h"
+
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+typedef struct ecex_plugin_slot {
+ char *name;
+ void *data;
+ size_t elem_size;
+ size_t count;
+ int type;
+ int exported;
+ int export_flags;
+} ecex_plugin_slot_t;
+
+typedef struct ecex_plugin_object {
+ char *name;
+ void *ptr;
+ size_t size;
+} ecex_plugin_object_t;
+
+typedef struct ecex_plugin_text {
+ int id;
+ char *text;
+ size_t len;
+} ecex_plugin_text_t;
+
+typedef struct ecex_plugin_file_handler {
+ char *extension;
+ ecex_file_handler_fn fn;
+} ecex_plugin_file_handler_t;
+
+struct ecex_plugin {
+ ecex_t *ed;
+ char *id;
+ int api_version;
+
+ ecex_plugin_slot_t *slots;
+ size_t slot_count;
+ size_t slot_cap;
+
+ ecex_plugin_object_t *objects;
+ size_t object_count;
+ size_t object_cap;
+
+ ecex_plugin_text_t *texts;
+ size_t text_count;
+ size_t text_cap;
+
+ ecex_plugin_file_handler_t *file_handlers;
+ size_t file_handler_count;
+ size_t file_handler_cap;
+};
+
+struct ecex_plugin_runtime {
+ ecex_plugin_t **plugins;
+ size_t plugin_count;
+ size_t plugin_cap;
+};
+
+static int plugin_id_valid(const char *id) {
+ if (!id || !id[0]) return 0;
+ for (const unsigned char *p = (const unsigned char *)id; *p; ++p) {
+ if ((*p >= 'a' && *p <= 'z') ||
+ (*p >= '0' && *p <= '9') ||
+ *p == '-' || *p == '_' || *p == '.') {
+ continue;
+ }
+ return 0;
+ }
+ return 1;
+}
+
+static int str_eq(const char *a, const char *b) {
+ if (!a) a = "";
+ if (!b) b = "";
+ return strcmp(a, b) == 0;
+}
+
+static int 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 *path_extension(const char *path) {
+ const char *dot;
+ if (!path) return NULL;
+ dot = strrchr(path, '.');
+ return dot && dot[0] ? dot : NULL;
+}
+
+static void slot_clear(ecex_plugin_slot_t *slot) {
+ if (!slot) return;
+ free(slot->name);
+ free(slot->data);
+ memset(slot, 0, sizeof(*slot));
+}
+
+static void object_clear(ecex_plugin_object_t *object) {
+ if (!object) return;
+ free(object->name);
+ free(object->ptr);
+ memset(object, 0, sizeof(*object));
+}
+
+static void text_clear(ecex_plugin_text_t *text) {
+ if (!text) return;
+ free(text->text);
+ memset(text, 0, sizeof(*text));
+}
+
+static void file_handler_clear(ecex_plugin_file_handler_t *handler) {
+ if (!handler) return;
+ free(handler->extension);
+ memset(handler, 0, sizeof(*handler));
+}
+
+static void plugin_free(ecex_plugin_t *plugin) {
+ if (!plugin) return;
+ for (size_t i = 0; i < plugin->slot_count; ++i) slot_clear(&plugin->slots[i]);
+ for (size_t i = 0; i < plugin->object_count; ++i) object_clear(&plugin->objects[i]);
+ for (size_t i = 0; i < plugin->text_count; ++i) text_clear(&plugin->texts[i]);
+ for (size_t i = 0; i < plugin->file_handler_count; ++i) file_handler_clear(&plugin->file_handlers[i]);
+ free(plugin->slots);
+ free(plugin->objects);
+ free(plugin->texts);
+ free(plugin->file_handlers);
+ free(plugin->id);
+ free(plugin);
+}
+
+ecex_plugin_runtime_t *ecex_plugin_runtime_new(void) {
+ return calloc(1, sizeof(ecex_plugin_runtime_t));
+}
+
+void ecex_plugin_runtime_free(ecex_plugin_runtime_t *runtime) {
+ if (!runtime) return;
+ for (size_t i = 0; i < runtime->plugin_count; ++i) {
+ plugin_free(runtime->plugins[i]);
+ }
+ free(runtime->plugins);
+ free(runtime);
+}
+
+static ecex_plugin_slot_t *slot_find(ecex_plugin_t *plugin, const char *name) {
+ if (!plugin || !name) return NULL;
+ for (size_t i = 0; i < plugin->slot_count; ++i) {
+ if (str_eq(plugin->slots[i].name, name)) return &plugin->slots[i];
+ }
+ return NULL;
+}
+
+static ecex_plugin_object_t *object_find(ecex_plugin_t *plugin, void *object) {
+ if (!plugin || !object) return NULL;
+ for (size_t i = 0; i < plugin->object_count; ++i) {
+ if (plugin->objects[i].ptr == object) return &plugin->objects[i];
+ }
+ return NULL;
+}
+
+static ecex_plugin_text_t *text_find(ecex_plugin_t *plugin, int id) {
+ if (!plugin) return NULL;
+ for (size_t i = 0; i < plugin->text_count; ++i) {
+ if (plugin->texts[i].id == id) return &plugin->texts[i];
+ }
+ return NULL;
+}
+
+ecex_plugin_t *ecex_plugin_find(ecex_t *ed, const char *id) {
+ if (!ed || !ed->plugins || !id) return NULL;
+ for (size_t i = 0; i < ed->plugins->plugin_count; ++i) {
+ ecex_plugin_t *plugin = ed->plugins->plugins[i];
+ if (plugin && str_eq(plugin->id, id)) return plugin;
+ }
+ return NULL;
+}
+
+ecex_plugin_t *ecex_plugin_register(ecex_t *ed, const char *id, int api_version) {
+ ecex_plugin_t *plugin;
+ if (!ed || !ed->plugins || !plugin_id_valid(id)) return NULL;
+ if (api_version != ECEX_PLUGIN_API_VERSION) return NULL;
+ if (ecex_plugin_find(ed, id)) return NULL;
+ if (ECEX_GROW_ARRAY(ed->plugins->plugins,
+ ed->plugins->plugin_count,
+ ed->plugins->plugin_cap,
+ 8) != ECEX_OK) {
+ return NULL;
+ }
+ plugin = calloc(1, sizeof(*plugin));
+ if (!plugin) return NULL;
+ plugin->id = ecex_strdup(id);
+ if (!plugin->id) {
+ free(plugin);
+ return NULL;
+ }
+ plugin->ed = ed;
+ plugin->api_version = api_version;
+ ed->plugins->plugins[ed->plugins->plugin_count++] = plugin;
+ return plugin;
+}
+
+ecex_plugin_t *ecex_plugin_require(ecex_t *ed, const char *id, int api_version) {
+ ecex_plugin_t *plugin;
+ if (api_version != ECEX_PLUGIN_API_VERSION) return NULL;
+ plugin = ecex_plugin_find(ed, id);
+ return plugin ? plugin : ecex_plugin_register(ed, id, api_version);
+}
+
+const char *ecex_plugin_id(ecex_plugin_t *plugin) {
+ return plugin ? plugin->id : NULL;
+}
+
+void *ecex_plugin_slot_alloc(ecex_plugin_t *plugin,
+ const char *name,
+ size_t count,
+ size_t elem_size) {
+ ecex_plugin_slot_t *slot;
+ void *data;
+ if (!plugin || !name || !name[0]) return NULL;
+ if (count == 0) count = 1;
+ if (elem_size == 0) elem_size = 1;
+ if (count > ((size_t)-1) / elem_size) return NULL;
+
+ slot = slot_find(plugin, name);
+ if (slot) {
+ if (slot->elem_size != elem_size) return NULL;
+ if (slot->count < count) {
+ data = realloc(slot->data, count * elem_size);
+ if (!data) return NULL;
+ memset((char *)data + slot->count * elem_size, 0, (count - slot->count) * elem_size);
+ slot->data = data;
+ slot->count = count;
+ }
+ return slot->data;
+ }
+
+ if (ECEX_GROW_ARRAY(plugin->slots,
+ plugin->slot_count,
+ plugin->slot_cap,
+ 16) != ECEX_OK) {
+ return NULL;
+ }
+ data = calloc(count, elem_size);
+ if (!data) return NULL;
+
+ slot = &plugin->slots[plugin->slot_count++];
+ memset(slot, 0, sizeof(*slot));
+ slot->name = ecex_strdup(name);
+ if (!slot->name) {
+ free(data);
+ --plugin->slot_count;
+ return NULL;
+ }
+ slot->data = data;
+ slot->count = count;
+ slot->elem_size = elem_size;
+ slot->type = elem_size == sizeof(int) ? ECEX_PLUGIN_I32 : ECEX_PLUGIN_BYTES;
+ return slot->data;
+}
+
+void *ecex_plugin_slot_get(ecex_plugin_t *plugin, const char *name) {
+ ecex_plugin_slot_t *slot = slot_find(plugin, name);
+ return slot ? slot->data : NULL;
+}
+
+int ecex_plugin_slot_free(ecex_plugin_t *plugin, const char *name) {
+ if (!plugin || !name) return ECEX_ERR;
+ for (size_t i = 0; i < plugin->slot_count; ++i) {
+ if (!str_eq(plugin->slots[i].name, name)) continue;
+ slot_clear(&plugin->slots[i]);
+ if (i + 1 < plugin->slot_count) {
+ memmove(&plugin->slots[i], &plugin->slots[i + 1],
+ (plugin->slot_count - i - 1) * sizeof(plugin->slots[i]));
+ }
+ --plugin->slot_count;
+ return ECEX_OK;
+ }
+ return ECEX_ERR;
+}
+
+int ecex_plugin_slot_set_export_flags(ecex_plugin_t *plugin,
+ const char *name,
+ int type,
+ int flags) {
+ ecex_plugin_slot_t *slot = slot_find(plugin, name);
+ if (!slot) return ECEX_ERR;
+ slot->type = type;
+ slot->exported = 1;
+ slot->export_flags = flags;
+ return ECEX_OK;
+}
+
+int ecex_plugin_slot_read_exported(ecex_t *ed,
+ const char *plugin_id,
+ const char *name,
+ void *out,
+ size_t out_cap,
+ size_t *out_len) {
+ ecex_plugin_t *plugin = ecex_plugin_find(ed, plugin_id);
+ ecex_plugin_slot_t *slot = slot_find(plugin, name);
+ size_t len;
+ if (out_len) *out_len = 0;
+ if (!slot || !slot->exported || !(slot->export_flags & ECEX_PLUGIN_EXPORT_READ)) return ECEX_ERR;
+ len = slot->count * slot->elem_size;
+ if (out_len) *out_len = len;
+ if (!out || out_cap < len) return ECEX_ERR;
+ if (len) memcpy(out, slot->data, len);
+ return ECEX_OK;
+}
+
+int ecex_plugin_slot_i32_get(ecex_plugin_t *plugin,
+ const char *name,
+ size_t index,
+ int fallback) {
+ ecex_plugin_slot_t *slot = slot_find(plugin, name);
+ if (!slot || !slot->data || slot->elem_size != sizeof(int) || index >= slot->count) return fallback;
+ return ((int *)slot->data)[index];
+}
+
+int ecex_plugin_slot_i32_get_scalar(ecex_plugin_t *plugin, const char *name, int fallback) {
+ return ecex_plugin_slot_i32_get(plugin, name, 0, fallback);
+}
+
+int ecex_plugin_slot_i32_set(ecex_plugin_t *plugin,
+ const char *name,
+ size_t index,
+ int value) {
+ int *data;
+ if (!plugin || !name) return ECEX_ERR;
+ data = (int *)ecex_plugin_slot_alloc(plugin, name, index + 1, sizeof(int));
+ if (!data) return ECEX_ERR;
+ data[index] = value;
+ return ECEX_OK;
+}
+
+static int slot_index_2d(size_t width, size_t x, size_t y, size_t *out) {
+ if (!out || width == 0 || x >= width) return ECEX_ERR;
+ if (y > (((size_t)-1) - x) / width) return ECEX_ERR;
+ *out = y * width + x;
+ return ECEX_OK;
+}
+
+int ecex_plugin_slot_i32_get_2d(ecex_plugin_t *plugin,
+ const char *name,
+ size_t width,
+ size_t x,
+ size_t y,
+ int fallback) {
+ size_t index;
+ if (slot_index_2d(width, x, y, &index) != ECEX_OK) return fallback;
+ return ecex_plugin_slot_i32_get(plugin, name, index, fallback);
+}
+
+int ecex_plugin_slot_i32_set_2d(ecex_plugin_t *plugin,
+ const char *name,
+ size_t width,
+ size_t x,
+ size_t y,
+ int value) {
+ size_t index;
+ if (slot_index_2d(width, x, y, &index) != ECEX_OK) return ECEX_ERR;
+ return ecex_plugin_slot_i32_set(plugin, name, index, value);
+}
+
+int ecex_plugin_slot_i32_set_scalar(ecex_plugin_t *plugin,
+ const char *name,
+ int value) {
+ return ecex_plugin_slot_i32_set(plugin, name, 0, value);
+}
+
+void *ecex_plugin_object_alloc(ecex_plugin_t *plugin, const char *name, size_t size) {
+ ecex_plugin_object_t *object;
+ void *ptr;
+ if (!plugin) return NULL;
+ if (size == 0) size = 1;
+ if (ECEX_GROW_ARRAY(plugin->objects,
+ plugin->object_count,
+ plugin->object_cap,
+ 16) != ECEX_OK) {
+ return NULL;
+ }
+ ptr = malloc(size);
+ if (!ptr) return NULL;
+ object = &plugin->objects[plugin->object_count++];
+ memset(object, 0, sizeof(*object));
+ object->name = ecex_strdup(name ? name : "");
+ if (!object->name) {
+ free(ptr);
+ --plugin->object_count;
+ return NULL;
+ }
+ object->ptr = ptr;
+ object->size = size;
+ return ptr;
+}
+
+void *ecex_plugin_object_calloc(ecex_plugin_t *plugin,
+ const char *name,
+ size_t count,
+ size_t size) {
+ void *ptr;
+ size_t total;
+ if (!plugin) 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_plugin_object_alloc(plugin, name, total);
+ if (!ptr) return NULL;
+ memset(ptr, 0, total);
+ return ptr;
+}
+
+int ecex_plugin_object_free(ecex_plugin_t *plugin, void *object) {
+ if (!plugin || !object) return ECEX_ERR;
+ for (size_t i = 0; i < plugin->object_count; ++i) {
+ if (plugin->objects[i].ptr != object) continue;
+ object_clear(&plugin->objects[i]);
+ if (i + 1 < plugin->object_count) {
+ memmove(&plugin->objects[i], &plugin->objects[i + 1],
+ (plugin->object_count - i - 1) * sizeof(plugin->objects[i]));
+ }
+ --plugin->object_count;
+ return ECEX_OK;
+ }
+ return ECEX_ERR;
+}
+
+int ecex_plugin_object_valid(ecex_plugin_t *plugin, void *object) {
+ return object_find(plugin, object) ? 1 : 0;
+}
+
+int ecex_plugin_object_i32_get(ecex_plugin_t *plugin,
+ void *object,
+ size_t byte_offset,
+ int fallback) {
+ ecex_plugin_object_t *entry = object_find(plugin, object);
+ int value;
+ if (!entry || !entry->ptr || byte_offset > entry->size ||
+ entry->size - byte_offset < sizeof(value)) {
+ return fallback;
+ }
+ memcpy(&value, (const char *)entry->ptr + byte_offset, sizeof(value));
+ return value;
+}
+
+int ecex_plugin_object_i32_set(ecex_plugin_t *plugin,
+ void *object,
+ size_t byte_offset,
+ int value) {
+ ecex_plugin_object_t *entry = object_find(plugin, object);
+ if (!entry || !entry->ptr || byte_offset > entry->size ||
+ entry->size - byte_offset < sizeof(value)) {
+ return ECEX_ERR;
+ }
+ memcpy((char *)entry->ptr + byte_offset, &value, sizeof(value));
+ return ECEX_OK;
+}
+
+void *ecex_plugin_object_ptr_get(ecex_plugin_t *plugin,
+ void *object,
+ size_t byte_offset) {
+ ecex_plugin_object_t *entry = object_find(plugin, object);
+ void *value = NULL;
+ if (!entry || !entry->ptr || byte_offset > entry->size ||
+ entry->size - byte_offset < sizeof(value)) {
+ return NULL;
+ }
+ memcpy(&value, (const char *)entry->ptr + byte_offset, sizeof(value));
+ return value;
+}
+
+int ecex_plugin_object_ptr_set(ecex_plugin_t *plugin,
+ void *object,
+ size_t byte_offset,
+ void *value) {
+ ecex_plugin_object_t *entry = object_find(plugin, object);
+ if (!entry || !entry->ptr || byte_offset > entry->size ||
+ entry->size - byte_offset < sizeof(value)) {
+ return ECEX_ERR;
+ }
+ memcpy((char *)entry->ptr + byte_offset, &value, sizeof(value));
+ return ECEX_OK;
+}
+
+int ecex_plugin_text_set(ecex_plugin_t *plugin, int id, const char *text, int len) {
+ ecex_plugin_text_t *entry;
+ char *copy;
+ size_t n;
+ if (!plugin || id < 0) return ECEX_ERR;
+ if (!text) text = "";
+ n = len < 0 ? strlen(text) : (size_t)len;
+ copy = malloc(n + 1);
+ if (!copy) return ECEX_ERR;
+ if (n) memcpy(copy, text, n);
+ copy[n] = '\0';
+
+ entry = text_find(plugin, id);
+ if (!entry) {
+ if (ECEX_GROW_ARRAY(plugin->texts,
+ plugin->text_count,
+ plugin->text_cap,
+ 32) != ECEX_OK) {
+ free(copy);
+ return ECEX_ERR;
+ }
+ entry = &plugin->texts[plugin->text_count++];
+ memset(entry, 0, sizeof(*entry));
+ entry->id = id;
+ }
+ free(entry->text);
+ entry->text = copy;
+ entry->len = n;
+ return ECEX_OK;
+}
+
+int ecex_plugin_text_set_from_buffer_title(ecex_plugin_t *plugin, int id, buffer_t *buffer) {
+ const char *title = NULL;
+ if (buffer) title = buffer->path ? buffer->path : buffer->name;
+ return ecex_plugin_text_set(plugin, id, title ? title : "(unnamed)", -1);
+}
+
+int ecex_plugin_text_free(ecex_plugin_t *plugin, int id) {
+ if (!plugin) return ECEX_ERR;
+ for (size_t i = 0; i < plugin->text_count; ++i) {
+ if (plugin->texts[i].id != id) continue;
+ text_clear(&plugin->texts[i]);
+ if (i + 1 < plugin->text_count) {
+ memmove(&plugin->texts[i], &plugin->texts[i + 1],
+ (plugin->text_count - i - 1) * sizeof(plugin->texts[i]));
+ }
+ --plugin->text_count;
+ return ECEX_OK;
+ }
+ return ECEX_ERR;
+}
+
+int ecex_plugin_text_free_all(ecex_plugin_t *plugin) {
+ if (!plugin) return ECEX_ERR;
+ for (size_t i = 0; i < plugin->text_count; ++i) text_clear(&plugin->texts[i]);
+ plugin->text_count = 0;
+ return ECEX_OK;
+}
+
+const char *ecex_plugin_text_get_drawable(ecex_t *ed, void *owner, int id) {
+ ecex_plugin_t *plugin = (ecex_plugin_t *)owner;
+ (void)ed;
+ ecex_plugin_text_t *entry = text_find(plugin, id);
+ return entry && entry->text ? entry->text : "";
+}
+
+int ecex_plugin_file_handler_register(ecex_plugin_t *plugin,
+ const char *extension,
+ ecex_file_handler_fn fn) {
+ ecex_plugin_file_handler_t *handler;
+ if (!plugin || !extension || !extension[0] || !fn) return ECEX_ERR;
+ for (size_t i = 0; i < plugin->file_handler_count; ++i) {
+ if (ascii_equal_ci(plugin->file_handlers[i].extension, extension)) {
+ plugin->file_handlers[i].fn = fn;
+ return ECEX_OK;
+ }
+ }
+ if (ECEX_GROW_ARRAY(plugin->file_handlers,
+ plugin->file_handler_count,
+ plugin->file_handler_cap,
+ 8) != ECEX_OK) {
+ return ECEX_ERR;
+ }
+ handler = &plugin->file_handlers[plugin->file_handler_count++];
+ memset(handler, 0, sizeof(*handler));
+ handler->extension = ecex_strdup(extension);
+ if (!handler->extension) {
+ --plugin->file_handler_count;
+ return ECEX_ERR;
+ }
+ handler->fn = fn;
+ return ECEX_OK;
+}
+
+int ecex_plugin_file_handlers_run(ecex_t *ed, buffer_t *buffer) {
+ const char *path;
+ const char *ext;
+ if (!ed || !ed->plugins || !buffer) return ECEX_ERR;
+ path = buffer->path ? buffer->path : buffer->name;
+ ext = path_extension(path);
+ if (!ext) return ECEX_OK;
+ for (size_t i = 0; i < ed->plugins->plugin_count; ++i) {
+ ecex_plugin_t *plugin = ed->plugins->plugins[i];
+ if (!plugin) continue;
+ for (size_t j = 0; j < plugin->file_handler_count; ++j) {
+ if (ascii_equal_ci(plugin->file_handlers[j].extension, ext)) {
+ return plugin->file_handlers[j].fn(ed, buffer);
+ }
+ }
+ }
+ return ECEX_OK;
+}
diff --git a/src/render.c b/src/render.c
index 86bc535..527f29c 100644
--- a/src/render.c
+++ b/src/render.c
@@ -40,6 +40,18 @@ static float ecex_maxf(float a, float b) {
return a > b ? a : b;
}
+static size_t minibuffer_row_count(app_t *app) {
+ if (!app || app->mode != APP_MODE_PREFIX || !app->message[0]) return 1;
+
+ size_t rows = 1;
+ for (const char *p = app->message; *p; ++p) {
+ if (*p != '\n') continue;
+ rows++;
+ if (rows >= ECEX_MINIBUFFER_MAX_ROWS) return ECEX_MINIBUFFER_MAX_ROWS;
+ }
+ return rows;
+}
+
static ui_metrics_t ui_metrics(app_t *app) {
float size = app && app->font.size_px > 1.0f ? app->font.size_px : 16.0f;
float line_h = app && app->font.line_height > 1.0f
@@ -56,7 +68,7 @@ static ui_metrics_t ui_metrics(app_t *app) {
m.content_bottom_pad = m.pad_y;
m.status_h = line_h + m.pad_y * 2.0f;
- m.minibuffer_h = line_h + m.pad_y * 2.0f;
+ m.minibuffer_h = line_h * (float)minibuffer_row_count(app) + m.pad_y * 2.0f;
m.cursor_w = ecex_maxf(2.0f, size * 0.10f);
m.cursor_h = ecex_maxf(1.0f, app ? app->font.ascent_px + app->font.descent_px : size);
@@ -198,7 +210,7 @@ static void render_status_bar(app_t *app) {
char status[768];
snprintf(status,
sizeof(status),
- " %s%s %s line:%zu col:%zu top:%zu size:%zu buffers:%zu windows:%zu commands:%zu%s%s",
+ "%s%s | %s | Ln %zu, Col %zu | Top %zu | %zu bytes | %zu buffers / %zu windows%s%s",
buf->name ? buf->name : "(unnamed)",
buf->modified ? " *" : "",
ecex_buffer_major_mode_name(app->ed, buf),
@@ -208,7 +220,6 @@ static void render_status_bar(app_t *app) {
buf->len,
app->ed->buffer_count,
ecex_window_count(app->ed),
- app->ed->command_count,
buf->path ? " " : "",
buf->path ? buf->path : "");
@@ -262,6 +273,11 @@ static void render_minibuffer(app_t *app) {
app->ed->theme.minibuffer_bg.b);
draw_rect(0.0f, y, (float)app->width, h);
+ glColor3f(app->ed->theme.status_border.r,
+ app->ed->theme.status_border.g,
+ app->ed->theme.status_border.b);
+ draw_rect(0.0f, y, (float)app->width, 1.0f);
+
glColor3f(app->ed->theme.minibuffer_fg.r,
app->ed->theme.minibuffer_fg.g,
app->ed->theme.minibuffer_fg.b);
@@ -345,9 +361,29 @@ static void render_minibuffer(app_t *app) {
}
}
} else if (app->mode == APP_MODE_PREFIX) {
- char line[ECEX_PREFIX_SIZE + 8];
- snprintf(line, sizeof(line), "%s-", app->prefix);
- draw_text(&app->font, text_x, text_y, line);
+ if (app->message[0]) {
+ const char *line = app->message;
+ size_t row = 0;
+ while (line && *line && row < ECEX_MINIBUFFER_MAX_ROWS) {
+ const char *end = strchr(line, '\n');
+ size_t len = end ? (size_t)(end - line) : strlen(line);
+ char scratch[ECEX_MINIBUFFER_SIZE];
+ if (len >= sizeof(scratch)) len = sizeof(scratch) - 1;
+ memcpy(scratch, line, len);
+ scratch[len] = '\0';
+ draw_text(&app->font,
+ text_x,
+ line_baseline(app, y + m.pad_y + app->font.line_height * (float)row),
+ scratch);
+ row++;
+ if (!end) break;
+ line = end + 1;
+ }
+ } else {
+ char line[ECEX_PREFIX_SIZE + 8];
+ snprintf(line, sizeof(line), "%s-", app->prefix);
+ draw_text(&app->font, text_x, text_y, line);
+ }
} else {
draw_text(&app->font, text_x, text_y, app->message);
}
@@ -418,6 +454,241 @@ static void draw_selection_for_line(app_t *app,
draw_rect(x, line_top, w, app->font.line_height);
}
+static int c_syntax_ident_start(char c) {
+ return (c >= 'a' && c <= 'z') ||
+ (c >= 'A' && c <= 'Z') ||
+ c == '_';
+}
+
+static int c_syntax_ident_char(char c) {
+ return c_syntax_ident_start(c) || (c >= '0' && c <= '9');
+}
+
+static int c_syntax_keyword(const char *text, size_t len) {
+ static const char *keywords[] = {
+ "alignas", "alignof", "auto", "bool", "break", "case", "char",
+ "const", "constexpr", "continue", "default", "do", "double",
+ "else", "enum", "extern", "false", "float", "for", "goto", "if",
+ "inline", "int", "long", "nullptr", "register", "restrict",
+ "return", "short", "signed", "sizeof", "static", "static_assert",
+ "struct", "switch", "thread_local", "true", "typedef", "typeof",
+ "typeof_unqual", "union", "unsigned", "void", "volatile", "while",
+ "_Alignas", "_Alignof", "_Atomic", "_Bool", "_Complex", "_Generic",
+ "_Imaginary", "_Noreturn", "_Static_assert", "_Thread_local", "NULL",
+ "size_t", "ptrdiff_t", "uint8_t", "uint16_t", "uint32_t", "uint64_t",
+ "int8_t", "int16_t", "int32_t", "int64_t", "FILE"
+ };
+
+ for (size_t i = 0; i < sizeof(keywords) / sizeof(keywords[0]); i++) {
+ if (strlen(keywords[i]) == len && strncmp(text, keywords[i], len) == 0) {
+ return 1;
+ }
+ }
+ return 0;
+}
+
+static int c_syntax_line_is_preprocessor(buffer_t *buf, size_t line_start, size_t line_end) {
+ if (!buf || !buf->data) return 0;
+ size_t i = line_start;
+ while (i < line_end && (buf->data[i] == ' ' || buf->data[i] == '\t')) i++;
+ return i < line_end && buf->data[i] == '#';
+}
+
+static int c_syntax_block_comment_at(buffer_t *buf, size_t pos) {
+ if (!buf || !buf->data) return 0;
+
+ int in_block = 0;
+ int in_string = 0;
+ int in_char = 0;
+ int escaped = 0;
+
+ for (size_t i = 0; i < pos && i < buf->len; i++) {
+ char c = buf->data[i];
+ char next = i + 1 < pos && i + 1 < buf->len ? buf->data[i + 1] : '\0';
+
+ if (in_block) {
+ if (c == '*' && next == '/') {
+ in_block = 0;
+ i++;
+ }
+ continue;
+ }
+
+ if (in_string) {
+ if (escaped) escaped = 0;
+ else if (c == '\\') escaped = 1;
+ else if (c == '"') in_string = 0;
+ else if (c == '\n') in_string = 0;
+ continue;
+ }
+
+ if (in_char) {
+ if (escaped) escaped = 0;
+ else if (c == '\\') escaped = 1;
+ else if (c == '\'') in_char = 0;
+ else if (c == '\n') in_char = 0;
+ continue;
+ }
+
+ if (c == '/' && next == '*') {
+ in_block = 1;
+ i++;
+ } else if (c == '/' && next == '/') {
+ while (i < pos && i < buf->len && buf->data[i] != '\n') i++;
+ } else if (c == '"') {
+ in_string = 1;
+ } else if (c == '\'') {
+ in_char = 1;
+ }
+ }
+
+ return in_block;
+}
+
+static void c_syntax_set_color(app_t *app, int kind, int highlighted) {
+ if (highlighted) {
+ set_editor_text_color(app, highlighted);
+ return;
+ }
+
+ switch (kind) {
+ case 1: glColor3f(0.43f, 0.78f, 1.00f); break; /* keyword */
+ case 2: glColor3f(0.68f, 0.91f, 0.42f); break; /* string */
+ case 3: glColor3f(0.64f, 0.67f, 0.73f); break; /* comment */
+ case 4: glColor3f(1.00f, 0.58f, 0.78f); break; /* number */
+ case 5: glColor3f(1.00f, 0.82f, 0.25f); break; /* preprocessor */
+ default: set_editor_text_color(app, highlighted); break;
+ }
+}
+
+static void c_syntax_draw_span(app_t *app,
+ buffer_t *buf,
+ size_t start,
+ size_t end,
+ float *x,
+ float baseline,
+ int kind,
+ int highlighted) {
+ if (!app || !buf || !x || start >= end) return;
+
+ char *span = buffer_substring(buf, start, end);
+ if (!span) return;
+
+ c_syntax_set_color(app, kind, highlighted);
+ draw_text(&app->font, *x, baseline, span);
+ *x += text_width(&app->font, span);
+ free(span);
+}
+
+static void draw_c_syntax_line(app_t *app,
+ buffer_t *buf,
+ size_t line_start,
+ size_t visible_start,
+ size_t line_end,
+ float x,
+ float baseline,
+ int highlighted) {
+ if (!app || !buf || !buf->data || visible_start >= line_end) return;
+
+ if (c_syntax_line_is_preprocessor(buf, line_start, line_end)) {
+ c_syntax_draw_span(app, buf, visible_start, line_end, &x, baseline, 5, highlighted);
+ return;
+ }
+
+ int in_block = c_syntax_block_comment_at(buf, visible_start);
+ size_t pos = visible_start;
+
+ while (pos < line_end) {
+ if (in_block) {
+ size_t end = pos;
+ while (end + 1 < line_end &&
+ !(buf->data[end] == '*' && buf->data[end + 1] == '/')) {
+ end++;
+ }
+ if (end + 1 < line_end) {
+ end += 2;
+ in_block = 0;
+ } else {
+ end = line_end;
+ }
+ c_syntax_draw_span(app, buf, pos, end, &x, baseline, 3, highlighted);
+ pos = end;
+ continue;
+ }
+
+ char c = buf->data[pos];
+ char next = pos + 1 < line_end ? buf->data[pos + 1] : '\0';
+
+ if (c == '/' && next == '/') {
+ c_syntax_draw_span(app, buf, pos, line_end, &x, baseline, 3, highlighted);
+ break;
+ }
+
+ if (c == '/' && next == '*') {
+ size_t end = pos + 2;
+ while (end + 1 < line_end &&
+ !(buf->data[end] == '*' && buf->data[end + 1] == '/')) {
+ end++;
+ }
+ if (end + 1 < line_end) end += 2;
+ else end = line_end;
+ c_syntax_draw_span(app, buf, pos, end, &x, baseline, 3, highlighted);
+ pos = end;
+ continue;
+ }
+
+ if (c == '"' || c == '\'') {
+ char quote = c;
+ size_t end = pos + 1;
+ int escaped = 0;
+ while (end < line_end) {
+ char sc = buf->data[end++];
+ if (escaped) {
+ escaped = 0;
+ } else if (sc == '\\') {
+ escaped = 1;
+ } else if (sc == quote) {
+ break;
+ }
+ }
+ c_syntax_draw_span(app, buf, pos, end, &x, baseline, 2, highlighted);
+ pos = end;
+ continue;
+ }
+
+ if ((c >= '0' && c <= '9') || (c == '.' && next >= '0' && next <= '9')) {
+ size_t end = pos + 1;
+ while (end < line_end) {
+ char nc = buf->data[end];
+ if ((nc >= '0' && nc <= '9') ||
+ (nc >= 'a' && nc <= 'f') ||
+ (nc >= 'A' && nc <= 'F') ||
+ nc == 'x' || nc == 'X' || nc == 'u' || nc == 'U' ||
+ nc == 'l' || nc == 'L' || nc == '.' || nc == '+' || nc == '-') {
+ end++;
+ } else {
+ break;
+ }
+ }
+ c_syntax_draw_span(app, buf, pos, end, &x, baseline, 4, highlighted);
+ pos = end;
+ continue;
+ }
+
+ if (c_syntax_ident_start(c)) {
+ size_t end = pos + 1;
+ while (end < line_end && c_syntax_ident_char(buf->data[end])) end++;
+ int kind = c_syntax_keyword(buf->data + pos, end - pos) ? 1 : 0;
+ c_syntax_draw_span(app, buf, pos, end, &x, baseline, kind, highlighted);
+ pos = end;
+ continue;
+ }
+
+ c_syntax_draw_span(app, buf, pos, pos + 1, &x, baseline, 0, highlighted);
+ pos++;
+ }
+}
+
static void draw_buffer_line(app_t *app,
buffer_t *buf,
view_rect_t rect,
@@ -467,12 +738,20 @@ static void draw_buffer_line(app_t *app,
draw_text(&app->font, rect.x + m.pad_x * 0.5f, line_baseline(app, line_top), nbuf);
}
- char *line = buffer_substring(buf, start, line_end);
- if (!line) return;
+ float text_x = rect.x + m.content_x + gutter;
+ float baseline = line_baseline(app, line_top);
+ const char *mode_name = ecex_buffer_major_mode_name(app->ed, buf);
- set_editor_text_color(app, highlighted);
- draw_text(&app->font, rect.x + m.content_x + gutter, line_baseline(app, line_top), line);
- free(line);
+ if (mode_name && strcmp(mode_name, "c-mode") == 0) {
+ draw_c_syntax_line(app, buf, line_start, start, line_end, text_x, baseline, highlighted);
+ } else {
+ char *line = buffer_substring(buf, start, line_end);
+ if (!line) return;
+
+ set_editor_text_color(app, highlighted);
+ draw_text(&app->font, text_x, baseline, line);
+ free(line);
+ }
}
@@ -653,7 +932,7 @@ void ecex_draw_text_aligned(ecex_draw_context_t *ctx, float x, float y, float w,
ecex_draw_text(ctx, tx, y, text);
}
-void ecex_draw_color_rgba8(ecex_draw_context_t *ctx, int r, int g, int b, int a) {
+void ecex_draw_color_rgba8_i(ecex_draw_context_t *ctx, int r, int g, int b, int a) {
if (!ctx) return;
if (r < 0) r = 0; if (r > 255) r = 255;
if (g < 0) g = 0; if (g > 255) g = 255;
@@ -705,10 +984,7 @@ static const char *ecex_draw_label_text(int label_id) {
}
static unsigned char ecex_ascii5x7_bits(char ch, int row) {
- /* Tiny host-side pixel font used by CCDJIT-safe label helpers. This avoids
- * routing plugin labels through the normal stb/font text renderer, which is
- * useful while plugin callback ABI issues are being isolated. Bits are
- * returned left-to-right in the low 5 bits. */
+ /* Last-resort fallback for host labels when no editor font texture exists. */
if (row < 0 || row >= 7) return 0;
switch (ch) {
case '0': { static const unsigned char b[7] = {14,17,19,21,25,17,14}; return b[row]; }
@@ -778,47 +1054,91 @@ static void ecex_draw_mini_text_i(ecex_draw_context_t *ctx, int x, int y, const
}
}
+static int ecex_draw_has_loaded_font(ecex_draw_context_t *ctx) {
+ app_t *app = draw_context_app(ctx);
+ return app && app->font.texture != 0;
+}
+
+static void ecex_draw_host_text_i(ecex_draw_context_t *ctx, int x, int y, const char *text) {
+ if (!ctx || !text) return;
+ if (ecex_draw_has_loaded_font(ctx)) {
+ ecex_draw_text(ctx, (float)x, (float)y, text);
+ } else {
+ ecex_draw_mini_text_i(ctx, x, y, text);
+ }
+}
+
+static void ecex_draw_color_packed_rgba8_i(ecex_draw_context_t *ctx, unsigned int rgba) {
+ ecex_draw_color_rgba8_i(ctx,
+ (int)((rgba >> 24) & 0xffu),
+ (int)((rgba >> 16) & 0xffu),
+ (int)((rgba >> 8) & 0xffu),
+ (int)(rgba & 0xffu));
+}
-extern const char *ecex_text_get_for_draw(ecex_t *ed, void *owner, int id);
-void ecex_draw_text_id_i(ecex_draw_context_t *ctx, void *owner, int id, int x, int y) {
+void ecex_draw_plugin_text_i(ecex_draw_context_t *ctx, void *owner, int id, int x, int y) {
app_t *app;
const char *text;
if (!ctx || !ctx->internal) return;
app = (app_t *)ctx->internal;
if (!app || !app->ed) return;
- text = ecex_text_get_for_draw(app->ed, owner, id);
+ text = ecex_plugin_text_get_drawable(app->ed, owner, id);
if (!text) text = "";
/*
- * Plugin-safe real-font path: plugin code passes only owner/id and integer
- * coordinates. The string itself lives in the host text registry, and the
+ * Plugin-safe real-font path: plugin code passes only plugin/id and integer
+ * coordinates. The string itself lives in plugin-owned host storage, and the
* actual font renderer is called here on the host side, not directly from
- * JIT-owned stack/string memory. Fixed labels may still use the mini-font
- * helpers, but arbitrary plugin text such as Markdown should render with
- * the loaded editor font.
+ * JIT-owned stack/string memory.
*/
- ecex_draw_text(ctx, (float)x, (float)y, text);
+ ecex_draw_host_text_i(ctx, x, y, text);
+}
+
+void ecex_draw_plugin_text_rect_i(ecex_draw_context_t *ctx,
+ void *owner,
+ int id,
+ int x,
+ int y,
+ int w,
+ int h,
+ int padding,
+ unsigned int bg_rgba,
+ unsigned int fg_rgba) {
+ app_t *app;
+ const char *text;
+ if (!ctx || !ctx->internal || w <= 0 || h <= 0) return;
+ app = (app_t *)ctx->internal;
+ if (!app || !app->ed) return;
+ if (padding < 0) padding = 0;
+
+ text = ecex_plugin_text_get_drawable(app->ed, owner, id);
+ if (!text) text = "";
+
+ ecex_draw_color_packed_rgba8_i(ctx, bg_rgba);
+ ecex_draw_rect_i(ctx, x, y, w, h);
+ ecex_draw_color_packed_rgba8_i(ctx, fg_rgba);
+ ecex_draw_host_text_i(ctx, x + padding, y + padding, text);
}
-int ecex_draw_context_height_i(ecex_draw_context_t *ctx) {
+int ecex_draw_context_height_px(ecex_draw_context_t *ctx) {
if (!ctx) return 0;
return (int)ctx->h;
}
-int ecex_draw_context_line_height_i(ecex_draw_context_t *ctx) {
+int ecex_draw_context_line_height_px(ecex_draw_context_t *ctx) {
int line_h;
if (!ctx) return 18;
line_h = (int)ctx->line_height;
return line_h < 18 ? 18 : line_h;
}
-int ecex_markdown_body_y_i(ecex_draw_context_t *ctx) {
+int ecex_markdown_body_y_px(ecex_draw_context_t *ctx) {
int line_h;
if (!ctx) return 36;
- line_h = ecex_draw_context_line_height_i(ctx);
+ line_h = ecex_draw_context_line_height_px(ctx);
return (int)ctx->content_y + line_h * 2;
}
@@ -925,12 +1245,12 @@ static void md_host_set_and_draw(ecex_draw_context_t *ctx, void *owner, int y, i
if (!app || !app->ed) return;
start = md_host_trim_start(text);
len = md_host_trim_len(start);
- if (ecex_text_set(app->ed, owner, 2, start, len) == 0) {
+ if (ecex_plugin_text_set((ecex_plugin_t *)owner, 2, start, len) == 0) {
ecex_draw_markdown_line_auto_i(ctx, owner, 2, y, style);
}
}
-int ecex_markdown_draw_line_from_buffer_i(ecex_draw_context_t *ctx, void *owner, buffer_t *buffer, int line_index, int y, int in_code) {
+int ecex_markdown_draw_buffer_line_i(ecex_draw_context_t *ctx, void *owner, buffer_t *buffer, int line_index, int y, int in_code) {
char line[512];
const char *text = NULL;
int copied;
@@ -942,8 +1262,8 @@ int ecex_markdown_draw_line_from_buffer_i(ecex_draw_context_t *ctx, void *owner,
if (!ctx || !ctx->internal || !buffer) return 18;
app = (app_t *)ctx->internal;
- line_h = ecex_draw_context_line_height_i(ctx);
- copied = ecex_buffer_line_copy(buffer, line_index, line, (int)sizeof(line));
+ line_h = ecex_draw_context_line_height_px(ctx);
+ copied = ecex_buffer_line_copy_text(buffer, line_index, line, (int)sizeof(line));
if (line_index < 4) {
ecex_log_int("markdown_host_line: index=", line_index);
ecex_log_int("markdown_host_line: copied=", copied);
@@ -953,14 +1273,14 @@ int ecex_markdown_draw_line_from_buffer_i(ecex_draw_context_t *ctx, void *owner,
if (md_host_line_is_fence(line)) {
next_in_code = !next_in_code;
- if (app && app->ed) ecex_text_set(app->ed, owner, 2, next_in_code ? "code" : "end code", -1);
+ if (app && app->ed) ecex_plugin_text_set((ecex_plugin_t *)owner, 2, next_in_code ? "code" : "end code", -1);
ecex_draw_markdown_line_auto_i(ctx, owner, 2, y, 5);
advance = line_h + 8;
return advance | (next_in_code ? 0x10000 : 0);
}
if (next_in_code) {
- if (app && app->ed) ecex_text_set(app->ed, owner, 2, line, copied > 224 ? 224 : copied);
+ if (app && app->ed) ecex_plugin_text_set((ecex_plugin_t *)owner, 2, line, copied > 224 ? 224 : copied);
ecex_draw_markdown_line_auto_i(ctx, owner, 2, y, 3);
return line_h | 0x10000;
}
@@ -973,7 +1293,7 @@ int ecex_markdown_draw_line_from_buffer_i(ecex_draw_context_t *ctx, void *owner,
}
if (md_host_line_is_hr(line)) {
- if (app && app->ed) ecex_text_set(app->ed, owner, 2, "", 0);
+ if (app && app->ed) ecex_plugin_text_set((ecex_plugin_t *)owner, 2, "", 0);
ecex_draw_markdown_line_auto_i(ctx, owner, 2, y, 6);
return line_h;
}
@@ -1008,13 +1328,13 @@ void ecex_draw_markdown_canvas_i(ecex_draw_context_t *ctx, void *owner, int titl
if (w < 1) w = full_w - x * 2;
if (w < 1) w = 1;
if (line_h < 18) line_h = 18;
- ecex_draw_color_rgba8(ctx, 29, 32, 33, 255);
+ ecex_draw_color_rgba8_i(ctx, 29, 32, 33, 255);
ecex_draw_rect_i(ctx, 0, 0, full_w, full_h);
- ecex_draw_color_rgba8(ctx, 250, 241, 199, 255);
- title = (app && app->ed) ? ecex_text_get_for_draw(app->ed, owner, title_id) : "";
+ ecex_draw_color_rgba8_i(ctx, 250, 241, 199, 255);
+ title = (app && app->ed) ? ecex_plugin_text_get_drawable(app->ed, owner, title_id) : "";
if (!title) title = "";
ecex_draw_text(ctx, (float)x, (float)y, title);
- ecex_draw_color_rgba8(ctx, 80, 73, 69, 255);
+ ecex_draw_color_rgba8_i(ctx, 80, 73, 69, 255);
ecex_draw_line_i(ctx, x, y + line_h + 8, x + w, y + line_h + 8, 1);
}
@@ -1024,49 +1344,49 @@ void ecex_draw_markdown_text_i(ecex_draw_context_t *ctx, void *owner, int text_i
int h;
if (!ctx || !ctx->internal) return;
app = (app_t *)ctx->internal;
- text = (app && app->ed) ? ecex_text_get_for_draw(app->ed, owner, text_id) : "";
+ text = (app && app->ed) ? ecex_plugin_text_get_drawable(app->ed, owner, text_id) : "";
if (!text) text = "";
if (w < 1) w = 1;
if (line_h < 18) line_h = 18;
h = line_h + 4;
switch (style) {
case 1: /* heading */
- ecex_draw_color_rgba8(ctx, 69, 84, 96, 255);
+ ecex_draw_color_rgba8_i(ctx, 69, 84, 96, 255);
ecex_draw_rect_i(ctx, x, y - 5, w, h + 6);
- ecex_draw_color_rgba8(ctx, 250, 189, 47, 255);
+ ecex_draw_color_rgba8_i(ctx, 250, 189, 47, 255);
ecex_draw_text(ctx, (float)(x + 10), (float)y, text);
break;
case 2: /* quote */
- ecex_draw_color_rgba8(ctx, 131, 165, 152, 255);
+ ecex_draw_color_rgba8_i(ctx, 131, 165, 152, 255);
ecex_draw_rect_i(ctx, x, y - 2, 4, h);
- ecex_draw_color_rgba8(ctx, 213, 196, 161, 255);
+ ecex_draw_color_rgba8_i(ctx, 213, 196, 161, 255);
ecex_draw_text(ctx, (float)(x + 14), (float)y, text);
break;
case 3: /* code */
- ecex_draw_color_rgba8(ctx, 40, 40, 40, 255);
+ ecex_draw_color_rgba8_i(ctx, 40, 40, 40, 255);
ecex_draw_rect_i(ctx, x, y - 2, w, h);
- ecex_draw_color_rgba8(ctx, 213, 196, 161, 255);
+ ecex_draw_color_rgba8_i(ctx, 213, 196, 161, 255);
ecex_draw_text(ctx, (float)(x + 10), (float)y, text);
break;
case 4: /* list */
- ecex_draw_color_rgba8(ctx, 184, 187, 38, 255);
+ ecex_draw_color_rgba8_i(ctx, 184, 187, 38, 255);
ecex_draw_rect_i(ctx, x + 6, y + line_h / 2 - 3, 6, 6);
- ecex_draw_color_rgba8(ctx, 235, 219, 178, 255);
+ ecex_draw_color_rgba8_i(ctx, 235, 219, 178, 255);
ecex_draw_text(ctx, (float)(x + 24), (float)y, text);
break;
case 5: /* fence */
- ecex_draw_color_rgba8(ctx, 80, 73, 69, 255);
+ ecex_draw_color_rgba8_i(ctx, 80, 73, 69, 255);
ecex_draw_rect_i(ctx, x, y - 3, w, line_h + 6);
- ecex_draw_color_rgba8(ctx, 142, 192, 124, 255);
+ ecex_draw_color_rgba8_i(ctx, 142, 192, 124, 255);
ecex_draw_text(ctx, (float)(x + 10), (float)y, text);
break;
case 6: /* hr */
- ecex_draw_color_rgba8(ctx, 80, 73, 69, 255);
+ ecex_draw_color_rgba8_i(ctx, 80, 73, 69, 255);
ecex_draw_line_i(ctx, x, y + line_h / 2, x + w, y + line_h / 2, 2);
break;
case 0:
default:
- ecex_draw_color_rgba8(ctx, 235, 219, 178, 255);
+ ecex_draw_color_rgba8_i(ctx, 235, 219, 178, 255);
ecex_draw_text(ctx, (float)x, (float)y, text);
break;
}
@@ -1106,7 +1426,7 @@ void ecex_draw_markdown_line_auto_i(ecex_draw_context_t *ctx, void *owner, int t
}
void ecex_draw_label_i(ecex_draw_context_t *ctx, int x, int y, int label_id) {
- ecex_draw_mini_text_i(ctx, x, y, ecex_draw_label_text(label_id));
+ ecex_draw_host_text_i(ctx, x, y, ecex_draw_label_text(label_id));
}
static void ecex_i32_to_ascii(int value, char *buf, size_t cap) {
@@ -1131,11 +1451,12 @@ static void ecex_i32_to_ascii(int value, char *buf, size_t cap) {
void ecex_draw_stat_i(ecex_draw_context_t *ctx, int x, int y, int label_id, int value) {
char num[24];
+ char text[128];
const char *prefix = ecex_draw_label_text(label_id);
if (!ctx || !prefix) return;
- ecex_draw_mini_text_i(ctx, x, y, prefix);
ecex_i32_to_ascii(value, num, sizeof(num));
- ecex_draw_mini_text_i(ctx, x + (int)strlen(prefix) * 12, y, num);
+ snprintf(text, sizeof(text), "%s%s", prefix, num);
+ ecex_draw_host_text_i(ctx, x, y, text);
}
@@ -1177,13 +1498,13 @@ static int ecex_tetris_preview_shape_cell(int piece, int rot, int col, int row)
}
static void ecex_draw_tetris_preview_color(ecex_draw_context_t *ctx, int piece, int alpha) {
- if (piece == 0) ecex_draw_color_rgba8(ctx, 51, 191, 242, alpha);
- else if (piece == 1) ecex_draw_color_rgba8(ctx, 242, 217, 51, alpha);
- else if (piece == 2) ecex_draw_color_rgba8(ctx, 179, 89, 242, alpha);
- else if (piece == 3) ecex_draw_color_rgba8(ctx, 77, 217, 89, alpha);
- else if (piece == 4) ecex_draw_color_rgba8(ctx, 242, 64, 64, alpha);
- else if (piece == 5) ecex_draw_color_rgba8(ctx, 64, 102, 242, alpha);
- else ecex_draw_color_rgba8(ctx, 242, 140, 51, alpha);
+ if (piece == 0) ecex_draw_color_rgba8_i(ctx, 51, 191, 242, alpha);
+ else if (piece == 1) ecex_draw_color_rgba8_i(ctx, 242, 217, 51, alpha);
+ else if (piece == 2) ecex_draw_color_rgba8_i(ctx, 179, 89, 242, alpha);
+ else if (piece == 3) ecex_draw_color_rgba8_i(ctx, 77, 217, 89, alpha);
+ else if (piece == 4) ecex_draw_color_rgba8_i(ctx, 242, 64, 64, alpha);
+ else if (piece == 5) ecex_draw_color_rgba8_i(ctx, 64, 102, 242, alpha);
+ else ecex_draw_color_rgba8_i(ctx, 242, 140, 51, alpha);
}
void ecex_draw_tetris_preview_i(ecex_draw_context_t *ctx, int piece, int x, int y, int cell, int alpha) {
@@ -1198,7 +1519,7 @@ void ecex_draw_tetris_preview_i(ecex_draw_context_t *ctx, int piece, int x, int
/* Clear a small preview box first so the old preview shape cannot linger
* when the new piece occupies fewer cells than the previous I piece. */
- ecex_draw_color_rgba8(ctx, 20, 23, 28, 255);
+ ecex_draw_color_rgba8_i(ctx, 20, 23, 28, 255);
ecex_draw_rect_i(ctx, x - 2, y - 2, cell * 4 + 4, cell * 4 + 4);
for (r = 0; r < 4; ++r) {
@@ -1275,15 +1596,23 @@ static void render_custom_buffer(app_t *app, buffer_t *buf, view_rect_t rect, si
const char *trace_env = getenv("ECEX_TRACE_CALLBACKS");
trace_callbacks = trace_env && trace_env[0] && trace_env[0] != '0';
if (trace_callbacks) {
- fprintf(stderr, "ecex-log: render_callback_enter buffer=%p fn=%p userdata=%p window=%zu %.1fx%.1f\n",
- (void *)buf, (void *)buf->render_fn, buf->render_userdata, index, rect.w, rect.h);
- fflush(stderr);
+ char msg[256];
+ snprintf(msg,
+ sizeof(msg),
+ "render callback start buffer=%p fn=%p userdata=%p window=%zu %.1fx%.1f",
+ (void *)buf,
+ (void *)buf->render_fn,
+ buf->render_userdata,
+ index,
+ rect.w,
+ rect.h);
+ ecex_log_group_begin(msg);
}
int result = buf->render_fn(app->ed, buf, &ctx, buf->render_userdata);
if (trace_callbacks) {
- fprintf(stderr, "ecex-log: render_callback_leave buffer=%p result=%d\n",
- (void *)buf, result);
- fflush(stderr);
+ char msg[128];
+ snprintf(msg, sizeof(msg), "render callback end buffer=%p result=%d", (void *)buf, result);
+ ecex_log_group_end(msg);
}
}
@@ -1310,13 +1639,13 @@ static void render_buffer_window(app_t *app, ecex_window_t *win, size_t index, f
glEnable(GL_SCISSOR_TEST);
glScissor(sx, sy, sw, sh);
- if (index == app->ed->current_window_index) {
- ensure_cursor_visible(app, buf, rect);
- }
-
int replace_content = buf->render_fn && (buf->render_flags & ECEX_RENDER_REPLACE_CONTENT);
if (!replace_content) {
+ if (index == app->ed->current_window_index) {
+ ensure_cursor_visible(app, buf, rect);
+ }
+
size_t rows = visible_row_count_for_rect(app, rect);
size_t pos = offset_for_line(buf, buf->scroll_line);
float line_top = rect.y + m.content_top;
@@ -1373,6 +1702,15 @@ void render(app_t *app) {
glfwGetFramebufferSize(app->window, &app->width, &app->height);
+ char frame_start[128];
+ snprintf(frame_start,
+ sizeof(frame_start),
+ "frame start size=%dx%d windows=%zu",
+ app->width,
+ app->height,
+ ecex_window_count(app->ed));
+ ecex_log_group_begin(frame_start);
+
setup_2d(app->width, app->height);
glClearColor(app->ed->theme.bg.r,
@@ -1387,4 +1725,6 @@ void render(app_t *app) {
render_windows(app);
render_minibuffer(app);
render_status_bar(app);
+
+ ecex_log_group_end("frame end");
}
diff --git a/tests/test_core.c b/tests/test_core.c
new file mode 100644
index 0000000..42dfd99
--- /dev/null
+++ b/tests/test_core.c
@@ -0,0 +1,88 @@
+#include "ecex.h"
+
+#include <assert.h>
+#include <stddef.h>
+#include <string.h>
+
+static void test_plugin_identity(void) {
+ ecex_t ed;
+ memset(&ed, 0, sizeof(ed));
+ ed.plugins = ecex_plugin_runtime_new();
+ assert(ed.plugins);
+
+ ecex_plugin_t *a = ecex_plugin_register(&ed, "alpha", ECEX_PLUGIN_API_VERSION);
+ assert(a);
+ assert(ecex_plugin_find(&ed, "alpha") == a);
+ assert(ecex_plugin_register(&ed, "alpha", ECEX_PLUGIN_API_VERSION) == 0);
+ assert(ecex_plugin_require(&ed, "alpha", ECEX_PLUGIN_API_VERSION) == a);
+ assert(ecex_plugin_register(&ed, "Bad ID", ECEX_PLUGIN_API_VERSION) == 0);
+ assert(ecex_plugin_register(&ed, "beta", ECEX_PLUGIN_API_VERSION + 1) == 0);
+
+ ecex_plugin_runtime_free(ed.plugins);
+}
+
+static void test_plugin_slots_and_exports(void) {
+ ecex_t ed;
+ memset(&ed, 0, sizeof(ed));
+ ed.plugins = ecex_plugin_runtime_new();
+ assert(ed.plugins);
+
+ ecex_plugin_t *owner = ecex_plugin_register(&ed, "owner", ECEX_PLUGIN_API_VERSION);
+ ecex_plugin_t *reader = ecex_plugin_register(&ed, "reader", ECEX_PLUGIN_API_VERSION);
+ assert(owner);
+ assert(reader);
+
+ assert(ecex_plugin_slot_i32_set(owner, "score", 0, 42) == 0);
+ assert(ecex_plugin_slot_i32_get_scalar(owner, "score", -1) == 42);
+ assert(ecex_plugin_slot_i32_set_2d(owner, "board", 10, 3, 4, 99) == 0);
+ assert(ecex_plugin_slot_i32_get_2d(owner, "board", 10, 3, 4, -1) == 99);
+ assert(ecex_plugin_slot_i32_get_2d(owner, "board", 0, 3, 4, -7) == -7);
+
+ int copied = 0;
+ size_t copied_len = 0;
+ assert(ecex_plugin_slot_read_exported(&ed, "owner", "score", &copied, sizeof(copied), &copied_len) != 0);
+ assert(ecex_plugin_slot_set_export_flags(owner, "score", ECEX_PLUGIN_I32, ECEX_PLUGIN_EXPORT_READ) == 0);
+ assert(ecex_plugin_slot_read_exported(&ed, "owner", "score", &copied, sizeof(copied), &copied_len) == 0);
+ assert(copied == 42);
+ assert(copied_len == sizeof(copied));
+
+ copied = 7;
+ assert(ecex_plugin_slot_read_exported(&ed, "owner", "score", &copied, 0, &copied_len) != 0);
+ assert(copied == 7);
+ assert(copied_len == sizeof(copied));
+
+ ecex_plugin_runtime_free(ed.plugins);
+}
+
+static void test_plugin_objects_and_text(void) {
+ ecex_t ed;
+ memset(&ed, 0, sizeof(ed));
+ ed.plugins = ecex_plugin_runtime_new();
+ assert(ed.plugins);
+
+ ecex_plugin_t *plugin = ecex_plugin_register(&ed, "objects", ECEX_PLUGIN_API_VERSION);
+ assert(plugin);
+
+ unsigned char *object = ecex_plugin_object_calloc(plugin, "state", 1, 16);
+ assert(object);
+ assert(ecex_plugin_object_i32_set(plugin, object, 4, 1234) == 0);
+ assert(ecex_plugin_object_i32_get(plugin, object, 4, -1) == 1234);
+ assert(ecex_plugin_object_i32_set(plugin, object, 14, 1) != 0);
+ assert(ecex_plugin_object_valid(plugin, object) == 1);
+ assert(ecex_plugin_object_free(plugin, object) == 0);
+ assert(ecex_plugin_object_valid(plugin, object) == 0);
+
+ assert(ecex_plugin_text_set(plugin, 1, "hello", -1) == 0);
+ assert(strcmp(ecex_plugin_text_get_drawable(&ed, plugin, 1), "hello") == 0);
+ assert(ecex_plugin_text_free_all(plugin) == 0);
+ assert(strcmp(ecex_plugin_text_get_drawable(&ed, plugin, 1), "") == 0);
+
+ ecex_plugin_runtime_free(ed.plugins);
+}
+
+int main(void) {
+ test_plugin_identity();
+ test_plugin_slots_and_exports();
+ test_plugin_objects_and_text();
+ return 0;
+}