diff options
Diffstat (limited to 'docs/plugin-api.md')
| -rw-r--r-- | docs/plugin-api.md | 329 |
1 files changed, 329 insertions, 0 deletions
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. |
