# 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. Use `ecex_completion_provider_add_word_detail` for one word with its own label, or `ecex_completion_provider_add_entries` for newline-separated entries in `worddetail` form: ```c ecex_completion_provider_add_entries(ed, "demo-words", "malloc\tvoid *malloc(size_t size)\n" "free\tvoid free(void *ptr)\n"); ``` 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 with signature details and special-cases `ed->` field names using typed word completion entries. ```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=` for clangd/LSP diagnostics. - `C-x c k` runs `${CC:-clang} -fsyntax-only -Wall -Wextra ` 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_CALLBACKS=1` also logs host callback entry/exit for renderer, animation, and mouse dispatch.