Repository navigation
Conversation
foxnne
force-pushed
the
sdk/hide-surface
branch
2 times, most recently
from
October 18, 2026 18:54
721d8ea to
bd40954
Compare
2 of 8 tasks
foxnne
force-pushed
the
sdk/hide-surface
branch
from
October 18, 2026 19:21
bd40954 to
e131fc3
Compare
host.hideSurface(id) undoes revealSurface as the user would: the tray showing the view, or the tray a place it is in was split off, shuts; a place in no tray (Main, a place split off it, a document pane, a dockspace cell) is left; a view in a float goes home alone, the float closing only when it was its last, and the tray it lands selected in shuts, folded or not. True when the view was in sight and is not now. Asked mid-frame it is queued and applied once the shape, floats and picker have drawn. A toggle is `if (!hideSurface(id)) revealSurface(id)`. Region.tray is decided in one place (a runtime split's place is resize but no tray); Split.close takes the size to reopen to, so a tray that drew nothing last frame shuts instead of being seeded open, and Region.reopen_extent (the user's persisted extent, else the shape's default) is what it opens back to. The explorer reads the sidebar region instead of a flag of its own. Fingerprint recorded. Part of #530 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
foxnne
force-pushed
the
sdk/hide-surface
branch
from
October 18, 2026 20:01
e131fc3 to
e81c563
Compare
Brings in the SDK 0.2.30 release (#580). The one conflict was the recorded SDK shape fingerprint, re-recorded for the merged boundary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
foxnne
added a commit
that referenced
this pull request
Oct 18, 2026
On Zig 0.17: main (with #575 and the SDK 0.2.30 release, #580) is merged in, and every gate passes on the 0.17 toolchain. Part of #530 ## What changes A plugin gets a folder of its own for what it keeps between runs that is not a setting (recents, an index, a cache): ```zig pub fn pluginDataFolder(self: *Host, plugin: *const Plugin) ?[]const u8 ``` - `<config>/plugin-data/<id>`, the id in lowercase. It follows the config folder, so a `FIZZY_PROFILE` sandbox gets its own. - **Plugin ids are unique ignoring ASCII case, enforced before the disk is touched.** - `Host.pluginById` stays exact, as do `isPluginDisabled`/`Undecided`/`AutoUpdate`, because everything keyed by a loaded id needs exact matching. `registerPlugin` refuses `Graphl` beside `graphl` (`error.DuplicatePluginId`), logging both names, through the new `Host.pluginByIdIgnoringCase`. - `App.pluginCaseClash(id)` checks for a loaded plugin, or a folder already in `plugins/`, whose id is `id` in another case. It runs before anything keyed by `id` touches the disk: - the store's install/update path (`PluginStore.startDownloadUrl`, before `plugins/<id>` is created or written: on macOS and Windows that folder *is* the other plugin's), through a new optional `PluginManager.VTable.refuseCaseClash`, since on main the store talks to the app only through `PluginManager`; - `loadUserPluginById`; - the startup scan. A clash becomes a store failure card through `App.recordCaseClash`: "id 'Graphl' differs only in case from the plugin 'graphl'". - **Uninstall from a clash card never deletes the loaded plugin's folder.** Before deleting `plugins/<id>`, `uninstallPlugin` compares it with the loaded plugin's folder by what each is on disk (`App.sameFolder`, same inode), not by name. On a case-insensitive filesystem they are one folder, so only the card goes. On a case-sensitive one, only that exact-case folder is deleted. - **The startup scan doesn't let byte order choose.** Two plugin folders whose names differ only in case (possible on a case-sensitive filesystem) are one id twice: neither loads, and one card names both and asks for one to be removed. It's the simplest deterministic rule. Which build was meant can't be told from the folders alone, and the declared id would be the same either way once case is folded. A folder whose name case-clashes with a loaded plugin gets the case-clash card, as before. - **Thread safety without walking the plugin list, symmetric for any app.** Two optional `EditorAPI.VTable` hooks, both on the main thread: - `pluginRegistered(ctx, id)`, fired by `Host.registerPlugin`, where the app sets up the plugin's entry; - `pluginUnregistered(ctx, id)`, fired by `Host.unregisterPlugin` before the plugin's `deinit`. The app marks the entry leaving: it is still handed out for that `deinit`, and stops at the next frame's top (`App.releaseRetiredFolders`, which every app built on `App` already calls). An app without fizzy's `Editor` stays correct. - `pluginDataFolder` only reads the table: under the lock, then the folder is made outside it, then `registered` is checked again under the lock. Entries stay after a plugin goes, so a path a worker holds stays valid. - If registration-time setup runs out of memory, the plugin's id is remembered (`plugin_data_missed`, no allocation) and its entry is made on its first ask. The out-of-memory is logged. - One key function (`pluginDataKey`) and one wasm-aware lock pair (`lockPluginData`/`unlockPluginData`) serve both sides. - **Ids that aren't plain identifiers:** not all registered ids pass `isValidPluginId`; the store page registers as `fizzy.store`. So `registerPlugin` doesn't enforce it. Such a plugin gets null from `pluginDataFolder`, logged once, and the docs say so. - **No `mkdir` on every call.** The folder is made on the first ask and checked again only on an ask a second or more after the last (`App.plugin_data_recheck_ns`). The contract, documented: a plugin whose write into a kept path fails with `FileNotFound` asks again, a second later if it just asked. - **Failure backoff:** `failed_at` is recorded after `createDirPath` returns. Asks within 5 s of it return null (`App.plugin_data_retry_ns`), and each run of failures is logged once. Running out of memory is logged. - **`App.deinit` frees what the App owns**: the data-folder table and the disabled/undecided/auto-update id lists, moved there from `Editor.deinit`. `Editor.deinit` calls it through a `defer` at its top. - Not `plugins/<id>`, which an update replaces and an uninstall deletes; the App, Host and PLUGINS.md docs agree. Always local, whether or not the open folder is on a mount. - **Web:** as with `paletteFolder`, the path names no directory. It is the prefix under which `core.fs.read`/`readZ`/`write`/`remove` keep files in the page's storage; those four calls do the same on the desktop, and `std.Io.Dir` calls on the path fail on the web. - **Uninstall leaves it**, as it leaves the plugin's `settings.zon` block. The named seam for removing it too is `Editor.uninstallPlugin` (see Follow-ups). Graphl asked for it (#530): today it keeps its recents beside `paletteFolder()`'s parent. Boundary: `EditorAPI.VTable.pluginDataFolder(ctx, id)` and the optional `pluginRegistered(ctx, id)`/`pluginUnregistered(ctx, id)`, the `Host`/`EditorAPI` calls, and `Host.pluginByIdIgnoringCase`. Docs: `docs/PLUGINS.md` §3.3. ## SDK impact - [ ] None - [ ] Core-only or additive: reaches plugins at the next SDK release - [x] Fingerprint moved: recorded in `sdk/src/version.zig` (`0xba4664870675e13d` after merging main, which released SDK 0.2.30; whichever of this and #566 merges second records the combined value), `sdk_version` untouched, PR labelled `sdk` - [ ] SDK release: bumps `sdk_version`, lists the `sdk` PRs since the last `sdk-v*` tag ## Verified - [x] macOS: `scripts/check-fmt.sh`, `zig build`, `zig build test`, `zig build test-integration` (54/54 steps, 595/595 tests), `zig build check-web`, `zig build test-sdk-version`, `zig build check-tests-reached`, `scripts/check-examples.sh`. A new `sdk` unit test covers `registerPlugin` refusing the same id and the same id in another case. The headless whole-editor test (fresh profile) checks: - `text`'s and `markdown`'s folders are `<profile>/plugin-data/<id>`, exist, are stable across asks, and differ; - `../escape` gets null and leaves no table entry, and `Text` finds `text`'s folder; - a test-registered `CaseTest` gets `plugin-data/casetest`, and `casetest` is then refused registration; - once `CaseTest` is unregistered its folder is still handed out (for its `deinit`) until the next frame's top, then not; - `loadUserPluginById("Text")` is refused before opening anything, and its card shows failed with the case reason; - uninstalling `Text` works, `text` stays loaded, and a file in `plugins/text` survives (on macOS `plugins/Text` is that folder); - `pluginCaseClash("graphl")` finds a `plugins/Graphl` folder. Not testable on this case-insensitive Mac: two folders differing only in case (the startup-scan rule), and deleting the exact-case folder on a case-sensitive filesystem. - `dotted.id` gets null; - a deleted folder is made again by an ask once its trusted second is wound past; - a test-registered `blocked` plugin with a file in its folder's way gets null, and null again right after the way clears (backoff); with the backoff wound past, the folder is made and the failure cleared. The warnings these tests cause are muted with `std.testing.log_level`. The test's one remaining `failed command:` comes from its own warning already on `main` (a tape needing `no-such-plugin`). - [ ] Windows: - [ ] Linux: - [ ] Web: builds (`check-web`); not run in a browser ## Follow-ups - Removing the data folder on uninstall is not built. It would be an option of `Editor.uninstallPlugin`, probably ask-first in its dialog, since the folder can hold work the user would miss. - The other items in #530 (timed recents, a home surface, the `scrolls_itself` warning) are left for their own PRs. Hiding a surface is the next PR (`sdk/hide-surface`). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sixth review of hideSurface. Mid-frame, revealSurface queues with hideSurface and both apply in the order asked; hideLater answers as hide would, a float view landing in sight where nothing shuts included. Split.close shuts an unsized region only when given a fallback (trays), and nothing is retained past a region's life: a declared but undrawn tray reads its size keys each frame. forget_when_empty places are no trays, initTree shares init's default extent, a view in a closing float stays revealable, and the explorer's menu title and rail tap use inSight like Toggle Explorer. Part of #530 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in the plugin data folder (#560). The one conflict was the recorded SDK shape fingerprint, re-recorded for the combined boundary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collaborator
Author
|
Closing in favour of a narrowed rebuild from current main: hideSurface acts only on a tray's shown view or a float's only view, deferred mid-frame, and leaves the explorer state, Split.close and revealSurface unchanged. This PR grew into reworking those, and kept bringing regressions. The new PR will link here. Its branch is kept for reference. |
2 of 8 tasks
Collaborator
Author
|
Superseded by #593: narrowed to trays and lone floats. |
foxnne
added a commit
that referenced
this pull request
Oct 18, 2026
Part of #530 Supersedes #566, narrowed to trays and lone floats. #566 grew into reworking the explorer's state, `Split.close` and a mid-frame reveal, and kept regressing; this keeps to the one primitive and leaves everything around it as it is on `main`. ## What changes A plugin can put its view away, not only show it, so a button can toggle it: ```zig pub fn hideSurface(self: *Host, id: []const u8) bool if (!host.hideSurface(id)) _ = host.revealSurface(id); // a toggle ``` It acts in two cases only, each the way the user's own gesture does: - **The view is what a tray shows.** A tray is a place the shape declared `resize` and not `forget_when_empty` (so not a place a split made), in the main window. If it is open, not folded, and `id` is its shown view, it shuts with whatever else it holds, through `Region.close`, as its rail icon or close does. `revealSurface` opens it again at the size the user left it. - **The view is alone in a float.** The float closes through `Layout.closeFloat`, as its close button does, and the view goes home. Anything else returns false and changes nothing: Main, a split's half (of a tray too), a plugin's pane, a view behind another tab, a view out of sight, a float with other views. How: - `Layout.hideTarget(id)` is the one predicate. It finds the region showing `id` the way `reveal` does, and says what a hide would do (`Hide.tray` / `Hide.float`) or null. `Layout.hide` applies it at once (between frames). - **During a frame** (a button in a view's draw), the shape and the floats are walking the lists a hide changes. `Layout.hideLater` answers with the same `hideTarget` and queues the id; `Layout.applyHides` does the queued hides once the shape, floats, drag overlay and picker have drawn, each asking `hideTarget` again. `revealSurface` is unchanged (immediate), so a hide asked first in a frame lands after a reveal asked later in it: a toggling caller makes one call a frame. Documented. - The registry entry of a declared place now carries its `resize` (one line in `Region.init`'s registration), which is how `hideTarget` knows a tray. - `Editor` wires the vtable member. A hide that shut the sidebar then calls the explorer's existing `Explorer.close`, so its `closed` flag (Toggle Explorer, its menu title) agrees; nothing in `Explorer` changes. - **A test fix this needs.** `markdown preview: skipping off-screen blocks lays the document out identically` lays out `docs/PLUGINS.md` itself and compared two f32 totals near 41000 pt to an absolute 0.01, where one rounding step is 0.004. The section added here tipped it (41430.58 vs 41430.59, locally and on CI's Linux run); it now compares relatively, at 1e-6. - The `extents_changed` check in fizzy's frame moved below the picker and the applied hides, so a change either makes is saved too. Contract: `docs/PLUGINS.md` §3.1.2, "Showing and putting away a view". ## SDK impact - [ ] None - [ ] Core-only or additive: reaches plugins at the next SDK release - [x] Fingerprint moved: recorded in `sdk/src/version.zig` (`0x1dd3a67d781503a7`, combined with main's after #587), `sdk_version` untouched, PR labelled `sdk`. New: `EditorAPI.VTable.hideSurface`, `EditorAPI.hideSurface`, `Host.hideSurface`. - [ ] SDK release: bumps `sdk_version`, lists the `sdk` PRs since the last `sdk-v*` tag ## Verified - [x] macOS, on the merge with main: all gates pass (`scripts/check-fmt.sh`, `zig build`, `test`, `test-integration`, `check-web`, `test-sdk-version`, `check-tests-reached`, `scripts/check-examples.sh`). New integration tests on the `ManyPanelCase` harness (whose frame now applies hides at its end, as fizzy's does): - a tray's view shuts the tray; a second hide and an unknown id are false; reveal reopens it; hide again shuts it (the toggle); - a view in Main, and in a place split off Main, is false and nothing changes; - a view in a tray the user split is false and nothing changes; - a view alone in a float closes the float and goes home to the panel; - a view in a float with another is false for both views, and the float stays; - a floating view asking from its own draw: the answer is true, the float is still open while it draws, and it is closing by the frame's end, then gone. - [ ] Windows: - [ ] Linux: - [ ] Web: builds (`check-web`) ## Follow-ups Deliberately left out, each a case `hideSurface` returns false for today: - views in a split tree of a tray (a panel the user split): which half shuts, and whether the tray does; - views in a float with other views (send one home, keep the float); - folded trays (a sidebar folded away on a narrow window), which count as out of sight; - `revealSurface` reopening the sidebar leaves `Explorer.closed` as it was (as on `main`); worth one source of truth for the explorer's state in its own PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
On Zig 0.17: main (with #575, the SDK 0.2.30 release #580, and #560) is merged in, and every gate passes on the 0.17 toolchain.
Part of fizzyedit/work#33
What changes
A plugin can put its view away, not only show it, so a button can toggle a panel:
hideSurfaceundoesrevealSurfacethe way the user would. It returns true when the view was in sight and is not now. It returns false, changing nothing, when the view was not in sight: another view is its place's selection, its place or the tray round it is shut or folded, or no place shows it.Region.tray: the shape's sidebar or bottom panel), or in a place the user split off one, the tray shuts with whatever else it holds, as its rail icon would shut it.revealSurfaceopens it again at the size the user left it.float_rules.goHome). The float's other views stay where the user put them, and the float closes only if this was its last view.Layout changes it needs
hideSurfacewhile the shape and the floats walk the very lists a hide changes. Mid-frame,hideSurfaceandrevealSurfaceboth queue (Layout.hideLater,revealLater, one queue), and the frame applies them in the order asked once the shape, floats, drag overlay and picker have drawn (Layout.applyLater). A hide then a reveal in one frame ends with the view shown.hideLateralso predicts where the view lands (ViewDrag.floatHomeFor): landing in sight where nothing shuts gives false, ashidedoes. Tested.markWindowRatiosDirtynow readsextents_changedafter all of this, which also closes the old gap where the picker's late changes were missed.ViewDrag.floatViewHomeiswithdrawplusgoHome, wheregoHomeissendHome's per-view step pulled out. It only goes ahead ifcanFloatViewHomeholds: the float is there and not closing, it holds the view, it can let go of it (withdraw'sclosablerule), and somewhere in the main window will show it (homeLanding,float_rules.goHomeasked before anything moves).revealstill finds it. Tested on the endless shape.locate(.open), used only byhide), butrevealstill finds a view not home yet.Sidebar.pickAction) all useinSight, so label and action agree.Explorer.isShut(editor): the user or a plugin shut the sidebar region; folding doesn't count, and no sidebar means not shut. It is what the menu title, theexplorer_focusedkeybind context (true when there's no sidebar, as before) and demo snapshots and their fingerprint read, so none of them change with the window's width.Explorer.inSight(editor): neither shut nor folded. Only Toggle Explorer uses it, so a folded explorer opens in one press.hideandrevealcount folded as out of sight the same way.Explorer.closedis gone.Split.close(id, fallback: ?f32), the root fix, for trays only. A tray that keeps no size (left empty and folded away) is shut as though it had been open atfallback, with nothing sliding. Without a fallback (anything that isn't a tray), an unsized region is left as it was, as before.Region.closepasses a fallback only for trays.hide_when_emptypath) reads its_size/_open/_showneach frame, so dvui keeps them exactly as long as the place is declared.Region.close(rail, Toggle Explorer, hide), the picker, andViewDrag.Region.reopen_extentis filled inState.registerRegion: the user's persisted extent, elsedefault_extent. The tray's declared default, padding and border included, is worked out once inRegion.init, before thehide_when_emptyearly return, andinitTreeuses the same value.Region.trayis decided in one place: Region'sregister, asresize and !forget_when_empty and !State.isMinted(name). A place that forgets itself when empty couldn't be reopened once forgotten. A place a runtime split made is no tray, so hiding never collapses a user's split pane, and no extra option is needed.fillInBox(regionIn, dockspace cells) registers no tray, because the cell and its size belong to the dockspace. A test checks hide there does nothing.Layout.locate: one pass finds the place; its tray is the place itself or, by name, the place it was split from (SplitTree.Forest.rootOf→ViewDrag.regionNamed).revealopens that tray too.docs/PLUGINS.md§3.1.2, "Showing and putting away", is the authoritative description.Host.hideSurfaceandLayout.hidepoint to it.Editor.onLayoutruns reveal or hide on last frame's regions and forwardsextents_changed.Boundary:
EditorAPI.VTable.hideSurface(ctx, id)plus theHost/EditorAPIcalls. Docs:docs/PLUGINS.md§3.1.2, "Showing and putting away". Corner-button code (#555) is untouched, butRegion.ziggains thetray/split_partlines, so a rebase may touch them.SDK impact
sdk/src/version.zig(0x5cf2c503df2d415d: the combined value, with main and sdk: a plugin has a data folder of its own #560 merged in),sdk_versionuntouched, PR labelledsdksdk_version, lists thesdkPRs since the lastsdk-v*tagVerified
macOS: all gates (
scripts/check-fmt.sh,zig build,test,test-integration,check-web,test-sdk-version,check-tests-reached,scripts/check-examples.sh). Tests on the layout harnesses (ManyPanelCase,SplitCase), through shared helpers (hideView,revealView,isShutoverViewDrag.regionNamed):regionIn, endless shape) does not shut, and nothing changes;hideLaterandhideboth answer false.In the whole-editor headless test:
Host.hideSurfaceon Output in the real bottom panel, and hiding the sidebar's view followed by a single Toggle Explorer press that opens it.Windows:
Linux:
Web: builds (
check-web)Follow-ups
Workspaces.layoutOfbuilds a between-framesLayouttoo, and also dropsextents_changed. Left as is here.State.dock) are not covered bytrayOf, which reads the split forest.devtools' "Show Profiler" could become a toggle with this.scrolls_itselfwarning.🤖 Generated with Claude Code