diff options
| -rw-r--r-- | Makefile | 8 | ||||
| -rw-r--r-- | config/c_mode_plugin.c | 192 | ||||
| -rw-r--r-- | config/c_tools_plugin.c | 210 | ||||
| -rw-r--r-- | config/ecex_api_completion_plugin.c | 806 | ||||
| -rw-r--r-- | config/ecexrc.c | 17 | ||||
| -rw-r--r-- | config/markdown_plugin.c | 68 | ||||
| -rw-r--r-- | config/render_demo.c | 118 | ||||
| -rw-r--r-- | config/tetris.c | 188 | ||||
| -rw-r--r-- | config/which_key_plugin.c | 107 | ||||
| -rw-r--r-- | docs/ccdjit-improvements.md | 68 | ||||
| -rw-r--r-- | docs/plugin-api.md | 329 | ||||
| -rw-r--r-- | include/app.h | 7 | ||||
| -rw-r--r-- | include/buffers.h | 10 | ||||
| -rw-r--r-- | include/ccdjit.h | 262 | ||||
| -rw-r--r-- | include/ecex.h | 127 | ||||
| -rw-r--r-- | include/libccdjit.a | bin | 507906 -> 613206 bytes | |||
| -rwxr-xr-x | include/libccdjit.so | bin | 369144 -> 431632 bytes | |||
| -rw-r--r-- | include/plugin.h | 110 | ||||
| -rw-r--r-- | include/types.h | 138 | ||||
| -rw-r--r-- | src/app.c | 223 | ||||
| -rw-r--r-- | src/buffers.c | 129 | ||||
| -rw-r--r-- | src/config.c | 195 | ||||
| -rw-r--r-- | src/ecex.c | 2600 | ||||
| -rw-r--r-- | src/eval.c | 217 | ||||
| -rw-r--r-- | src/log.c | 345 | ||||
| -rw-r--r-- | src/main.c | 8 | ||||
| -rw-r--r-- | src/media.c | 74 | ||||
| -rw-r--r-- | src/plugin.c | 613 | ||||
| -rw-r--r-- | src/render.c | 488 | ||||
| -rw-r--r-- | tests/test_core.c | 88 |
30 files changed, 6639 insertions, 1106 deletions
@@ -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 Binary files differindex 3045696..d062f38 100644 --- a/include/libccdjit.a +++ b/include/libccdjit.a diff --git a/include/libccdjit.so b/include/libccdjit.so Binary files differindex 40456db..c3d9203 100755 --- a/include/libccdjit.so +++ b/include/libccdjit.so 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; }; @@ -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(¶m, 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, ¶m) != 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; } @@ -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); } } @@ -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); +} @@ -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; +} |
