Skip to content

Commit dac1525

Browse files
committed
Fix workbench tabs
Allow ABI-version mismatches to reject loading plugin Handle loading config area plugins excersize 3rd party plugin Update build files for sdk simplify creating plugin work on rough edges Remove pixel art specific vtable hooks make pixel art specific hooks commands instead finish removing pixel art specifics in EditorAPI begin refining sdk unify builtin and 3rd party plugins split root refine plugin structure across all fix web build fix tab bar on bottom panel make core.gpa not have to be set by plugin authors Fix sprites panel small visual fixes, refresh api refine build.zig's in plugins begin work towards plugin store chunks 1 + 2 begin chunk 3 chunk 4 chunk 5 chunk 6 chunk 7 separate pixi from fizzy entirely Phase b1 b1b begin store_text_sidebar plan Phase 0 Phase 1-2 Phase 3-4 sdk: key ABI fingerprint lock by (arch, os) The comptime self-check in src/sdk/version.zig compared every native target's live structural fingerprint against a single hardcoded recorded_abi_fingerprint. The fingerprint hashes @offsetOf/@Alignof of the plugin-boundary types, which follows each target's C ABI, so aarch64-macos, x86_64-linux, and x86_64-windows legitimately produce different (but each internally valid) fingerprints for the same boundary. The check only exempted wasm32, so every non-aarch64-macos native build failed at compile time (surfaced as CI failures building the pixi plugin on linux/windows). Replace the single constant with a small per-(arch, os) table, recorded_abi_fingerprints, and look up the entry matching the target being built. Also corrects the aarch64-macos entry itself (0x1bb54eb7506cbd78 -> 0x9f75e41ec2f5c17f): it was already stale even for that one target on the current toolchain resolution, independent of the arch issue above -- caught because plugin_sdk=true only registers modules and never actually compiles them, so it had never been exercised by a real build. Verified via genuinely compiling pixi (a real third-party plugin, not the plugin_sdk export path) for aarch64-macos, x86_64-linux-gnu, and x86_64-windows-gnu, all clean. update fingerprint scheme update dvui refine store design plugin store work phase 5 sqlite update docs Cleanup storefront/fix panel text editor fixes update text editing begin implementing language plugins add plugin icons get ICON.png from plugin repo Add new file selection dialog Fix homepage Remove example, add Image Fix bottom panel and docs per plugin Make bottom panel close when no plugin renders to it sdk: add LanguageSupport hover/gotoDefinition hooks + workbench revealPosition Adds the SDK plumbing for Ctrl/Cmd+click goto-definition and always-on hover tooltips in the text editor, ahead of a real LSP-backed provider: - LanguageSupport.VTable.hover/gotoDefinition + HoverResult/DefinitionLocation - Host.hoverFor/gotoDefinitionFor lookups - Plugin.VTable.revealPosition (external plugins moving the caret in a document they don't own) + text plugin's pending_cursor-based impl - workbench.Api.revealPosition (open-if-needed + jump to a byte offset), with Workbench's pending_reveals polling for the not-yet-open case - TextEntryWidget: addTextHover per tree-sitter token + Ctrl/Cmd-click peek during processEvents, surfaced as hovered_span/definition_click - TextEditor: renders a FloatingTooltipWidget from hoverFor and calls revealPosition on a Ctrl/Cmd+click definition hit sdk: pass path explicitly to hover/gotoDefinition instead of a threadlocal The previewDocumentPath() threadlocal only ever worked for previewPane because text/markdown happen to be statically linked into the same binary and share one copy of it. A genuinely separate plugin dylib (e.g. the third-party zig language plugin) gets its own private compiled copy of every SDK source file, so the host's write into its copy of the threadlocal was invisible from inside the plugin's copy — hover/gotoDefinition silently never fired for any real dylib plugin. Pass path as an explicit parameter instead, same as bytes. Bumps sdk_version to 0.17.0. temp: add diagnostic logging for hover/gotoDefinition debugging text: keep large hover tooltips capped + scrollable, and interactive A hover tooltip with a lot of content (a long doc comment, a big type signature) would grow to cover the very token it's describing. Once the mouse ends up over the tooltip instead of the source token, hover detection loses the span and the tooltip disappears — which un-covers the token, which re-triggers hover, which reopens the tooltip directly under the mouse again. Constant flicker. Cap the tooltip at a fixed max size and put the content in a scrollArea so it scrolls instead of growing unbounded, and mark the FloatingTooltipWidget interactive so moving the mouse onto the tooltip itself (to scroll it) doesn't count as leaving hover and close it. text: style the hover tooltip to match the app's panel/dialog language Background matches the explorer/sidebar panel color (dvui.themeGet().color(.window, .fill), lighter than the tooltip's previous default), standard 8px corner radius, no border, and the same soft drop shadow used by the sidebar's own tooltip (Sidebar.zig:154-171) instead of a hard-edged box. add markdown service Add output log attempt to add autocomplete, broken refine autocomplete Refine completion design update dvui format on save Add info for highlighted completion suggestions update dvui core: finish ReorderWidget dvui corner_radius -> corners migration BoxShadow/Options corner styling moved from a flat Rect to CornerRect (tl/tr/br/bl Corner structs); ReorderWidget's finalSlot() and drag target fill were left half-migrated (called the new cornersGet() accessor but still assigned into the old corner_radius field name). correctly handle menu items, fix formatting on windows performance pass on text editing more accurate cache boundaries Fix editor performance on large zig files turn off diagnostic logs, remove unnecessary code update dvui move generic lsp client to core
1 parent 595d80c commit dac1525

