clean up
This commit is contained in:
+158
-60
@@ -9,15 +9,28 @@ The facts below were checked against `gpui-base 0.6.1` (crates.io), the `gpui-ki
|
||||
repository at `main`, and this workspace's `Cargo.lock` (zed at `4b47ceb`,
|
||||
2026-09-17). Line counts come from `wc -l` under `crates/ui/src`.
|
||||
|
||||
## Status
|
||||
|
||||
- **Phase 0: landed.** Manifest only; no Rust changed. The API drift across the
|
||||
three days between the snapshot and the old pin turned out to be purely
|
||||
additive, so nothing had to be fixed.
|
||||
- **Phase 1: landed.** Base is wired in, `sync_base` is in place, and 1,945 lines
|
||||
of dead weight are gone. `history.rs` moved to phase 2 once it turned out its
|
||||
only consumer is `input/state.rs`. No dependency became unused, so the pruning
|
||||
step is a no-op (four dependencies were already unused before this work).
|
||||
- **Phases 2-5: not started.**
|
||||
- One pre-existing, unrelated breakage was found; see
|
||||
[A pre-existing wasm blocker](#a-pre-existing-wasm-blocker).
|
||||
|
||||
## The two facts that shape the work
|
||||
|
||||
**GPUI still comes from upstream — addressed as the `gpui-pre` package.** `gpui-base`
|
||||
declares its GPUI dependency as `gpui = { package = "gpui-pre", version = "0.3.1" }`:
|
||||
the crate in the graph is the published package `gpui-pre`, and `gpui` is only the name
|
||||
used in code. That package is upstream zed's gpui (a snapshot of `zed@d89e9c2`)
|
||||
republished unchanged, so nothing is forked and there is no source to align. Coop
|
||||
currently pins zed's git repository at `4b47ceb` (2026-09-17), roughly four days ahead
|
||||
of that snapshot.
|
||||
used in code. That package is upstream zed's gpui (a snapshot of `zed@d89e9c2`,
|
||||
published 2026-09-14) republished unchanged, so nothing is forked and there is no
|
||||
source to align. Coop previously pinned zed's git repository at `4b47ceb`
|
||||
(2026-09-17), a few days ahead of that snapshot.
|
||||
|
||||
The two cannot be mixed. Zed's git `gpui` and the `gpui-pre` package are different
|
||||
crates, so `App`, `Window`, `Entity`, and elements from one are not the other's types,
|
||||
@@ -39,9 +52,10 @@ deeper than `ui::<module>::<Item>`:
|
||||
| `notification`, `avatar`, `menu`, `scroll`, `group_box`, `indicator`, `switch`, `modal`, `tooltip` | 12 | 16 |
|
||||
| `list`, `checkbox`, `popover`, `resizable`, `skeleton`, `tab`, `divider` (module), `history`, `animation`, `actions` | 0 references | 0 |
|
||||
|
||||
`ui::list` and `ui::checkbox` have no consumers at all; the message list in
|
||||
`crates/chat_ui` uses GPUI's own `list::ListState`. The modules with zero external
|
||||
references still serve as internal machinery for `dock`, `menu`, `modal`, and `input`.
|
||||
`ui::list` and `ui::checkbox` had no consumers at all — the message list in
|
||||
`crates/chat_ui` uses GPUI's own `list::ListState` — so phase 1 deleted both. The other
|
||||
modules with zero external references still serve as internal machinery for `dock`,
|
||||
`menu`, `modal`, and `input`.
|
||||
|
||||
The consequence: this is not a rewrite of an app-facing library. Most of the work is
|
||||
deleting internals and re-expressing a few thousand lines of presentation over base
|
||||
@@ -59,15 +73,36 @@ primitives.
|
||||
- `gpui-component` is not adopted. It is a complete, styled visual language, and taking
|
||||
it would replace the design system rather than preserve it.
|
||||
|
||||
Two `Theme` types will exist — `theme::Theme` and `gpui_base::Theme` — as separate GPUI
|
||||
Two `Theme` types exist — `theme::Theme` and `gpui_base::Theme` — as separate GPUI
|
||||
globals. Coop's stays the application-facing one. Base's is touched in exactly one
|
||||
place: a `theme::sync_base(cx)` that projects coop's colors into
|
||||
`gpui_base::Theme::global_mut(cx).tokens` (`SemanticThemeTokens`: colors, radius,
|
||||
typography, shadow) plus `ThemeAppearance`, `ScrollbarTheme`, and `ResizableTheme`. It
|
||||
runs from `ui::init` and on every theme change. This is needed because base paints a
|
||||
few things itself — the focus ring from `FocusableExt`, text selection under glyphs,
|
||||
scrollbars, resize handles, and the dialog backdrop — and those should follow coop's
|
||||
palette rather than base's default.
|
||||
place: `theme::sync_base(cx)`, called from `ui::init` and from `Theme::change` so that
|
||||
it re-runs on every theme change. It is a no-op before coop's theme global exists,
|
||||
which is the case when `ui::init` runs ahead of `theme::init`; `Theme::change` is the
|
||||
hook that actually keeps the projection current.
|
||||
|
||||
It projects the color roles base can act on — the focus ring, the wash under selected
|
||||
text, scrollbars, and overlay backdrops — and nothing else:
|
||||
|
||||
| `gpui_base::ColorTokens` | coop `ThemeColors` |
|
||||
| --- | --- |
|
||||
| `background` / `foreground` | `background` / `text` |
|
||||
| `surface` / `surface_foreground` | `surface_background` / `text` |
|
||||
| `primary` / `primary_foreground` | `element_background` / `element_foreground` |
|
||||
| `secondary` / `secondary_foreground` | `secondary_background` / `secondary_foreground` |
|
||||
| `muted` / `muted_foreground` | `ghost_element_background_alt` / `text_muted` |
|
||||
| `accent` / `accent_foreground` | `ghost_element_hover` / `text` |
|
||||
| `destructive` / `destructive_foreground` | `danger_background` / `danger_foreground` |
|
||||
| `border` / `input` | `border` |
|
||||
| `ring` | `ring` |
|
||||
| `selection` | `selection` |
|
||||
|
||||
It also sets `ThemeAppearance` from coop's mode, `ScrollbarTheme`'s mode from coop's
|
||||
`scrollbar_mode`, and `TypographyTokens::sans` from coop's `font_family`. Radius,
|
||||
spacing, typography sizes, shadows, and scrollbar geometry keep their base defaults:
|
||||
coop has a single `radius`/`radius_lg`/`font_size` where base has six-point scales, so
|
||||
any mapping would be invented rather than derived. `ResizableTheme` needs nothing —
|
||||
base's documented `None` fallback already resolves to `border` at rest and `ring` while
|
||||
dragging, both of which are projected.
|
||||
|
||||
## What each module becomes
|
||||
|
||||
@@ -85,8 +120,9 @@ palette rather than base's default.
|
||||
| `button.rs` | 626 | Skin: base behavior plus coop's existing variant tables | `Button`, `StateStyle` |
|
||||
| `switch.rs` | 287 | Skin | `Switch`, `SwitchTrack`, `SwitchThumb` |
|
||||
| `avatar.rs` | 141 | Skin | `Avatar`, `AvatarImage`, `AvatarFallback` |
|
||||
| `history.rs`, `index_path.rs`, `element_ext.rs`, `event.rs`, `focusable.rs` | 340 | Delete | `History`/`UndoHistory`, `IndexPath`, `ElementExt`, `InteractiveElementExt`, `FocusableExt`, `FocusTrapElement` |
|
||||
| `styled.rs`, `actions.rs`, `animation.rs` | 305 | Keep `ui::StyledExt`, `Size`, and `Sizable` as the app's import; base's `h_flex`/`v_flex` helpers are identical (`flex_row` + `items_center`) and can be delegated to | `styled`, `StateStyle` |
|
||||
| `history.rs` | 184 | Defer to phase 2 | `UndoHistory`, not `History`: base's `History` is navigation (back/forward), while `UndoHistory` is the grouped undo/redo with `max_undos`, `group_interval`, `start_grouping`/`end_grouping`, and `set_ignoring` in place of the fork's `pub(crate) ignore` field. Its only consumer is `input/state.rs`, which phase 2 replaces |
|
||||
| `index_path.rs`, `element_ext.rs`, `event.rs`, `focusable.rs` | 156 | Delete | `IndexPath`, `ElementExt`, `InteractiveElementExt`. `FocusableCycle` has no counterpart — base's `FocusableExt` is a different concept (whether a component draws a focus ring) — so it is dropped rather than re-based |
|
||||
| `styled.rs`, `actions.rs`, `animation.rs` | 305 | Keep `ui::StyledExt`, `Size`, and `Sizable` as the app's import. `Selectable`, `Disableable`, and `Collapsible` now come from `gpui_base::component_traits`; the local three-line `h_flex`/`v_flex` wrappers stay rather than delegating to base's identical ones | `styled`, `StateStyle` |
|
||||
| `icon.rs`, `kbd.rs`, `divider.rs`, `skeleton.rs`, `group_box.rs`, `indicator.rs` | 1,023 | Keep; no base equivalent, these are the design system | — |
|
||||
| `menu/` | 2,208 | Keep; base has no menu. Optional later: re-base anchoring and dismissal on `Popup`/`Positioner` | `Popup` (optional) |
|
||||
| `dock/` + `tab/` | 3,356 | Keep for now; see phase 5 | base dock (different contract) |
|
||||
@@ -100,58 +136,99 @@ The workspace manifest's GPUI entries become:
|
||||
|
||||
```toml
|
||||
[workspace.dependencies]
|
||||
gpui = { package = "gpui-pre", version = "0.3.5" }
|
||||
gpui_platform = { package = "gpui-pre-platform", version = "0.3.5", features = ["font-kit", "x11", "wayland"] }
|
||||
gpui_linux = { package = "gpui-pre-linux", version = "0.3.5" }
|
||||
gpui_windows = { package = "gpui-pre-windows", version = "0.3.5" }
|
||||
gpui_macos = { package = "gpui-pre-macos", version = "0.3.5" }
|
||||
gpui_web = { package = "gpui-pre-web", version = "0.3.5" }
|
||||
reqwest_client = { package = "gpui-pre-reqwest-client", version = "0.3.5" }
|
||||
sum_tree = { package = "gpui-pre-sum-tree", version = "0.3.5" }
|
||||
gpui = { package = "gpui-pre", version = "0.3.5" }
|
||||
gpui_platform = { package = "gpui-pre-platform", version = "0.3.5", features = ["font-kit", "x11", "wayland"] }
|
||||
gpui_linux = { package = "gpui-pre-linux", version = "0.3.5" }
|
||||
gpui_windows = { package = "gpui-pre-windows", version = "0.3.5" }
|
||||
gpui_macos = { package = "gpui-pre-macos", version = "0.3.5" }
|
||||
gpui_web = { package = "gpui-pre-web", version = "0.3.5" }
|
||||
gpui_util = { package = "gpui-pre-util", version = "0.3.5" }
|
||||
reqwest_client = { package = "gpui-pre-reqwest-client", version = "0.3.5" }
|
||||
sum_tree = { package = "gpui-pre-sum-tree", version = "0.3.5" }
|
||||
gpui_tokio = { path = "crates/gpui_tokio" }
|
||||
gpui-base = "0.6.1"
|
||||
```
|
||||
|
||||
Because of the `package =` alias, `use gpui::…` and `use gpui_platform::…` keep
|
||||
compiling unchanged. `gpui_web` moves from `web/Cargo.toml` into the workspace table
|
||||
with the rest.
|
||||
compiling unchanged. The aliases match the ones `gpui-pre` uses internally, and
|
||||
`gpui_web` moved out of `web/Cargo.toml` into this table with the rest.
|
||||
|
||||
The only alternative — leaving the workspace on zed's git `gpui` and redirecting
|
||||
`gpui-base`'s dependency to it — means vendoring `gpui-base` and owning its source.
|
||||
That is a fork, and this plan deliberately avoids it.
|
||||
|
||||
`gpui_tokio` is the one missing piece: longbridge does not republish it, and
|
||||
`crates/state` uses it in three places (`init`, `spawn`, `spawn_result`). Either vendor
|
||||
zed's small crate into the workspace, or drop it for `cx.background_spawn`. Decide in
|
||||
phase 0.
|
||||
`gpui_tokio` is the one crate in the family longbridge does not republish. `crates/state`
|
||||
uses it to run `browser-signer-proxy` and `nostr-blossom` work, and the nostr client's
|
||||
reqwest backend needs a Tokio reactor, so the runtime cannot be dropped for
|
||||
`cx.background_spawn`. It is vendored verbatim from zed at `4b47ceb` into
|
||||
`crates/gpui_tokio` (Apache-2.0, ~100 lines), which is the smallest change that keeps
|
||||
the existing behaviour.
|
||||
|
||||
`gpui-base` and `gpui-pre` move together on minor versions (`0.6.x` requires `0.3.x`);
|
||||
bump both in the same change.
|
||||
|
||||
## Phases
|
||||
|
||||
### Phase 0 — move `gpui` onto the `gpui-pre` package (manifest only)
|
||||
### Phase 0 — move `gpui` onto the `gpui-pre` package (manifest only) — landed
|
||||
|
||||
Point the workspace's GPUI entries at the published `gpui-pre` crates and fix whatever
|
||||
the four days of API drift between `4b47ceb` and `zed@d89e9c2` broke. There is no GPUI
|
||||
source to align, patch, or vendor. Confirm that the entry points coop calls still exist
|
||||
in 0.3.5: `gpui_platform::application()`, `web_init()`, and `single_threaded_web()`.
|
||||
Point the workspace's GPUI entries at the published `gpui-pre` crates. There is no GPUI
|
||||
source to align, patch, or vendor.
|
||||
|
||||
Exit criteria: `cargo check` passes for `desktop` and for
|
||||
`cargo check -p coop_web --target wasm32-unknown-unknown`, and the drift fixes are
|
||||
listed in the pull request. The change rewrites the dependency graph, so it stays in a
|
||||
pull request of its own.
|
||||
**No drift had to be fixed.** The gap between the snapshot (`zed@d89e9c2`) and the old
|
||||
pin (`4b47ceb`) is 67 commits, but only 16 touch the GPUI crates, and the public surface
|
||||
only gained names: `ShapedLineCursor`, `MissingGlyphSink`, `MissingGlyph`,
|
||||
`FallbackFontClass`, `MEASUREMENT_VERSION`, dynamic font installation, and inspector
|
||||
registration. Nothing coop used was removed or changed shape, so every `use gpui::…`
|
||||
compiled unchanged. The three entry points coop calls —
|
||||
`gpui_platform::application()`, `gpui_platform::web_init()`, and
|
||||
`gpui_platform::single_threaded_web()` — all exist in 0.3.5.
|
||||
|
||||
### Phase 1 — Wire base, delete dead weight (no visual change)
|
||||
Exit criteria: `cargo check` passes for `desktop`, and the wasm criterion is blocked by
|
||||
a pre-existing bug unrelated to GPUI — see
|
||||
[A pre-existing wasm blocker](#a-pre-existing-wasm-blocker). `cargo check -p theme -p ui
|
||||
--target wasm32-unknown-unknown`, which covers everything this migration touches,
|
||||
passes. The change rewrites the dependency graph, so it stays in a pull request of its
|
||||
own.
|
||||
|
||||
Add `gpui-base`, make `ui::init` call `gpui_base::init(cx)` followed by
|
||||
`theme::sync_base(cx)`, and re-export the base utilities the app already imports under
|
||||
their current names (`ElementExt`, `InteractiveElementExt`, `IndexPath`, `History`,
|
||||
`Disableable`, `Selectable`). Delete `checkbox.rs` and `list/`, which have no
|
||||
consumers, along with `history.rs`, `index_path.rs`, `element_ext.rs`, and `event.rs`
|
||||
once base supplies them. Drop the dependencies this leaves unused.
|
||||
### Phase 1 — Wire base, delete dead weight (no visual change) — landed
|
||||
|
||||
Exit criteria: no diff outside `crates/ui` and `crates/theme`, the app launches, and
|
||||
switching the theme still restyles everything.
|
||||
What changed:
|
||||
|
||||
- `crates/ui` and `crates/theme` take `gpui-base`.
|
||||
- `ui::init` calls `gpui_base::init(cx)` then `theme::sync_base(cx)`; the `list::init(cx)`
|
||||
call went with `list/`.
|
||||
- `ui`'s crate root re-exports `ElementExt`, `IndexPath`, and `InteractiveElementExt`
|
||||
from `gpui_base`, so existing `use ui::{…}` sites are unchanged. In particular
|
||||
`chat_ui`'s `.on_double_click(…)` is served by base's `InteractiveElementExt`, which
|
||||
is the same implementation as the fork's.
|
||||
- `ui::styled` no longer defines `Selectable`, `Disableable`, or `Collapsible`; it
|
||||
re-exports them from `gpui_base::component_traits`. All three are signature-identical
|
||||
to the fork's, so the `impl` blocks in `avatar`, `button`, `input`, and the rest
|
||||
compile untouched. The path is `component_traits` rather than the crate root because
|
||||
`gpui_base::Collapsible` is base's *component* of that name, not the trait.
|
||||
- Deleted: `checkbox.rs` (312), `list/` (1,477), `index_path.rs` (69),
|
||||
`element_ext.rs` (27), `event.rs` (21), `focusable.rs` (39) — 1,945 lines, with no
|
||||
external consumers and a base counterpart for everything except `FocusableCycle`.
|
||||
|
||||
Two corrections this phase produced:
|
||||
|
||||
- **`history.rs` moved to phase 2.** It maps to base's `UndoHistory`, not `History`:
|
||||
base's `History` is navigation (back/forward), while `UndoHistory` is the grouped
|
||||
undo/redo. Swapping it means editing `input/state.rs` — six `ignore` writes become
|
||||
`set_ignoring`, and `Change` loses its `HistoryItem` impl — which is phase 2's file.
|
||||
- **No dependency became unused.** `ropey`, `sum_tree`, `lsp-types`, `tree-sitter`,
|
||||
`regex`, `unicode-segmentation`, `uuid`, and `instant` are all still used by `input/`
|
||||
and `history.rs`, and the deleted files used none of the others, so pruning happens in
|
||||
phase 2. Separately, four dependencies — `common`, `anyhow`, `itertools`, and `smol` —
|
||||
were already unreferenced anywhere in `crates/ui/src` *before* this change. They are
|
||||
left alone here because removing them is unrelated to the migration.
|
||||
|
||||
Exit criteria: no diff outside `crates/ui` and `crates/theme` — met; the only files
|
||||
touched are the two manifests, `ui/src/lib.rs`, `ui/src/styled.rs`, and
|
||||
`theme/src/lib.rs`. `cargo check` and `cargo build` both pass with no warnings, and
|
||||
`theme` and `ui` still compile for `wasm32-unknown-unknown`. The remaining part of the
|
||||
acceptance — launching the app and walking the settings dialog and chat panel — has to
|
||||
be done by hand and has not been run.
|
||||
|
||||
### Phase 2 — `input/` (the largest single win, ~6.9k lines)
|
||||
|
||||
@@ -213,8 +290,10 @@ menu positioning and dismissal on base `Popup`/`Positioner` is optional and late
|
||||
|
||||
There is no UI test suite to lean on, so each phase gets the same treatment:
|
||||
|
||||
- `cargo check` at the workspace root, plus
|
||||
`cargo check -p coop_web --target wasm32-unknown-unknown` for the web target.
|
||||
- `cargo check` and `cargo build` at the workspace root.
|
||||
- `cargo check -p theme -p ui --target wasm32-unknown-unknown`. The web target cannot
|
||||
be checked end to end until the pre-existing blocker below is fixed, so the migrated
|
||||
crates are checked directly.
|
||||
- Launch the app and walk the surfaces the phase touched. The settings dialog is the
|
||||
densest single smoke surface (Button, GroupBox, Switch, Input, DropdownMenu,
|
||||
PopupMenuItem), followed by the chat panel and the sidebar.
|
||||
@@ -223,6 +302,22 @@ There is no UI test suite to lean on, so each phase gets the same treatment:
|
||||
because base has no equivalent (`set_loading` is the known candidate), list it in the
|
||||
pull request.
|
||||
|
||||
### A pre-existing wasm blocker
|
||||
|
||||
`cargo check -p coop_web --target wasm32-unknown-unknown` fails while compiling
|
||||
`errno 0.3.14`, which refuses `wasm32-unknown-unknown`. The path is
|
||||
`coop_web → workspace → browser-signer-proxy → smol → async-io → rustix → errno`, none
|
||||
of which involves GPUI. `crates/workspace/Cargo.toml` declares `browser-signer-proxy`,
|
||||
but nothing under `crates/workspace/src` references it; the crate is only used by
|
||||
`crates/state`, where it is already gated `#[cfg(not(target_arch = "wasm32"))]`.
|
||||
|
||||
Every version on that path (`errno 0.3.14`, `rustix 1.1.5`, `async-io 2.6.0`,
|
||||
`smol 2.0.2`) is identical before and after phase 0, and no file on it is part of this
|
||||
work, so the web build was already broken. The remedy is deleting that one stale
|
||||
dependency line, but that is unrelated to the migration and is deliberately left out.
|
||||
Until it is done, read the wasm exit criterion for phases 1-4 as "`theme` and `ui`
|
||||
compile for `wasm32-unknown-unknown`".
|
||||
|
||||
## Risks and non-goals
|
||||
|
||||
- **Snapshot lag.** The `gpui-pre` package is a republished snapshot, so it trails zed
|
||||
@@ -236,21 +331,24 @@ There is no UI test suite to lean on, so each phase gets the same treatment:
|
||||
- **Two `Theme` globals.** Confine `gpui_base::Theme` to `theme::sync_base` and
|
||||
`crates/ui` internals; application code keeps using `theme::ActiveTheme`. Avoid
|
||||
importing both `Theme` types into one file.
|
||||
- **`gpui_tokio`** has to be vendored or dropped (phase 0).
|
||||
- **`gpui_tokio` is vendored, not ours.** `crates/gpui_tokio` is zed's crate kept
|
||||
verbatim at `crates/gpui_tokio/src/lib.rs` because the `gpui-pre` family does not
|
||||
publish it and the nostr client needs a Tokio reactor. Re-sync or delete it if
|
||||
longbridge ever ships an equivalent.
|
||||
- **Non-goals:** adopting `gpui-component`, migrating dock/tab/menu, rewriting the
|
||||
self-contained pieces (`icon`, `kbd`, `divider`, `skeleton`, `group_box`,
|
||||
`indicator`), and changing any color, radius, or spacing value.
|
||||
|
||||
## Pull request sequence
|
||||
|
||||
| PR | Content | Touches outside `crates/ui` |
|
||||
| --- | --- | --- |
|
||||
| 1 | Phase 0: move `gpui` to the `gpui-pre` package, fix drift | `Cargo.toml`, possibly `crates/state` |
|
||||
| 2 | Phase 1: base wiring, `sync_base`, deletions | none |
|
||||
| 3 | Phase 2: input | none, or the named gaps above |
|
||||
| 4 | Phase 3: popover, modal, notification, tooltip | none |
|
||||
| 5–10 | Phase 4: one leaf module each | none |
|
||||
| later | Phase 5: dock, as its own plan | `crates/workspace`, `crates/chat_ui` |
|
||||
| PR | Content | Touches outside `crates/ui` | Status |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | Phase 0: `gpui` moves to the `gpui-pre` package, `gpui_tokio` vendored | root `Cargo.toml`, `Cargo.lock`, `web/Cargo.toml`, new `crates/gpui_tokio`; `crates/state` needed no edit | landed |
|
||||
| 2 | Phase 1: base wiring, `sync_base`, deletions | `crates/theme` | landed |
|
||||
| 3 | Phase 2: input, plus `history.rs` → `UndoHistory` and the `ropey`/`sum_tree`/… pruning | none, or the named gaps | not started |
|
||||
| 4 | Phase 3: popover, modal, notification, tooltip | none | not started |
|
||||
| 5–10 | Phase 4: one leaf module each | none | not started |
|
||||
| later | Phase 5: dock, as its own plan | `crates/workspace`, `crates/chat_ui` | not started |
|
||||
|
||||
The end state: the application keeps its design system and its call sites, `crates/ui`
|
||||
shrinks by roughly half, and the parts that are genuinely hard — text editing,
|
||||
|
||||
Reference in New Issue
Block a user