1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
|
# 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
`word<TAB>detail` 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=<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_CALLBACKS=1` also logs host callback entry/exit for renderer,
animation, and mouse dispatch.
|