251 files changed

Lines changed: 20273 additions & 62242 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.cursor/rules/lang-plugins.mdc

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
description: Language plugin system — LanguageSupport registry, text/markdown/zig/json plugins
3+
globs:
4+
- src/sdk/**
5+
- src/plugins/text/**
6+
- src/plugins/markdown/**
7+
- docs/PLUGINS.md
8+
---
9+
10+
# Language Plugins
11+
12+
Source of truth: `docs/PLUGINS.md` (markdown lives in `plugins/markdown`, not `core`).
13+
14+
## Invariants
15+
16+
- **`text` owns all text documents** — load/save/draw/undo. Language plugins never implement document vtable hooks.
17+
- **Language plugins register `LanguageSupport` only** — optional `treeSitterHighlight` and/or `previewPane` hooks looked up by extension via `Host.treeSitterHighlightFor` / `Host.previewProviderFor`.
18+
- **Plugins never import each other** — they meet only at the SDK registry.
19+
- **`markdown` is a bundled in-tree utility plugin** — owns cmark renderer + `.md` preview; shell imports `@import("markdown")` plugin hub for store READMEs.
20+
- **`zig` / `json` are external store plugins** in `~/dev/fizzyedit/zig` and `~/dev/fizzyedit/json`.
21+
22+
## SDK change checklist
23+
24+
When touching the language boundary:
25+
26+
1. `src/sdk/language.zig` — types
27+
2. `src/sdk/Host.zig` — registry + lookup fns + `unregisterPlugin` cleanup
28+
3. `src/sdk/dylib.zig` — add types to `sdk_boundary_types`
29+
4. `src/sdk/version.zig` — bump `sdk_version` + `recorded_sdk_shape_fingerprint`
30+
5. Run `zig build test-sdk-version`
31+
32+
## Build rule
33+
34+
Plugin builds must not leak app-only deps (Velopack). After build-graph changes:
35+
36+
```sh
37+
cd src/plugins/image && rm -rf .zig-cache zig-out zig-pkg && zig build -Doptimize=ReleaseFast
38+
ls zig-pkg | grep -i velo # must be empty
39+
```

.github/workflows/ci.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,31 @@ jobs:
6565
env:
6666
ZIG_GLOBAL_CACHE_DIR: ${{ github.workspace }}/zig-global-cache
6767
run: zig build test --summary all
68+
69+
- name: Fetch wasm dependencies (with retries)
70+
shell: bash
71+
env:
72+
ZIG_GLOBAL_CACHE_DIR: ${{ github.workspace }}/zig-global-cache
73+
run: |
74+
n=0
75+
until [ "$n" -ge 5 ]; do
76+
zig build --fetch -Dtarget=wasm32-freestanding && break
77+
n=$((n+1))
78+
echo "Fetch attempt $n failed, sleeping $((n*10))s before retry..."
79+
sleep $((n*10))
80+
done
81+
if [ "$n" -ge 5 ]; then
82+
echo "All fetch attempts failed"; exit 1
83+
fi
84+
85+
- name: Compile web (wasm) build
86+
shell: bash
87+
env:
88+
ZIG_GLOBAL_CACHE_DIR: ${{ github.workspace }}/zig-global-cache
89+
run: zig build check-web
90+
91+
- name: Verify SDK version / ABI fingerprint lock
92+
shell: bash
93+
env:
94+
ZIG_GLOBAL_CACHE_DIR: ${{ github.workspace }}/zig-global-cache
95+
run: zig build test-sdk-version --summary all

CLAUDE.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Fizzy
2+
3+
Cross-platform, open-source general editor written in Zig, UI via [DVUI](https://github.com/david-vanderson/dvui). Targets native (macOS/Linux/Windows) and web (wasm32). Layout/UX is IDE-shaped (VSCode-like): sidebar rail + explorer, menubar, center tabs/splits, bottom panel, infobar.
4+
5+
**Read this file first, then go deeper via the links below — don't re-derive the architecture from scratch.**
6+
7+
## The core idea: shell + plugins
8+
9+
Fizzy the app is a near-empty **shell** (window, frame loop, menu/sidebar/panel layout, document model) that owns **no editing features**. Everything the user sees — pixel-art editing, the file explorer/tabs/splits, text editing — is contributed by **plugins** that register against a stable SDK. Plugins never import each other; they meet only at the SDK.
10+
11+
```
12+
Shell (Editor) ←── Host registries + EditorAPI ──→ Plugin (register(host) + vtable)
13+
```
14+
15+
- **`src/sdk/`** — the entire contract. `Host` (registries + service locator), `Plugin` (identity + vtable of hooks the shell calls), `DocHandle` (opaque `{ptr, id, owner}` — shell routes every doc op to `owner`, never inspects `ptr`), `EditorAPI` (shell read/util surface plugins reach back through), `regions.zig` (sidebar/bottom/center/menu/settings/command contribution structs), `dylib.zig`/`dvui_context.zig` (runtime-library C-ABI + dvui injection).
16+
- **`src/editor/`** — the shell itself: `Editor.zig` (frame loop, plugin registration/loading), `PluginLoader.zig` (dlopen), `Menu.zig`, `Sidebar.zig`, `Settings.zig`, etc.
17+
- **`src/core/`** — shared infra (Atlas/Sprite, math, gfx, fs, paths, platform detection) used by shell *and* plugins. Not plugin-owned; don't move it.
18+
- **`src/plugins/`** — bundled built-in plugins. Each is file-for-file the **same shape a third-party plugin would use**: `build.zig`, `build.zig.zon`, `root.zig` (dylib entry, copy-only), `src/plugin.zig` (the one file you actually implement: `register(host)` + `Plugin.VTable`), plus fizzy-internal glue isolated in a `static/` subfolder + a root `<name>.zig` hub. Builds standalone with `cd src/plugins/<name> && zig build`.
19+
20+
**Two link modes, one source:** built-in plugins compile **static** (linked directly, all targets incl. web) or **dynamic** (`.dylib`/`.so`/`.dll`, desktop-only, `dlopen`'d — this is how third-party plugins ship too). `FIZZY_STATIC_<NAME>=1` env var forces static for a given built-in (useful when debugging dylib loading).
21+
22+
## Currently bundled plugins (check `ls src/plugins/` — this list moves)
23+
24+
- **`workbench`** — file tree, tabs/splits, center provider; owns no documents. Exposes a `workbench-api` service other plugins use to open/close/manage files without importing workbench.
25+
- **`text`** — generic text/code editor; fallback owner for any file extension nothing else claims. (Recently renamed from `code`.)
26+
- **`image`** — read-only PNG/JPG/JPEG viewer with zoom/pan (fallback when pixi is not installed).
27+
- **`markdown`**`.md` preview utility plugin.
28+
- `shared` — build helpers used across plugins' `static/integration.zig` (not a plugin itself).
29+
30+
**Pixi (pixel-art editor) has been extracted out of this repo** into an external, third-party-style plugin ([`fizzyedit/pixi`](https://github.com/fizzyedit/pixi), `~/dev/fizzyedit/pixi`) — it ships and updates purely through the plugin store (`docs/PLUGINS.md` §6), with no special treatment in the shell. Older docs/handoffs (`HANDOFF.md`) still describe pixi as in-tree — that's historical, not current. **Trust `ls src/plugins/` and `git log` over any doc's plugin list.**
31+
32+
## Writing a plugin
33+
34+
1. Copy `src/plugins/text/` as your template (or `src/plugins/image/` for a document-owning viewer).
35+
2. Implement `src/plugin.zig`: a `Plugin` value (id, display_name, vtable), `register(host)` (wires state, calls `host.registerPlugin` + any `host.register{SidebarView,BottomView,CenterProvider,Menu,SettingsSection,Command,Service}`), and a `VTable` with only the hooks you need.
36+
3. Editor plugins (open/save/draw files) implement the document vtable cluster: `fileTypePriority`, `loadDocument`, `drawDocument`, `saveDocument`, `isDirty`, undo/redo, etc. Shell plugins (workbench-style) skip all of that and register a center provider + sidebar views instead.
37+
4. User-invoked actions (copy/paste/transform/delete, plugin-specific features) are **`Command`s**, not vtable hooks — registered by id, dispatched by the shell via `host.runCommand("<id>")` without knowing what they do. Editing verbs follow the convention `"<active_owner_id>.<action>"`.
38+
5. `zig build install` builds for the current OS and drops the plugin straight into the fizzy plugins dir (`~/Library/Application Support/fizzy/plugins/` on macOS) — no manual copying, just relaunch.
39+
6. Memory: `host.allocator` (persistent, you own frees) vs `host.arena()` (per-frame scratch, never hold past the frame). Never touch `dvui.currentWindow().gpa` directly.
40+
7. There's no ABI version negotiation — a structural **fingerprint** over every boundary type is computed at compile time on both sides; any mismatch is a hard reject at load (`fizzy_plugin_abi_fingerprint`). Fingerprint bumps are meant to be rare/deliberate (pinned dvui + zig version).
41+
42+
Full contract — from an empty `zig init`-style folder through the SDK, ABI/versioning, and publishing to the in-app store — progressively in one doc: **[`docs/PLUGINS.md`](docs/PLUGINS.md)**.
43+
44+
## Plugin store: built, not forward-looking
45+
46+
The plugin registry/install flow (author repo → release CI → `fizzyedit/plugins` registry →
47+
in-app store) is fully built and is the canonical publishing path for every third-party
48+
plugin, `pixi` included. It's documented end-to-end in `docs/PLUGINS.md` §6; the registry
49+
repo itself is [`fizzyedit/plugins`](https://github.com/fizzyedit/plugins) and the reusable
50+
release CI is [`fizzyedit/plugin-build-action`](https://github.com/fizzyedit/plugin-build-action).
51+
Don't trust older narrative docs that call this forward-looking/not-yet-built.
52+
53+
## Historical docs (not current — don't re-derive architecture from these)
54+
55+
- **`HANDOFF.md`** — historical Phase 4 handoff (compile-time modular separation, predates the
56+
pixi extraction and the `code``text` rename). Superseded by `docs/PLUGINS.md` for anything
57+
plugin-related; useful only for the older "how did we get here" narrative.
58+
59+
## Build
60+
61+
```sh
62+
zig build # native exe
63+
zig build check-web # wasm
64+
zig build test # unit/integration tests
65+
zig build test-sdk-version # CI lock: ABI fingerprint bump must bump sdk_version too
66+
```
67+
68+
Run all of these after touching the SDK boundary (`src/sdk/**`) or a plugin's vtable usage.
69+
70+
### Keep the plugin build free of app-only dependencies
71+
72+
Every plugin build (including third-party ones like pixi) compiles this repo's root `build.zig` via `b.dependency("fizzy", .{ .plugin_sdk = true })`. Because `@import` is **comptime + transitive**, anything the root `build()` can reach at comptime is pulled into *every* plugin build — even code guarded by a runtime `if (plugin_sdk) return;`. So an app-only dependency reached through a plain top-level `@import("<dep>")` (like Velopack, the host's self-installer/updater) leaks its whole graph — wrapper + prebuilt archives — into plugin builds that never use it.
73+
74+
Rule: **app-only deps must never be reachable via comptime `@import` from the root build graph.** The pattern (see Velopack):
75+
76+
- Mark the dep `.lazy = true` in `build.zig.zon`.
77+
- Never `@import("velopack_zig")` anywhere in the build graph. The helper surface is **vendored** in `build/velopack.zig` (pure `std.Build` glue) and takes the dependency as a handle.
78+
- Resolve it lazily *inside the app build only*: `const vz = b.lazyDependency("velopack_zig", .{}) orelse return;` in `build/app.zig`, then thread `vz` through `build/exe.zig` / `build/package.zig`.
79+
80+
Acceptance test after any build-graph change: cross-build the image plugin and confirm no leak —
81+
82+
```sh
83+
cd src/plugins/image && rm -rf .zig-cache zig-out zig-pkg && zig build -Doptimize=ReleaseFast
84+
ls zig-pkg | grep -i velo # must be empty
85+
```
86+
87+
CI builds plugins for all 6 host targets by cross-compiling with `-Dtarget=` (see `fizzyedit/plugin-build-action`); pure-Zig + vendored-C plugins don't need per-arch runners.
88+
89+
## When you need more than this file
90+
91+
- Full plugin contract + lifecycle/hook tables → `docs/PLUGINS.md`

0 commit comments

Comments
 (0)