You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/PLUGINS.md
+41-11Lines changed: 41 additions & 11 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -47,7 +47,7 @@ the first time; use it as reference after that.
47
47
|`Plugin`| A plugin's identity + **vtable** of optional hooks. The shell calls these; a plugin implements only what it needs. |
48
48
|`DocHandle`| Opaque handle to an open document: `{ ptr, id, owner: *Plugin }`. The shell stores these per tab and **routes every document operation to `owner`** — it never inspects `ptr`. |
49
49
|`regions`| The contribution structs a plugin registers: `SidebarView`, `BottomView`, `CenterProvider`, `MenuContribution`, `Command`, `LanguageSupport`, … There is no settings region here — see `sdk.settings` below. |
50
-
|`sdk.settings` (`settings.zig`) | Comptime settings API: `sdk.settings.Schema(struct { … })` derives a persisted-values type + a `SettingsSchema` you register with the Host. The shell's settings pane draws it for you — no hand-rolled dvui settings section. |
50
+
|`sdk.settings` (`settings.zig`) | Comptime settings API: `sdk.settings.Schema(struct { … })`over `Value(T, .{ .description = … })` cells derives a persisted-values type + a `SettingsSchema` you register with the Host. The shell's settings pane draws it for you — no hand-rolled dvui settings section. |
51
51
|`dylib` / `dvui_context`| The C-ABI entry contract + dvui-context injection used when a plugin is loaded as a runtime library. |
52
52
53
53
**The shell owns no features.** Each frame it iterates the Host registries and draws whatever
@@ -327,15 +327,22 @@ fallback when a loaded plugin has no fetchable `ICON.png` (e.g. a sideloaded dyl
327
327
328
328
There is no `registerSettingsSection` and no author-written settings schema in
329
329
`plugin.zig.zon`. Persisted settings are declared as a plain Zig struct (build-options-style,
330
-
comptime-derived), then registered from `register(host)`:
330
+
comptime-derived) whose every field is a self-describing `sdk.settings.Value` cell, then
331
+
registered from `register(host)`:
331
332
332
333
```zig
333
334
const MySettings = sdk.settings.Schema(struct {
334
-
insert_spaces_on_tab: bool = true,
335
-
tab_size: enum(u8) { two = 2, four = 4, eight = 8 } = .four,
336
-
format_on_save: bool = false,
335
+
insert_spaces_on_tab: sdk.settings.Value(bool, .{
336
+
.description = "Insert spaces instead of a tab character when pressing Tab.",
337
+
}) = .init(true),
338
+
tab_size: sdk.settings.Value(enum(u8) { two = 2, four = 4, eight = 8 }, .{
339
+
.description = "How many columns a tab occupies.",
340
+
}) = .init(.four),
341
+
format_on_save: sdk.settings.Value(bool, .{
342
+
.description = "Reformat the document each time it is saved.",
Copy file name to clipboardExpand all lines: docs/PLUGIN_MANIFEST_PLAN.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -36,6 +36,7 @@
36
36
| R13 — Shell fields persist non-default-only too | done | 2026-07-26 — R12's non-default-only rule applied to the *other* half of `settings.zon`: `Settings.serialize` now diffs every shell field against `const default_value: Settings = .{}` and emits only what differs (an untouched shell serializes to `.{}`, which `composeMergedText` already splices `.plugins` onto — covered by a new unit test). A field hand-written back at its default drops out on the next write, matching plugin blocks. One ownership consequence: with fields now routinely absent from disk, `std.zon.parse` fills them from the struct's declared defaults — and `theme`'s default is a comptime string literal, so the old `std.zon.parse.free(gpa, parsed)` calls would have handed a non-allocated pointer to the allocator. New `Settings.freeParsed` zeroes that field before the whole-struct free (`Allocator.free` early-returns on an empty slice) and is used at all three parse sites; `Settings`' process-lifetime `loaded` snapshot is gone with it, since `load` can now free its parse immediately after duping `theme`. No SDK/fingerprint change (shell-only). Verified: `zig build`/`test`/`check-web`, plus a live macOS run in an isolated `HOME`/`TMPDIR` sandbox — an all-defaults-spelled-out file migrated down to just `.content_opacity`, a hand-edited `.font_body_size` at its default was dropped on the reconcile-triggered rewrite while a non-default `.theme` survived, and a clean quit showed no invalid free. |
37
37
| R14 — String settings ownership rule | done | 2026-07-26 — the plugin-side half of the same hazard R13 fixed for the shell, found while doing R13 and fixed before a plugin could hit it. `Schema(T).applyZon` used to `std.zon.parse.free` the whole previous value, so **any plugin with a `[]const u8` setting whose default is non-empty would have invalid-freed a string literal on its very first `load()`** — and again on every reconcile, since R12 omits default fields from disk and `std.zon.parse` fills those from the same literals. Now one explicit rule, stated once and used everywhere: *a string field's bytes are schema-owned (host allocator) unless the slice is exactly `T`'s declared default*, tested by pointer identity (`freeOwned`/`fieldIsOwned`). Persistence comparisons stay content-based, so an allocated copy equal to the default still prunes out of settings.zon. Consequences: new `Schema(T).deinit(value)` for plugins to call on teardown (no-op for scalar-only structs, documented in `docs/PLUGINS.md` §3.1.1); `Access.setString` implemented at last (dupe + release the old value under the same rule), which retires the "read-only until setString owns allocation policy" TODO in `PluginSettingsPane` — string settings now draw an editable text entry (commit on Enter; re-seeds from the live value while unfocused, so an abandoned edit reverts and an externally reconciled change shows up without a pane-side notification). No SDK boundary change — `Access` already had the member, fingerprint/`test-sdk-version` unchanged. Verified: `zig build`/`test-all`/`check-web`/`test-sdk-version`; new `Schema` unit tests under `test-integration` (`testing.allocator` catches both the invalid free and a leak) — note these live in the *integration* layer, `zig build test` alone does not run them; plus a live macOS sandbox run with a temporary string setting added to `text`: loaded from disk at register, hand-edited twice (allocated → allocated → back to the default), no crash, block correctly pruned when the value returned to its default. |
38
38
| R13 follow-up — shutdown-write use-after-free | done | 2026-07-26 — R13's first real run panicked (`incorrect alignment` in `std.hash_map`) at quit. Pre-existing latent bug in `Editor.deinit`, surfaced by R13: `deinit` stops the watchers first (correct), but `stop()` *is* the watcher's teardown — `std.HashMapUnmanaged.deinit` leaves the maps `undefined` — and `deinit` then goes on to call `saveSettingsRaw` → `writeMergedSettings` → `notifyPathChanged` → `by_path.get` on that dead map. It only bites when the shutdown write actually happens (`writeMergedSettings` returns early on an unchanged hash), which is why it survived until R13 made "the composed text differs from what's on disk" the common case. Fix: `Editor.deinit` clears `document_watcher`/`settings_watcher` after stopping them (clearing is part of stopping, not tidiness — it also prevents `SettingsWatcher.stop`'s `config_folder` double-free), plus `DocumentWatcher.stop` now resets its maps to `.empty` instead of leaving them undefined, making a late query an empty-map miss rather than garbage. A/B verified in the macOS sandbox: same seeded config + edit-then-quit-inside-the-debounce sequence panics with the fix stashed and quits cleanly with it applied, writing the normalized file either way. |
39
+
| R15 — Self-describing setting cells + VSCode-shaped rows | done | 2026-07-28 — a settings struct field is no longer a bare `bool`/`u8`/enum but a `sdk.settings.Value(T, .{ .description = … }) = .init(default)` **cell**: payload type, default, and a **required** description, all comptime (metadata lives in the type, so `@sizeOf(Value(T, …)) == @sizeOf(T)` and only the payload is ever stored). Omitting the description is a compile error at the declaration site — the point being that the pane now has a permanent place for it, and no shell-side table of strings can drift from the plugin that owns the setting. `Setting` gained `description` and a real `label` (derived from the field name in sentence case, `insert_spaces_on_tab` → `Insert spaces on tab`, overridable via `Options.name`); `key` stays the field/zon name. **On-disk format is unchanged**: everything persistence touches goes through a new comptime `Plain(T)` mirror (built with 0.16's `@Struct`) that has the same field names but bare payload types and the cells' defaults, so `applyZon`/`diffSerialize`/`fullSerialize` still read and write `.{ .tab_size = 8 }` exactly as before — no migration, and R11 reconciliation / R12 diff-only persistence needed no changes. **Any type is now a legal setting**: `kindFor` no longer `@compileError`s on an unsupported type, it returns the new `TypeTag.other`, and `Access` gained `getZonText`/`setZonText` so the pane can draw such a value as editable zon text (a parse failure simply doesn't commit — the entry re-seeds from the live value). **UI** (`src/editor/SettingRow.zig`, new): one row drawer shared by shell and plugin settings so they cannot drift — bold name, dim mono key underneath, wrapped description, then the full-width control (VSCode's order), with booleans the one exception (plain `dvui.checkbox` *before* the description on one line, since a full-width control for two states is all frame and no information). The short-lived full-width toggle button in `core.dvui` was deleted — this replaced it. Search (`SettingsTree.scoreLeaf`) now scores the zon key alongside the label/path/keywords, and both name and key are highlight-rendered, so a row that survived on a key match can show why. Fizzy's own settings (`explorer/settings.zig`) grew required `key`/`description` fields (plus `inline_control`) and all 11 items were filled in, keeping shell and plugin rows identical. sdk **0.1.46** (fingerprint `0xf5802c523f9bbc82` — `Setting` gained a field, `Kind`/`TypeTag` gained `other`, `Access` gained two members). Verified: `zig build`, `zig build test`, `zig build test-sdk-version`, `zig build test-integration` (23 SDK tests, incl. new cases for label derivation, `Options` bound refinement, the `.other` zon round-trip, and a diff-serialize assertion that the on-disk shape has no `.v` in it — confirmed actually executing via `-Dtest-filter`), and the text plugin's standalone `cd src/plugins/text && zig build`. |
39
40
| Old Phase 2 (sidecar enforcement) |**cancelled**| superseded by this revision |
0 commit comments