aboutsummaryrefslogtreecommitdiff
path: root/docs/plugin-api.md
diff options
context:
space:
mode:
authorDavid Moc <personal@cdatgoose.org>2026-06-02 13:50:21 +0200
committerDavid Moc <personal@cdatgoose.org>2026-06-02 13:50:21 +0200
commita15cb041654ae307add0b998b526c87c3f42bf5f (patch)
tree225bb4b70e9fa05aa5f4d2722a1a9cf5fc6fca7f /docs/plugin-api.md
parent6aeaa171dc1ca43392f53cbd02097f76e1b1c5a0 (diff)
Add plugin hooks and mode plugins
Diffstat (limited to 'docs/plugin-api.md')
-rw-r--r--docs/plugin-api.md329
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.