From 134638d6f24bb94e7e2bb58f968ab579c679ce36 Mon Sep 17 00:00:00 2001 From: devnull03 <56480041+devnull03@users.noreply.github.com> Date: Sat, 5 Sep 2026 22:06:59 -0700 Subject: [PATCH 01/14] spike(deps): repoint workspace at gpui-kit 0.6 / gpui-pre Measures the cost of the gpui-component 0.5.2 -> 0.6 move. Upstream renamed the repo to gpui-kit and switched its gpui dependency from zed's git repo to the crates.io `gpui-pre` snapshot, so `gpui` and `gpui-component` become different crates to cargo and the whole workspace has to move at once. Uses cargo's `package =` rename rather than new import paths, so every existing `use gpui::*` / `use gpui_component::*` keeps resolving and the compile errors that remain are genuine API drift rather than path churn. Resolved so far: - settings: `RenderOptions` fields are private, use the `size()` accessor. - Panel split into `gpui_base::dock::Panel` (behaviour) and the styled `Panel` (presentation); `zoomable` moved to base as `-> bool` and `zoom_control` took over the affordance. Splits the four panel impls. - Multi-line inputs are a type now, not a flag: `InputState.multi_line(true)` becomes `TextareaState`, rendered with `Textarea`. Also enables panel zoom (was uniformly `None`) and binds Shift+Esc to `dock::ToggleZoom`, which 0.6 mounts on every tab panel's own frame. Remaining: crates/workspace's dock layer. `DockItem` and the left/right/bottom dock accessors are gone in favour of base's pane-tree model. `app` and `project-wizard` are still unmeasured behind it. --- Cargo.lock | 1046 +++++++++++++----------- Cargo.toml | 12 +- crates/app/src/actions.rs | 7 + crates/diagnostics/src/panel.rs | 20 +- crates/settings/src/lib.rs | 4 +- crates/table/src/delegate.rs | 8 +- crates/table/src/note.rs | 4 +- crates/table/src/panel.rs | 14 +- crates/workspace/src/panels/agent.rs | 23 +- crates/workspace/src/panels/details.rs | 28 +- crates/workspace/src/views/mod.rs | 29 +- 11 files changed, 672 insertions(+), 523 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8b125db7..493eb918 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,12 +14,12 @@ dependencies = [ [[package]] name = "accesskit_atspi_common" -version = "0.18.1" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e8c61bee90b42a772d39d06a740207dc71a4e780004ace1db8d99fb1baaa954" +checksum = "023da0e5097f46df7092d5280b02efb9bbf8d93298daeced42652463e357d636" dependencies = [ "accesskit", - "accesskit_consumer 0.36.0", + "accesskit_consumer 0.38.0", "atspi-common", "phf 0.13.1", "serde", @@ -28,9 +28,9 @@ dependencies = [ [[package]] name = "accesskit_consumer" -version = "0.36.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e0d7e25d06f4dc21d1774d67146e9e80d6789216cbd4d1e88185b0095dba60" +checksum = "f950720ce064757a1b629caad3a408e8d2c63bb01f29b8a3ff8daa331053ffeb" dependencies = [ "accesskit", "hashbrown 0.16.1", @@ -38,9 +38,9 @@ dependencies = [ [[package]] name = "accesskit_consumer" -version = "0.37.0" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950720ce064757a1b629caad3a408e8d2c63bb01f29b8a3ff8daa331053ffeb" +checksum = "5d10a236f96f87d70732e44520046785431ef01d5bcd6b041317bfadd2f88245" dependencies = [ "accesskit", "hashbrown 0.16.1", @@ -62,9 +62,9 @@ dependencies = [ [[package]] name = "accesskit_unix" -version = "0.21.1" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b016ca8db0ea0ea2ceff29a9d6240391492d960716aa471967c00e8cc8cb197c" +checksum = "03e156ed3802e35eefe894ef2671bc6c889303d8a7e110b5e1b48f504b91362f" dependencies = [ "accesskit", "accesskit_atspi_common", @@ -80,12 +80,12 @@ dependencies = [ [[package]] name = "accesskit_windows" -version = "0.33.1" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36e93ac7bf50b964f1cbb75f741629a4e950571baa1ef1274457ab5a80d9bcc2" +checksum = "106c2b961215864d1c2e703ee63269c25c4e80a577ffb2c1017b9c17dcdf83a1" dependencies = [ "accesskit", - "accesskit_consumer 0.37.0", + "accesskit_consumer 0.38.0", "hashbrown 0.16.1", "static_assertions", "windows 0.62.2", @@ -124,7 +124,7 @@ name = "agent-runtime" version = "0.4.0-beta.2" dependencies = [ "alacritty_terminal", - "gpui", + "gpui-pre", "log", "settings", "windows 0.62.2", @@ -262,11 +262,11 @@ dependencies = [ "checks", "data-exchange", "diagnostics", - "dirs", - "gpui", + "dirs 6.0.0", "gpui-component", - "gpui-component-assets", - "gpui_platform", + "gpui-kit-assets", + "gpui-pre", + "gpui-pre-platform", "keyring", "log", "os_info", @@ -537,15 +537,6 @@ dependencies = [ "syn", ] -[[package]] -name = "atoi_simd" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a49e05797ca52e312a0c658938b7d00693ef037799ef7187678f212d7684cf" -dependencies = [ - "debug_unsafe", -] - [[package]] name = "atomic" version = "0.5.3" @@ -677,11 +668,20 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bindgen" -version = "0.71.1" +version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ "bitflags 2.11.0", "cexpr", @@ -922,25 +922,7 @@ dependencies = [ "log", "quick-xml 0.31.0", "serde", - "zip 2.4.2", -] - -[[package]] -name = "calamine" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da56b262e8a827c6b12c3dde4ea4622e0ff542bd2e9ea5855e4cb523481d77b7" -dependencies = [ - "atoi_simd", - "byteorder", - "chrono", - "codepage", - "encoding_rs", - "fast-float2", - "log", - "quick-xml 0.38.4", - "serde", - "zip 4.6.1", + "zip", ] [[package]] @@ -1049,7 +1031,7 @@ version = "0.4.0-beta.2" dependencies = [ "diagnostics", "edtf", - "gpui", + "gpui-pre", "log", "reqwest", "serde_json", @@ -1172,16 +1154,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "collections" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#fc9258b4632544c0819953c59e2c31b9e92ea362" -dependencies = [ - "gpui_util", - "indexmap", - "rustc-hash 2.1.1", -] - [[package]] name = "color_quant" version = "1.1.0" @@ -1665,7 +1637,7 @@ name = "data-exchange" version = "0.4.0-beta.2" dependencies = [ "base64", - "calamine 0.26.1", + "calamine", "csv", "getrandom 0.3.4", "log", @@ -1676,7 +1648,7 @@ dependencies = [ "settings", "sha2", "thiserror 2.0.18", - "zip 2.4.2", + "zip", ] [[package]] @@ -1714,12 +1686,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "debug_unsafe" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eed2c4702fa172d1ce21078faa7c5203e69f5394d48cc436d25928394a867a2" - [[package]] name = "deflate64" version = "0.1.11" @@ -1776,22 +1742,12 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "derive_refineable" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#fc9258b4632544c0819953c59e2c31b9e92ea362" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "diagnostics" version = "0.4.0-beta.2" dependencies = [ - "gpui", "gpui-component", + "gpui-pre", "log", "settings", ] @@ -1807,13 +1763,34 @@ dependencies = [ "subtle", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + [[package]] name = "dirs" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ - "dirs-sys", + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.4.6", + "windows-sys 0.48.0", ] [[package]] @@ -1824,7 +1801,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.5.2", "windows-sys 0.61.2", ] @@ -2147,10 +2124,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] -name = "fast-float2" -version = "0.2.4" +name = "fancy-regex" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6e8948ce679d00a02a94739ea185595dca7118ed04feb991127e443bd3d761f" +checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f" +dependencies = [ + "bit-set 0.8.0", + "regex-automata", + "regex-syntax", +] [[package]] name = "fastrand" @@ -2238,7 +2220,6 @@ checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", - "zlib-rs", ] [[package]] @@ -2717,99 +2698,63 @@ dependencies = [ ] [[package]] -name = "gpui" -version = "0.2.2" -source = "git+https://github.com/zed-industries/zed#fc9258b4632544c0819953c59e2c31b9e92ea362" +name = "gpui-base" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2caaf00ebe0482774dd370a82a936edf4bf19a18a0d1a39353d20ecf61f70330" dependencies = [ - "accesskit", + "aho-corasick", "anyhow", "async-channel", - "async-task", - "backtrace", - "bindgen", - "bitflags 2.11.0", "chrono", - "collections", - "core-video", - "ctor", - "derive_more", - "embed-resource", - "etagere", "futures", - "futures-concurrency", - "getrandom 0.3.4", - "gpui_macros", - "gpui_shared_string", - "gpui_util", - "heapless", - "http_client", - "image", - "inventory", - "itertools 0.14.0", - "log", - "lyon", - "num_cpus", - "parking", - "parking_lot", - "pin-project", - "pollster 0.4.0", - "postage", - "profiling", - "proptest", - "rand 0.9.2", + "gpui-pre", + "gpui-pre-macros", + "gpui-pre-platform", + "gpui-pre-sum-tree", + "html5ever", + "instant", + "lsp-types", + "markdown", + "markup5ever_rcdom", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", "raw-window-handle", - "refineable", "regex", - "resvg 0.46.0", - "scheduler", + "ropey", "schemars", - "seahash", "serde", "serde_json", - "slotmap", "smallvec", - "spin 0.10.0", - "strum", - "sum_tree", - "taffy", - "thiserror 2.0.18", + "smol", + "syntect", "tracing", - "ttf-parser", - "url", - "usvg 0.46.0", - "util_macros", - "uuid", - "waker-fn", + "unicode-segmentation", "web-time", - "windows 0.61.3", - "zed-font-kit", - "zed-scap", - "ztracing", ] [[package]] name = "gpui-component" -version = "0.5.2" -source = "git+https://github.com/longbridge/gpui-component#98af8912ab0b3fe08df519dff7acd96a77b19586" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd7938c395be32cea6127b92f7206501ad1fe7e121a609de1aafdedcb4d98be" dependencies = [ - "aho-corasick", "anyhow", - "async-channel", "chrono", "core-text", "enum-iterator", - "futures", - "gpui", - "gpui-component-assets", + "gpui-base", "gpui-component-macros", - "gpui_macros", - "html5ever", + "gpui-kit-assets", + "gpui-pre", + "gpui-pre-macros", + "gpui-pre-sum-tree", "instant", "itertools 0.13.0", "log", "lsp-types", "markdown", - "markup5ever_rcdom", "notify", "num-traits", "objc2 0.6.4", @@ -2818,7 +2763,6 @@ dependencies = [ "once_cell", "paste", "raw-window-handle", - "regex", "resvg 0.45.1", "ropey", "rust-i18n", @@ -2829,52 +2773,126 @@ dependencies = [ "smallvec", "smol", "tracing", - "unicode-segmentation", "uuid", "windows 0.58.0", - "zed-sum-tree", ] [[package]] -name = "gpui-component-assets" -version = "0.5.1" -source = "git+https://github.com/longbridge/gpui-component#98af8912ab0b3fe08df519dff7acd96a77b19586" +name = "gpui-component-macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dacef57cadf1ce9b05b1f1ba6b3f3fed2521fd814f548eea0e4d8e43c5857795" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "gpui-kit-assets" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb6fed67d6488fa9333b6534d17f5eaef32b83e3d7148b83a3d444a1884bd1c" dependencies = [ "anyhow", - "gpui", + "gpui-pre", + "gpui-pre-reqwest", "log", "rust-embed", "wasm-bindgen", "wasm-bindgen-futures", - "zed-reqwest", ] [[package]] -name = "gpui-component-macros" -version = "0.5.1" -source = "git+https://github.com/longbridge/gpui-component#98af8912ab0b3fe08df519dff7acd96a77b19586" +name = "gpui-pre" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8b0f6f593153b6eb84c336ea7ccf12a1fca186f0db99af628e72f85048c1d7b" dependencies = [ - "proc-macro2", - "quote", - "syn", + "accesskit", + "anyhow", + "async-channel", + "async-task", + "backtrace", + "bindgen", + "bitflags 2.11.0", + "chrono", + "core-video", + "ctor", + "derive_more", + "embed-resource", + "etagere", + "futures", + "futures-concurrency", + "getrandom 0.3.4", + "gpui-pre-collections", + "gpui-pre-http-client", + "gpui-pre-macros", + "gpui-pre-refineable", + "gpui-pre-scheduler", + "gpui-pre-shared-string", + "gpui-pre-sum-tree", + "gpui-pre-util", + "gpui-pre-util-macros", + "gpui-pre-ztracing", + "heapless", + "image", + "inventory", + "itertools 0.14.0", + "log", + "lyon", + "num_cpus", + "parking", + "parking_lot", + "pin-project", + "pollster 0.4.0", + "postage", + "profiling", + "proptest", + "rand 0.9.2", + "raw-window-handle", + "regex", + "resvg 0.46.0", + "schemars", + "seahash", + "serde", + "serde_json", + "slotmap", + "smallvec", + "spin 0.10.0", + "strum", + "taffy", + "thiserror 2.0.18", + "tracing", + "ttf-parser", + "url", + "usvg 0.46.0", + "uuid", + "waker-fn", + "web-time", + "windows 0.62.2", + "zed-font-kit", + "zed-scap", ] [[package]] -name = "gpui_apple" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#fc9258b4632544c0819953c59e2c31b9e92ea362" +name = "gpui-pre-apple" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e027ea436bb876a8da6962862ffdbf9708c6b8352415325647c4fc2a48467d" dependencies = [ "anyhow", "block", "cbindgen", "cocoa 0.26.0", - "collections", "core-foundation 0.10.0", "core-video", "derive_more", "etagere", "foreign-types 0.5.0", - "gpui", + "gpui-pre", + "gpui-pre-collections", "image", "log", "metal", @@ -2883,9 +2901,53 @@ dependencies = [ ] [[package]] -name = "gpui_linux" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#fc9258b4632544c0819953c59e2c31b9e92ea362" +name = "gpui-pre-collections" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89c8efa2e51e368c8538a7be1ea9a12127ca03abe6cc01d9d3474e9ac4f53016" +dependencies = [ + "gpui-pre-util", + "indexmap", + "rustc-hash 2.1.1", +] + +[[package]] +name = "gpui-pre-derive-refineable" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a098d319acc9f84bf159944f96c5ea43a4f4cd7ed759f984acbd15719495aa0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "gpui-pre-http-client" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3495a45a28cb800c8626d2053406114bcaca26d390a4d4b183365b5bb8fdaf02" +dependencies = [ + "anyhow", + "async-compression", + "bytes", + "derive_more", + "futures", + "http", + "http-body", + "log", + "parking_lot", + "serde", + "serde_json", + "serde_urlencoded", + "url", +] + +[[package]] +name = "gpui-pre-linux" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9025a2a9f7e9241651bb5f9df6b69233ca4111b233e1fb3a4544d69ecc52bc3" dependencies = [ "accesskit", "accesskit_unix", @@ -2896,13 +2958,13 @@ dependencies = [ "bytemuck", "calloop", "calloop-wayland-source", - "collections", "filedescriptor", "futures", - "gpui", - "gpui_util", - "gpui_wgpu", - "http_client", + "gpui-pre", + "gpui-pre-collections", + "gpui-pre-http-client", + "gpui-pre-util", + "gpui-pre-wgpu", "libc", "log", "notify-rust", @@ -2929,9 +2991,10 @@ dependencies = [ ] [[package]] -name = "gpui_macos" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#fc9258b4632544c0819953c59e2c31b9e92ea362" +name = "gpui-pre-macos" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54678bfe6be71aa8e3fd31c3ae51af5d81e367344762540fd7cfdb778621abb1" dependencies = [ "accesskit", "accesskit_macos", @@ -2940,7 +3003,6 @@ dependencies = [ "block", "block2 0.6.2", "cocoa 0.26.0", - "collections", "core-foundation 0.10.0", "core-foundation-sys", "core-graphics 0.24.0", @@ -2949,15 +3011,16 @@ dependencies = [ "dispatch2", "foreign-types 0.5.0", "futures", - "gpui", - "gpui_apple", - "gpui_util", + "gpui-pre", + "gpui-pre-apple", + "gpui-pre-collections", + "gpui-pre-media", + "gpui-pre-util", "image", "itertools 0.14.0", "libc", "log", "mach2", - "media", "metal", "objc", "objc2 0.6.4", @@ -2975,33 +3038,139 @@ dependencies = [ ] [[package]] -name = "gpui_macros" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#fc9258b4632544c0819953c59e2c31b9e92ea362" +name = "gpui-pre-macros" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be2db7b5097b4d523b2bce933604bcc5acdaf679bb9a150e8299e6c07efc29c" dependencies = [ "heck 0.5.0", + "proc-macro-crate", "proc-macro2", "quote", "syn", ] [[package]] -name = "gpui_platform" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#fc9258b4632544c0819953c59e2c31b9e92ea362" +name = "gpui-pre-media" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a35102d2a165f2fbf21521d56e37d6f0ab906372d13b3e1fa94d68b299b22b74" +dependencies = [ + "anyhow", + "bindgen", + "core-foundation 0.10.0", + "core-video", + "foreign-types 0.5.0", + "metal", + "objc", +] + +[[package]] +name = "gpui-pre-perf" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1db0c046b93c2a29120f8ee4c04bc80a4d7d6117d1164ef349faface8943491" +dependencies = [ + "gpui-pre-collections", + "serde", + "serde_json", +] + +[[package]] +name = "gpui-pre-platform" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35131851444188e573d21d3e8020a51af16bf08b177e25b1120815edbd448c80" dependencies = [ "console_error_panic_hook", - "gpui", - "gpui_linux", - "gpui_macos", - "gpui_web", - "gpui_windows", + "gpui-pre", + "gpui-pre-linux", + "gpui-pre-macos", + "gpui-pre-web", + "gpui-pre-windows", ] [[package]] -name = "gpui_shared_string" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#fc9258b4632544c0819953c59e2c31b9e92ea362" +name = "gpui-pre-refineable" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864e2e54a3029481dae5b6aae3ba2905f2fb1dfe66ced913b68c9ff527f8e327" +dependencies = [ + "gpui-pre-derive-refineable", +] + +[[package]] +name = "gpui-pre-reqwest" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05be23908e707966824f8c51a609904b7f30739e8d915e120a53c1b995ba964c" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-native-certs", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration 0.6.1", + "tokio", + "tokio-rustls", + "tokio-socks", + "tokio-util", + "tower", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "windows-registry 0.4.0", +] + +[[package]] +name = "gpui-pre-scheduler" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b58a78c4e0c032900704ea49922641c534cf8bbf1bf22eda6ba00a76e88c6c3" +dependencies = [ + "async-task", + "backtrace", + "chrono", + "flume", + "futures", + "parking_lot", + "rand 0.9.2", + "wasm_thread", + "web-time", +] + +[[package]] +name = "gpui-pre-shared-string" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82fc99fe88e44173758a500522d3f4059a6541da4b471af720e3f60cf34a2bc3" dependencies = [ "schemars", "serde", @@ -3009,9 +3178,23 @@ dependencies = [ ] [[package]] -name = "gpui_util" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#fc9258b4632544c0819953c59e2c31b9e92ea362" +name = "gpui-pre-sum-tree" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "002baed852f20cef1188d3d5e0f749025fc718e4d6cea026b9077a9a6c10d042" +dependencies = [ + "gpui-pre-ztracing", + "heapless", + "log", + "rayon", + "tracing", +] + +[[package]] +name = "gpui-pre-util" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fe779c4cb00929aafcb2b307cd1240588aebb5d1eb328c59b80f77c05a41fad" dependencies = [ "anyhow", "log", @@ -3019,21 +3202,33 @@ dependencies = [ ] [[package]] -name = "gpui_web" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#fc9258b4632544c0819953c59e2c31b9e92ea362" +name = "gpui-pre-util-macros" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f0ccc4bccb6a31786095d15fc9f40d6a4c6a295522e3460331dd7e929ff2f79" +dependencies = [ + "gpui-pre-perf", + "quote", + "syn", +] + +[[package]] +name = "gpui-pre-web" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27728bd9180d1a10516d719fed446adf85872cedf0e262ad0738f4edc9deaf33" dependencies = [ "anyhow", "console_error_panic_hook", "futures", - "gpui", - "gpui_wgpu", - "http_client", + "gpui-pre", + "gpui-pre-http-client", + "gpui-pre-scheduler", + "gpui-pre-wgpu", "js-sys", "log", "parking_lot", "raw-window-handle", - "scheduler", "uuid", "wasm-bindgen", "wasm-bindgen-futures", @@ -3043,17 +3238,18 @@ dependencies = [ ] [[package]] -name = "gpui_wgpu" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#fc9258b4632544c0819953c59e2c31b9e92ea362" +name = "gpui-pre-wgpu" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "695c613099863bbf434ff896d196e3f55d4d5f59769dac99596e79dbb18a8aff" dependencies = [ "anyhow", "bytemuck", - "collections", "cosmic-text", "etagere", - "gpui", - "gpui_util", + "gpui-pre", + "gpui-pre-collections", + "gpui-pre-util", "itertools 0.14.0", "log", "parking_lot", @@ -3069,19 +3265,20 @@ dependencies = [ ] [[package]] -name = "gpui_windows" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#fc9258b4632544c0819953c59e2c31b9e92ea362" +name = "gpui-pre-windows" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a4b60579aa2cb4ef7b6f22133a7b9fe12458062961343bdcf36e259047bc7ad" dependencies = [ "accesskit", "accesskit_windows", "anyhow", - "collections", "dunce", "etagere", "futures", - "gpui", - "gpui_util", + "gpui-pre", + "gpui-pre-collections", + "gpui-pre-util", "image", "itertools 0.14.0", "log", @@ -3090,12 +3287,42 @@ dependencies = [ "raw-window-handle", "smallvec", "uuid", - "windows 0.61.3", - "windows-core 0.61.2", - "windows-numerics 0.2.0", - "windows-registry 0.5.3", + "windows 0.62.2", + "windows-core 0.62.2", + "windows-numerics 0.3.1", + "windows-registry 0.6.1", +] + +[[package]] +name = "gpui-pre-zlog" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1416ea5f018e3a8a1c8be266332c443583f28f8f87379a84ab1e77622173ac0" +dependencies = [ + "anyhow", + "chrono", + "gpui-pre-collections", + "log", +] + +[[package]] +name = "gpui-pre-ztracing" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cc64a25a3cc4e4f1d8acb00074d3925339dae657c020083735738ba8af96bf7" +dependencies = [ + "gpui-pre-zlog", + "gpui-pre-ztracing-macro", + "tracing", + "tracing-subscriber", ] +[[package]] +name = "gpui-pre-ztracing-macro" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f3ea75672348f37d94472e579f5979ee19b744d0fa5aaadc2fe129ccafa80b" + [[package]] name = "h2" version = "0.4.13" @@ -3298,26 +3525,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "http_client" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#fc9258b4632544c0819953c59e2c31b9e92ea362" -dependencies = [ - "anyhow", - "async-compression", - "bytes", - "derive_more", - "futures", - "http", - "http-body", - "log", - "parking_lot", - "serde", - "serde_json", - "serde_urlencoded", - "url", -] - [[package]] name = "httparse" version = "1.10.1" @@ -4287,20 +4494,6 @@ dependencies = [ "digest", ] -[[package]] -name = "media" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#fc9258b4632544c0819953c59e2c31b9e92ea362" -dependencies = [ - "anyhow", - "bindgen", - "core-foundation 0.10.0", - "core-video", - "foreign-types 0.5.0", - "metal", - "objc", -] - [[package]] name = "memchr" version = "2.8.0" @@ -5459,16 +5652,6 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" -[[package]] -name = "perf" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#fc9258b4632544c0819953c59e2c31b9e92ea362" -dependencies = [ - "collections", - "serde", - "serde_json", -] - [[package]] name = "phf" version = "0.11.3" @@ -5631,7 +5814,7 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" name = "plugin-api" version = "0.4.0-beta.2" dependencies = [ - "gpui", + "gpui-pre", "serde_json", "settings", ] @@ -5641,7 +5824,7 @@ name = "plugin-host" version = "0.4.0-beta.2" dependencies = [ "diagnostics", - "gpui", + "gpui-pre", "log", "mlua", "plugin-api", @@ -5796,9 +5979,9 @@ version = "0.4.0-beta.2" dependencies = [ "anyhow", "block2 0.6.2", - "dirs", - "gpui", + "dirs 6.0.0", "gpui-component", + "gpui-pre", "image", "log", "mime_guess", @@ -5812,7 +5995,7 @@ dependencies = [ "symphonia", "tiff", "windows 0.62.2", - "zip 2.4.2", + "zip", ] [[package]] @@ -5857,13 +6040,12 @@ name = "project-wizard" version = "0.4.0-beta.2" dependencies = [ "anyhow", - "calamine 0.31.0", "csv", "data-exchange", "diagnostics", - "dirs", - "gpui", + "dirs 6.0.0", "gpui-component", + "gpui-pre", "log", "plugin-api", "serde", @@ -5874,8 +6056,9 @@ dependencies = [ [[package]] name = "proptest" -version = "1.10.0" -source = "git+https://github.com/proptest-rs/proptest?rev=3dca198a8fef1b32e3a66f1e1897c955b4dc5b5b#3dca198a8fef1b32e3a66f1e1897c955b4dc5b5b" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" dependencies = [ "bit-set 0.8.0", "bit-vec 0.8.0", @@ -5894,7 +6077,8 @@ dependencies = [ [[package]] name = "proptest-macro" version = "0.5.0" -source = "git+https://github.com/proptest-rs/proptest?rev=3dca198a8fef1b32e3a66f1e1897c955b4dc5b5b#3dca198a8fef1b32e3a66f1e1897c955b4dc5b5b" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efaa288b896cb2b345da7b7f2110ab19e51565b83495b56fcec98a62f8b1f33e" dependencies = [ "convert_case 0.11.0", "proc-macro2", @@ -5963,7 +6147,6 @@ version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" dependencies = [ - "encoding_rs", "memchr", "serde", ] @@ -6260,6 +6443,17 @@ dependencies = [ "bitflags 2.11.0", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + [[package]] name = "redox_users" version = "0.5.2" @@ -6291,14 +6485,6 @@ dependencies = [ "syn", ] -[[package]] -name = "refineable" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#fc9258b4632544c0819953c59e2c31b9e92ea362" -dependencies = [ - "derive_refineable", -] - [[package]] name = "regex" version = "1.12.3" @@ -6735,22 +6921,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "scheduler" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#fc9258b4632544c0819953c59e2c31b9e92ea362" -dependencies = [ - "async-task", - "backtrace", - "chrono", - "flume", - "futures", - "parking_lot", - "rand 0.9.2", - "wasm_thread", - "web-time", -] - [[package]] name = "schemars" version = "1.2.1" @@ -7032,9 +7202,9 @@ name = "settings" version = "0.4.0-beta.2" dependencies = [ "anyhow", - "dirs", - "gpui", + "dirs 6.0.0", "gpui-component", + "gpui-pre", "log", "rusqlite", "serde", @@ -7084,7 +7254,7 @@ version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32824fab5e16e6c4d86dc1ba84489390419a39f97699852b66480bb87d297ed8" dependencies = [ - "dirs", + "dirs 6.0.0", ] [[package]] @@ -7256,7 +7426,7 @@ name = "spellcheck" version = "0.4.0-beta.2" dependencies = [ "diagnostics", - "gpui", + "gpui-pre", "log", "reqwest", "settings", @@ -7379,18 +7549,6 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" -[[package]] -name = "sum_tree" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#fc9258b4632544c0819953c59e2c31b9e92ea362" -dependencies = [ - "heapless", - "log", - "rayon", - "tracing", - "ztracing", -] - [[package]] name = "sval" version = "2.17.0" @@ -7695,6 +7853,24 @@ dependencies = [ "syn", ] +[[package]] +name = "syntect" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925" +dependencies = [ + "bincode", + "fancy-regex", + "flate2", + "fnv", + "once_cell", + "regex-syntax", + "serde", + "serde_derive", + "thiserror 2.0.18", + "walkdir", +] + [[package]] name = "sys-locale" version = "0.3.2" @@ -7771,8 +7947,8 @@ dependencies = [ "ai", "base64", "diagnostics", - "gpui", "gpui-component", + "gpui-pre", "log", "plugin-api", "plugin-host", @@ -8472,7 +8648,7 @@ dependencies = [ "tar", "tempfile", "windows 0.62.2", - "zip 2.4.2", + "zip", ] [[package]] @@ -8562,16 +8738,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" -[[package]] -name = "util_macros" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#fc9258b4632544c0819953c59e2c31b9e92ea362" -dependencies = [ - "perf", - "quote", - "syn", -] - [[package]] name = "uuid" version = "1.21.0" @@ -8844,7 +9010,8 @@ dependencies = [ [[package]] name = "wasm_thread" version = "0.3.3" -source = "git+https://github.com/zed-industries/wasm_thread?rev=0cf96c7708dfb97ccf3da50347e25edcf75d6937#0cf96c7708dfb97ccf3da50347e25edcf75d6937" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7516db7f32decdadb1c3b8deb1b7d78b9df7606c5cc2f6241737c2ab3a0258e" dependencies = [ "futures", "js-sys", @@ -8872,6 +9039,7 @@ checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" dependencies = [ "cc", "downcast-rs", + "log", "rustix", "scoped-tls", "smallvec", @@ -9210,8 +9378,8 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" name = "window-wrapper" version = "0.4.0-beta.2" dependencies = [ - "gpui", "gpui-component", + "gpui-pre", "schemars", "serde", "serde_json", @@ -9486,6 +9654,17 @@ dependencies = [ "windows-strings 0.4.2", ] +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + [[package]] name = "windows-result" version = "0.1.2" @@ -9568,6 +9747,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -9619,6 +9807,21 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -9685,6 +9888,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -9703,6 +9912,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -9721,6 +9936,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -9751,6 +9972,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -9769,6 +9996,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -9787,6 +10020,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -9805,6 +10044,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -9949,8 +10194,8 @@ version = "0.4.0-beta.2" dependencies = [ "agent-runtime", "diagnostics", - "gpui", "gpui-component", + "gpui-pre", "log", "preview", "schemars", @@ -9960,12 +10205,6 @@ dependencies = [ "table", ] -[[package]] -name = "workspace-hack" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beffa227304dbaea3ad6a06ac674f9bc83a3dec3b7f63eeb442de37e7cb6bb01" - [[package]] name = "writeable" version = "0.6.2" @@ -10053,15 +10292,17 @@ dependencies = [ [[package]] name = "xim-ctext" version = "0.3.0" -source = "git+https://github.com/zed-industries/xim-rs.git?rev=16f35a2c881b815a2b6cdfd6687988e84f8447d8#16f35a2c881b815a2b6cdfd6687988e84f8447d8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ac61a7062c40f3c37b6e82eeeef835d5cc7824b632a72784a89b3963c33284c" dependencies = [ "encoding_rs", ] [[package]] name = "xim-parser" -version = "0.2.1" -source = "git+https://github.com/zed-industries/xim-rs.git?rev=16f35a2c881b815a2b6cdfd6687988e84f8447d8#16f35a2c881b815a2b6cdfd6687988e84f8447d8" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dcee45f89572d5a65180af3a84e7ddb24f5ea690a6d3aa9de231281544dd7b7" dependencies = [ "bitflags 2.11.0", ] @@ -10324,14 +10565,15 @@ dependencies = [ [[package]] name = "zed-font-kit" version = "0.14.1-zed" -source = "git+https://github.com/zed-industries/font-kit?rev=94b0f28166665e8fd2f53ff6d268a14955c82269#94b0f28166665e8fd2f53ff6d268a14955c82269" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3898e450f36f852edda72e3f985c34426042c4951790b23b107f93394f9bff5" dependencies = [ "bitflags 2.11.0", "byteorder", "core-foundation 0.10.0", "core-graphics 0.24.0", "core-text", - "dirs", + "dirs 5.0.1", "dwrote", "float-ord", "freetype-sys", @@ -10345,59 +10587,11 @@ dependencies = [ "yeslogic-fontconfig-sys", ] -[[package]] -name = "zed-reqwest" -version = "0.12.15-zed" -source = "git+https://github.com/zed-industries/reqwest.git?rev=c15662463bda39148ba154100dd44d3fba5873a4#c15662463bda39148ba154100dd44d3fba5873a4" -dependencies = [ - "base64", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "ipnet", - "js-sys", - "log", - "mime", - "mime_guess", - "once_cell", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-native-certs", - "rustls-pemfile", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "system-configuration 0.6.1", - "tokio", - "tokio-rustls", - "tokio-socks", - "tokio-util", - "tower", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", - "windows-registry 0.4.0", -] - [[package]] name = "zed-scap" version = "0.0.8-zed" -source = "git+https://github.com/zed-industries/scap?rev=4afea48c3b002197176fb19cd0f9b180dd36eaac#4afea48c3b002197176fb19cd0f9b180dd36eaac" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b338d705ae33a43ca00287c11129303a7a0aa57b101b72a1c08c863f698ac8" dependencies = [ "anyhow", "cocoa 0.25.0", @@ -10415,22 +10609,11 @@ dependencies = [ "xcb", ] -[[package]] -name = "zed-sum-tree" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d490156d0d7311855564d6e1d6dccab992405a0c0e15e1c8ef18920c02177e35" -dependencies = [ - "arrayvec", - "log", - "rayon", - "workspace-hack", -] - [[package]] name = "zed-xim" version = "0.4.0-zed" -source = "git+https://github.com/zed-industries/xim-rs.git?rev=16f35a2c881b815a2b6cdfd6687988e84f8447d8#16f35a2c881b815a2b6cdfd6687988e84f8447d8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0b46ed118eba34d9ba53d94ddc0b665e0e06a2cf874cfa2dd5dec278148642" dependencies = [ "ahash", "hashbrown 0.14.5", @@ -10570,37 +10753,6 @@ dependencies = [ "zstd", ] -[[package]] -name = "zip" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" -dependencies = [ - "arbitrary", - "crc32fast", - "flate2", - "indexmap", - "memchr", - "zopfli", -] - -[[package]] -name = "zlib-rs" -version = "0.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" - -[[package]] -name = "zlog" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#fc9258b4632544c0819953c59e2c31b9e92ea362" -dependencies = [ - "anyhow", - "chrono", - "collections", - "log", -] - [[package]] name = "zmij" version = "1.0.21" @@ -10647,22 +10799,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "ztracing" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#fc9258b4632544c0819953c59e2c31b9e92ea362" -dependencies = [ - "tracing", - "tracing-subscriber", - "zlog", - "ztracing_macro", -] - -[[package]] -name = "ztracing_macro" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#fc9258b4632544c0819953c59e2c31b9e92ea362" - [[package]] name = "zune-core" version = "0.4.12" diff --git a/Cargo.toml b/Cargo.toml index 2ce56c95..bece5297 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,10 +41,14 @@ anyhow = "1" # spreadsheet format for the price of one extension match. calamine = { version = "0.31", features = ["dates"] } csv = "1.3" -gpui = { git = "https://github.com/zed-industries/zed" } -gpui_platform = { git = "https://github.com/zed-industries/zed", features = ["font-kit", "wayland", "x11"] } -gpui-component = { git = "https://github.com/longbridge/gpui-component" } -gpui-component-assets = { git = "https://github.com/longbridge/gpui-component" } +# SPIKE: gpui-kit 0.6. Upstream moved off zed's git repo onto the crates.io `gpui-pre*` +# snapshot, so these are renamed packages rather than new names in our source — every +# `use gpui::*` / `use gpui_component::*` in the workspace keeps resolving unchanged, which +# makes every remaining compile error genuine API drift. +gpui = { package = "gpui-pre", version = "0.3" } +gpui_platform = { package = "gpui-pre-platform", version = "0.3", features = ["font-kit", "wayland", "x11"] } +gpui-component = { version = "0.6" } +gpui-component-assets = { package = "gpui-kit-assets", version = "0.6" } dirs = "6" log = "0.4" # cpal-plus-symphonia glue. `preview` already decodes audio with symphonia for cover art, and the diff --git a/crates/app/src/actions.rs b/crates/app/src/actions.rs index a1cc9fb5..5296050b 100644 --- a/crates/app/src/actions.rs +++ b/crates/app/src/actions.rs @@ -6,6 +6,7 @@ //! (e.g. the dock toggles, handled on the `App` root in `main.rs`). use gpui::*; +use gpui_component::dock::ToggleZoom; use crate::app_menus::OpenSettings; @@ -36,6 +37,12 @@ pub fn key_bindings() -> Vec { // Save the open project's data to its `.qrate` file. Global: saving shouldn't depend on // where focus sits (a focused cell editor's `Input` context doesn't bind Ctrl+S). KeyBinding::new("secondary-s", Save, None), + // Zoom the focused panel to fill the window, Zed's Shift+Esc. Global and unhandled by us: + // the library mounts the `ToggleZoom` handler on every tab panel's own frame, so the + // action dispatches from wherever focus sits up to the panel containing it — which is + // what makes one binding mean "zoom *this* panel". A panel opts out with + // `BasePanel::zoomable`; zooming back out is never refused, so this cannot strand anyone. + KeyBinding::new("shift-escape", ToggleZoom, None), KeyBinding::new("secondary-b", ToggleLeftDock, None), KeyBinding::new("secondary-`", ToggleBottomDock, None), KeyBinding::new("secondary-alt-b", ToggleRightDock, None), diff --git a/crates/diagnostics/src/panel.rs b/crates/diagnostics/src/panel.rs index 4a8c4bd5..84829eff 100644 --- a/crates/diagnostics/src/panel.rs +++ b/crates/diagnostics/src/panel.rs @@ -3,7 +3,7 @@ use std::rc::Rc; use gpui::prelude::FluentBuilder as _; use gpui::*; use gpui_component::button::{Button, ButtonVariants as _}; -use gpui_component::dock::{Panel, PanelControl, PanelEvent}; +use gpui_component::dock::{BasePanel, Panel, PanelEvent}; use gpui_component::menu::{ContextMenuExt as _, DropdownMenu as _, PopupMenu, PopupMenuItem}; use gpui_component::tab::{Tab, TabBar}; use gpui_component::{ActiveTheme as _, Icon, IconName, Sizable as _, h_flex, v_flex}; @@ -213,22 +213,24 @@ impl Focusable for ProblemsPanel { impl EventEmitter for ProblemsPanel {} -impl Panel for ProblemsPanel { +impl BasePanel for ProblemsPanel { fn panel_name(&self) -> &'static str { "ProblemsPanel" } - fn title(&mut self, _w: &mut Window, _cx: &mut Context) -> impl IntoElement { - SharedString::from("Problems") - } - - // The library always renders the ⋯ menu button; these just empty it of Close + Zoom. + // The library always renders the ⋯ menu button; this just empties it of Close. fn closable(&self, _cx: &App) -> bool { false } - fn zoomable(&self, _cx: &App) -> Option { - None + fn zoomable(&self, _cx: &App) -> bool { + true + } +} + +impl Panel for ProblemsPanel { + fn title(&mut self, _w: &mut Window, _cx: &mut Context) -> impl IntoElement { + SharedString::from("Problems") } } diff --git a/crates/settings/src/lib.rs b/crates/settings/src/lib.rs index 17a1cb7f..f5f73bb4 100644 --- a/crates/settings/src/lib.rs +++ b/crates/settings/src/lib.rs @@ -403,8 +403,8 @@ pub fn path_picker_item( }); let write = Arc::clone(&write); PathPickerApp { - field_size: options.size, - button_size: Some(options.size), + field_size: options.size(), + button_size: Some(options.size()), button_id: SharedString::from(format!("browse-{key}")), files, directories, diff --git a/crates/table/src/delegate.rs b/crates/table/src/delegate.rs index 9883d972..9d8c584b 100644 --- a/crates/table/src/delegate.rs +++ b/crates/table/src/delegate.rs @@ -7,7 +7,7 @@ use gpui::{ Window, div, px, }; use gpui_component::{ - input::InputState, + input::TextareaState, table::{Column, TableDelegate, TableState}, }; use serde::{Deserialize, Serialize}; @@ -64,11 +64,11 @@ pub struct QrateTableDelegate { pub(crate) selected_rows: BTreeSet, pub(crate) editing: EditState, /// Shared single-line editor, reused across whichever cell is being edited. - pub(crate) editor: Entity, + pub(crate) editor: Entity, /// Which cell/row/column the note editor is open on, if any. pub(crate) note_edit: Option, /// Shared note editor, the same one-per-table arrangement as [`Self::editor`]. - pub(crate) note_editor: Entity, + pub(crate) note_editor: Entity, /// Each row's resolved image path, parallel to `rows`. `None` until `TablePanel` resolves it. image_paths: Vec>, /// View→source row mapping: `visible_rows[view] == source`. The library only ever sees this @@ -96,7 +96,7 @@ pub struct QrateTableDelegate { } impl QrateTableDelegate { - pub(crate) fn new(editor: Entity, note_editor: Entity) -> Self { + pub(crate) fn new(editor: Entity, note_editor: Entity) -> Self { Self { columns: Vec::new(), rows: Vec::new(), diff --git a/crates/table/src/note.rs b/crates/table/src/note.rs index fb652d22..722f1ed8 100644 --- a/crates/table/src/note.rs +++ b/crates/table/src/note.rs @@ -13,7 +13,7 @@ use gpui::{ IntoElement, ParentElement as _, Path, SharedString, Styled as _, Window, canvas, deferred, div, point, px, }; -use gpui_component::input::Input; +use gpui_component::input::Textarea; use gpui_component::menu::{PopupMenu, PopupMenuItem}; use gpui_component::table::TableState; use gpui_component::{ActiveTheme as _, h_flex}; @@ -724,7 +724,7 @@ pub(crate) fn editor( .text_color(cx.theme().muted_foreground) .child(scope), ) - .child(Input::new(&delegate.note_editor).h_full()), + .child(Textarea::new(&delegate.note_editor).h_full()), )) .into_any_element(), ) diff --git a/crates/table/src/panel.rs b/crates/table/src/panel.rs index f50f45cc..3bdd5286 100644 --- a/crates/table/src/panel.rs +++ b/crates/table/src/panel.rs @@ -6,7 +6,7 @@ use gpui_component::{ ActiveTheme, Disableable as _, IconName, Selectable as _, Sizable as _, button::{Button, ButtonVariants as _}, h_flex, - input::{Escape, Input, InputEvent, InputState}, + input::{Escape, Input, InputEvent, InputState, Textarea, TextareaState}, table::{DataTable, TableEvent, TableState}, v_flex, }; @@ -106,14 +106,10 @@ pub struct TablePanel { impl TablePanel { pub fn new(window: &mut Window, cx: &mut Context) -> Self { let editor = cx.new(|cx| { - InputState::new(window, cx) - .multi_line(true) - .submit_on_enter(true) + TextareaState::new(window, cx).submit_on_enter(true) }); let note_editor = cx.new(|cx| { - InputState::new(window, cx) - .multi_line(true) - .submit_on_enter(true) + TextareaState::new(window, cx).submit_on_enter(true) .placeholder("Note") }); let search_input = cx.new(|cx| InputState::new(window, cx).placeholder("Find in table")); @@ -1205,7 +1201,7 @@ pub(crate) fn paste_cells( /// suggestion list) needs a height it can't compute itself. Place the result with /// `deferred(float_at(anchor.origin, within, box))`. pub fn editor_box( - editor: &Entity, + editor: &Entity, anchor: Bounds, within: Bounds, window: &mut Window, @@ -1256,7 +1252,7 @@ pub fn editor_box( .rounded(cx.theme().radius) .shadow_lg() .child( - Input::new(editor) + Textarea::new(editor) .appearance(false) .h_full() .px(px(cell::CELL_PAD_X)) diff --git a/crates/workspace/src/panels/agent.rs b/crates/workspace/src/panels/agent.rs index e365a131..7f42cfce 100644 --- a/crates/workspace/src/panels/agent.rs +++ b/crates/workspace/src/panels/agent.rs @@ -5,7 +5,7 @@ use std::time::{Duration, Instant}; use gpui::prelude::FluentBuilder as _; use gpui::*; -use gpui_component::dock::{DockPlacement, Panel, PanelControl, PanelEvent}; +use gpui_component::dock::{BasePanel, DockPlacement, Panel, PanelEvent}; use gpui_component::menu::{ContextMenuExt as _, PopupMenuItem}; use gpui_component::scroll::ScrollableElement as _; use gpui_component::tab::{Tab, TabBar}; @@ -364,11 +364,22 @@ impl Focusable for AgentPanel { impl EventEmitter for AgentPanel {} -impl Panel for AgentPanel { +impl BasePanel for AgentPanel { fn panel_name(&self) -> &'static str { "AgentPanel" } + // The library always renders the ⋯ menu button; this just empties it of Close. + fn closable(&self, _cx: &App) -> bool { + false + } + + fn zoomable(&self, _cx: &App) -> bool { + true + } +} + +impl Panel for AgentPanel { fn title(&mut self, _w: &mut Window, _cx: &mut Context) -> impl IntoElement { SharedString::from("Agent") } @@ -452,14 +463,6 @@ impl Panel for AgentPanel { ) } - // The library always renders the ⋯ menu button; these just empty it of Close + Zoom. - fn closable(&self, _cx: &App) -> bool { - false - } - - fn zoomable(&self, _cx: &App) -> Option { - None - } } impl Render for AgentPanel { diff --git a/crates/workspace/src/panels/details.rs b/crates/workspace/src/panels/details.rs index 52bfda1e..a9644d07 100644 --- a/crates/workspace/src/panels/details.rs +++ b/crates/workspace/src/panels/details.rs @@ -8,9 +8,9 @@ use gpui::*; use gpui_component::{ ActiveTheme, IconName, Sizable, StyledExt as _, button::{Button, ButtonVariants}, - dock::{DockPlacement, Panel, PanelControl, PanelEvent}, + dock::{BasePanel, DockPlacement, Panel, PanelEvent}, h_flex, - input::{Escape, InputEvent, InputState}, + input::{Escape, InputEvent, TextareaState}, resizable::{resizable_panel, v_resizable}, scroll::ScrollableElement, table::TableState, @@ -64,7 +64,7 @@ pub struct DetailsPanel { _crop_sub: Subscription, /// The field editor, shared across whichever field is open — the same one-per-panel /// arrangement the grid uses for its cell editor. - editor: Entity, + editor: Entity, /// `(source_rows, data_col)` of the field being edited, in the grid's own coordinates so a /// filter change between opening and committing can't redirect the write. Several rows when /// the field belongs to a bundle: one edit box writing the same value down the selection. @@ -105,9 +105,7 @@ impl DetailsPanel { // *wrapped* height, and a single-line input runs the text off the right edge instead. // `submit_on_enter` keeps Enter committing the field (Shift+Enter inserts a newline). let editor = cx.new(|cx| { - InputState::new(window, cx) - .multi_line(true) - .submit_on_enter(true) + TextareaState::new(window, cx).submit_on_enter(true) }); let _editor_sub = cx.subscribe(&editor, |this, _input, event: &InputEvent, cx| { if matches!(event, InputEvent::PressEnter { .. } | InputEvent::Blur) { @@ -462,22 +460,24 @@ impl Focusable for DetailsPanel { impl EventEmitter for DetailsPanel {} -impl Panel for DetailsPanel { +impl BasePanel for DetailsPanel { fn panel_name(&self) -> &'static str { "DetailsPanel" } - fn title(&mut self, _w: &mut Window, _cx: &mut Context) -> impl IntoElement { - SharedString::from("Details") - } - - // The library always renders the ⋯ menu button; these just empty it of Close + Zoom. + // The library always renders the ⋯ menu button; this just empties it of Close. fn closable(&self, _cx: &App) -> bool { false } - fn zoomable(&self, _cx: &App) -> Option { - None + fn zoomable(&self, _cx: &App) -> bool { + true + } +} + +impl Panel for DetailsPanel { + fn title(&mut self, _w: &mut Window, _cx: &mut Context) -> impl IntoElement { + SharedString::from("Details") } } diff --git a/crates/workspace/src/views/mod.rs b/crates/workspace/src/views/mod.rs index d2a8f130..6d80ce79 100644 --- a/crates/workspace/src/views/mod.rs +++ b/crates/workspace/src/views/mod.rs @@ -17,7 +17,7 @@ use gpui_component::table::TableState; use gpui_component::{ ActiveTheme as _, Icon, IconName, Sizable as _, button::Button, - dock::{DockArea, Panel, PanelControl, PanelEvent}, + dock::{BasePanel, DockArea, Panel, PanelEvent}, h_flex, slider::{Slider, SliderEvent, SliderState}, tab::{Tab, TabBar}, @@ -279,11 +279,24 @@ impl Focusable for ViewsPanel { impl EventEmitter for ViewsPanel {} -impl Panel for ViewsPanel { +impl BasePanel for ViewsPanel { fn panel_name(&self) -> &'static str { "ViewsPanel" } + /// Centre panel: not closable, so the main view always keeps its body. + fn closable(&self, _cx: &App) -> bool { + false + } + + /// Zooming the centre is the point of Shift+Esc — it drops the surrounding docks and gives + /// the grid or the gallery the whole window. + fn zoomable(&self, _cx: &App) -> bool { + true + } +} + +impl Panel for ViewsPanel { /// The centre has no name worth showing, so the title cell carries the view switcher instead — /// which puts it at the far left of the same row as the Find button and the ⋯ menu. /// @@ -318,18 +331,6 @@ impl Panel for ViewsPanel { ) } - /// Centre panel: not closable, so the main view always keeps its body. - fn closable(&self, _cx: &App) -> bool { - false - } - - /// A zoom control makes no sense for the main body. Note this *does* render: `zoomable: None` - /// only greys the ⋯ menu's "Zoom In" entry and drops the zoom toolbar button — the ⋯ itself - /// is unconditional in `TabPanel::render_toolbar`. - fn zoomable(&self, _cx: &App) -> Option { - None - } - /// Rendered by `TabPanel::render_toolbar` immediately left of the ⋯ menu, forced to /// `.xsmall().ghost()` by the library. Only the grid has anything to find in. fn toolbar_buttons(&mut self, _w: &mut Window, _cx: &mut Context) -> Option> { From f62367bf13d91cb549ded802362c694f68683ba1 Mon Sep 17 00:00:00 2001 From: devnull03 <56480041+devnull03@users.noreply.github.com> Date: Sat, 5 Sep 2026 22:36:45 -0700 Subject: [PATCH 02/14] feat(workspace): port the dock onto gpui-kit 0.6's pane-tree model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 0.6 dock replaces `DockItem` and the per-side accessors with base's pane-tree model, so this is a port rather than a rename: - `DockArea` is built through `DockSkin::dock_area`, which installs the component appearance as the area's renderer and hands back the handle its settings (the toggle-button visibility) are reached through. - Layouts are described with `DockLayout` and installed with `set_center` / `set_dock` + `set_dock_size`, instead of `DockItem` and `set_*_dock`. - Panels reach the dock as `panel_handle(entity)`. Base stores what it is given and a skin can only recover presentation from the wrapper, so a bare entity would draw its `panel_name` where its title belongs. - `register_panel` builders take a `PanelBuildContext` and return an `Arc`. - The registry reads placements out of the `PaneTree` rather than tracking them by hand, and `move_panel` is now the library's own — which keeps the panel alive and carries its active state across, so the `remove_panel` + `add_panel` workaround and the stale-snapshot warning both go away. - `toggle_dock_immediately` is down to a guard plus `DockArea::toggle_dock`, which collapses the tree and emits `LayoutChanged` itself. The whole workspace compiles: `app` and `project-wizard`, unmeasured behind this crate until now, needed no changes at all. cargo fmt and cargo clippy -D warnings are clean. --- .envrc | 1 + crates/table/src/panel.rs | 7 +- crates/workspace/src/dock_button.rs | 2 +- crates/workspace/src/lib.rs | 117 ++++++------ crates/workspace/src/panel_registry.rs | 243 +++++++++++++------------ crates/workspace/src/panels/agent.rs | 1 - crates/workspace/src/panels/details.rs | 4 +- crates/workspace/src/views/mod.rs | 22 ++- flake.lock | 27 +++ flake.nix | 20 ++ nix/devshell.nix | 50 +++++ shell.nix | 5 + 12 files changed, 309 insertions(+), 190 deletions(-) create mode 100644 .envrc create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 nix/devshell.nix create mode 100644 shell.nix diff --git a/.envrc b/.envrc new file mode 100644 index 00000000..3550a30f --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/crates/table/src/panel.rs b/crates/table/src/panel.rs index 3bdd5286..e362dc5d 100644 --- a/crates/table/src/panel.rs +++ b/crates/table/src/panel.rs @@ -105,11 +105,10 @@ pub struct TablePanel { impl TablePanel { pub fn new(window: &mut Window, cx: &mut Context) -> Self { - let editor = cx.new(|cx| { - TextareaState::new(window, cx).submit_on_enter(true) - }); + let editor = cx.new(|cx| TextareaState::new(window, cx).submit_on_enter(true)); let note_editor = cx.new(|cx| { - TextareaState::new(window, cx).submit_on_enter(true) + TextareaState::new(window, cx) + .submit_on_enter(true) .placeholder("Note") }); let search_input = cx.new(|cx| InputState::new(window, cx).placeholder("Find in table")); diff --git a/crates/workspace/src/dock_button.rs b/crates/workspace/src/dock_button.rs index 2a6c4cb1..a4359b6a 100644 --- a/crates/workspace/src/dock_button.rs +++ b/crates/workspace/src/dock_button.rs @@ -131,7 +131,7 @@ impl Render for DockToggleButton { // points at one panel, which can sit in an open dock and still be hidden behind a sibling // tab — lighting up for that aims the reader at something not on screen. let lit = self.dock.upgrade().is_some_and(|area| match self.toggles { - Toggles::Dock(placement) => area.read(cx).is_dock_open(placement, cx), + Toggles::Dock(placement) => area.read(cx).is_dock_open(placement), Toggles::Panel(meta) => PanelRegistry::visible(meta.name, &area, cx), }); diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index d8dbbe2e..b89c4c1a 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -22,7 +22,8 @@ use std::sync::Arc; use gpui::*; use gpui_component::dock::{ - DockArea, DockAreaState, DockEvent, DockItem, DockPlacement, PanelView, register_panel, + BasePanelView, DockArea, DockAreaState, DockEvent, DockLayout, DockPlacement, DockSkin, + panel_handle, register_panel, }; use settings::AppSettings; @@ -65,27 +66,18 @@ impl Global for BottomDockCrop {} /// Every caller gets the same grid redraw and persistence behavior because the state change owns /// its event. pub(crate) fn toggle_dock_immediately( - area: &DockArea, + area: &mut DockArea, placement: DockPlacement, window: &mut Window, cx: &mut Context, ) -> bool { - let dock = match placement { - DockPlacement::Left => area.left_dock(), - DockPlacement::Bottom => area.bottom_dock(), - DockPlacement::Right => area.right_dock(), - DockPlacement::Center => None, - } - .cloned(); - let Some(dock) = dock else { + // The centre is not a dock, and a dock we never built cannot be toggled. + if placement == DockPlacement::Center || !area.has_dock(placement) { return false; - }; - dock.update(cx, |dock, cx| { - let open = !dock.is_open(); - dock.panel().clone().set_collapsed(!open, window, cx); - dock.set_open(open, window, cx); - }); - cx.emit(DockEvent::LayoutChanged); + } + // Collapsing the panel tree and emitting `LayoutChanged` are both `toggle_dock`'s job now — + // it used to take driving the `Dock` entity by hand. + area.toggle_dock(placement, window, cx); true } @@ -101,21 +93,27 @@ impl Workspace { pub fn new(window: &mut Window, cx: &mut Context) -> Self { // Register panel constructors so a persisted layout can be reconstructed by name. // The centre is the view host, not the grid — the grid is one view it mounts. - register_panel(cx, "ViewsPanel", |weak, _state, _info, window, cx| { - Box::new(cx.new(|cx| ViewsPanel::new(weak, window, cx))) + // `panel_handle` rather than a bare entity: base stores what it is given, and a skin can + // only recover presentation — our titles, the view switcher — from the wrapper. A bare + // entity still docks and persists, it just draws its `panel_name` where the title goes. + register_panel(cx, "ViewsPanel", |ctx, window, cx| { + let weak = ctx.dock_area(); + panel_handle(cx.new(|cx| ViewsPanel::new(weak, window, cx))) }); - register_panel(cx, "DetailsPanel", |_weak, _state, _info, window, cx| { - Box::new(cx.new(|cx| DetailsPanel::new(window, cx))) + register_panel(cx, "DetailsPanel", |_ctx, window, cx| { + panel_handle(cx.new(|cx| DetailsPanel::new(window, cx))) }); - register_panel(cx, "AgentPanel", |_weak, _state, _info, window, cx| { - Box::new(cx.new(|cx| AgentPanel::new(window, cx))) + register_panel(cx, "AgentPanel", |_ctx, window, cx| { + panel_handle(cx.new(|cx| AgentPanel::new(window, cx))) }); - register_panel(cx, "ProblemsPanel", |_weak, _state, _info, window, cx| { - Box::new(cx.new(|cx| ProblemsPanel::new(window, cx))) + register_panel(cx, "ProblemsPanel", |_ctx, window, cx| { + panel_handle(cx.new(|cx| ProblemsPanel::new(window, cx))) }); - let dock_area = - cx.new(|cx| DockArea::new("qrate-main", Some(DOCK_LAYOUT_VERSION), window, cx)); + // The appearance is a separate object in 0.6, installed as the area's renderer. The + // handle it hands back is how its settings are reached afterwards. + let (dock_area, skin) = + DockSkin::dock_area("qrate-main", Some(DOCK_LAYOUT_VERSION), window, cx); // Restore if saved, else build default; building first then loading would orphan a throwaway table. if !Self::restore_layout(&dock_area, window, cx) { @@ -123,57 +121,59 @@ impl Workspace { let centre = cx.new(|cx| ViewsPanel::new(weak.clone(), window, cx)); // Which dock each panel starts in is the panel's own declaration, so they're built // as a flat list and grouped by it rather than one `set_*_dock` call apiece. - let panels: [(&PanelMeta, Arc); 3] = [ + let panels: [(&PanelMeta, Arc); 3] = [ ( &DETAILS_META, - Arc::new(cx.new(|cx| DetailsPanel::new(window, cx))), + panel_handle(cx.new(|cx| DetailsPanel::new(window, cx))), ), ( &PROBLEMS_META, - Arc::new(cx.new(|cx| ProblemsPanel::new(window, cx))), + panel_handle(cx.new(|cx| ProblemsPanel::new(window, cx))), ), ( &AGENT_META, - Arc::new(cx.new(|cx| AgentPanel::new(window, cx))), + panel_handle(cx.new(|cx| AgentPanel::new(window, cx))), ), ]; dock_area.update(cx, |area, cx| { - // `tab`, not `panel`: `DockItem::panel` embeds the centre bare, with no title bar - // at all — and the title bar is where the view switcher lives. Restoring a saved - // layout always produces tabs anyway, so this only fixes the first launch, which - // would otherwise come up with no way to leave the grid. - area.set_center(DockItem::tab(centre, &weak, window, cx), window, cx); - for placement in [ - DockPlacement::Left, - DockPlacement::Right, - DockPlacement::Bottom, + // `tabs`, not a bare panel: a bare centre has no title bar at all — and the title + // bar is where the view switcher lives. Restoring a saved layout always produces + // tabs anyway, so this only fixes the first launch, which would otherwise come up + // with no way to leave the grid. + area.set_center( + DockLayout::tabs().panel_view(panel_handle(centre), cx), + window, + cx, + ); + for (placement, size) in [ + (DockPlacement::Left, px(300.)), + (DockPlacement::Right, px(340.)), + (DockPlacement::Bottom, px(200.)), ] { - let items: Vec> = panels + let mut layout = DockLayout::tabs(); + let mut filled = false; + for (_, view) in panels .iter() .filter(|(meta, _)| meta.default_placement == placement) - .map(|(_, view)| view.clone()) - .collect(); - if items.is_empty() { - continue; + { + layout = layout.panel_view(view.clone(), cx); + filled = true; } - let item = DockItem::tabs(items, &weak, window, cx); - match placement { - DockPlacement::Left => { - area.set_left_dock(item, Some(px(300.)), true, window, cx) - } - DockPlacement::Right => { - area.set_right_dock(item, Some(px(340.)), true, window, cx) - } - _ => area.set_bottom_dock(item, Some(px(200.)), true, window, cx), + if !filled { + continue; } + // Size is a property of the dock now, not an argument to filling it — and + // `set_dock` deliberately preserves the size of whatever it replaces. + area.set_dock(placement, layout, window, cx); + area.set_dock_size(placement, size, window, cx); } }); } // We drive dock open/close from our own title/status-bar buttons, so hide the built-in - // toggle arrows. Done in both paths — `load` doesn't carry this runtime-only flag. - dock_area.update(cx, |area, cx| area.set_toggle_button_visible(false, cx)); + // toggle arrows. A skin setting in 0.6, so it survives `load` without being reapplied. + skin.set_toggle_button_visible(false, cx); // Whichever path ran above built the panels; this is what learns where they landed. PanelRegistry::sync(&dock_area, cx); @@ -351,10 +351,7 @@ fn prune(node: &mut serde_json::Value) { impl Render for Workspace { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { // Bottom dock closed: overhang 29px and clip to hide the library's residual strip; open: sit flush. - let bottom_open = self - .dock_area - .read(cx) - .is_dock_open(DockPlacement::Bottom, cx); + let bottom_open = self.dock_area.read(cx).is_dock_open(DockPlacement::Bottom); let overshoot = if bottom_open { px(0.) } else { diff --git a/crates/workspace/src/panel_registry.rs b/crates/workspace/src/panel_registry.rs index b3f811ac..3dc59658 100644 --- a/crates/workspace/src/panel_registry.rs +++ b/crates/workspace/src/panel_registry.rs @@ -1,17 +1,19 @@ //! Which dock each panel is in, and what it puts in the status bar. //! -//! gpui_component owns the docking; what it has no notion of is a panel that *moves*, or a bar -//! button that follows one. Both need an answer to "where is DetailsPanel right now?", and this -//! is the only place that has one: `DockItem::remove_panel` takes `&self` and never pops from -//! the `DockItem::Tabs` snapshot it removes from, so the library's own view of a dock's contents -//! is wrong from the first move onwards. Never re-derive a placement from `DockItem`. +//! The dock owns the arrangement; what it has no notion of is a bar button that follows a panel +//! around. That needs an answer to "where is DetailsPanel right now?", and this is where it +//! lives — alongside each panel's icon, label and bar side. +//! +//! Unlike the `DockItem` model this replaced, the placement here is a *cache* of something the +//! dock already knows: a `PaneTree` is the single source of truth for what a dock holds, and +//! [`PanelRegistry::sync`] re-reads it rather than tracking moves by hand. use std::sync::Arc; use gpui::*; use gpui_component::{ IconName, - dock::{DockArea, DockEvent, DockItem, DockPlacement, PanelView}, + dock::{BasePanelView, DockArea, DockEvent, DockPlacement, InsertTarget, NodeId, PaneRef}, }; use crate::Workspace; @@ -62,7 +64,7 @@ pub static PANELS: [&PanelMeta; 3] = [&DETAILS_META, &PROBLEMS_META, &AGENT_META pub struct PanelEntry { pub meta: &'static PanelMeta, - pub view: Arc, + pub view: Arc, pub placement: DockPlacement, } @@ -89,34 +91,24 @@ impl PanelRegistry { /// "Dock is open" alone lights a button up for a panel hidden behind its neighbour, which /// points the reader at something they cannot see. /// - /// Read off the live `TabPanel`, not the `DockItem` membership snapshot this module warns - /// about: which tab is active is state the library keeps current. pub fn visible(name: &str, dock_area: &Entity, cx: &App) -> bool { let Some(placement) = Self::placement(name, cx) else { return false; }; let area = dock_area.read(cx); - if !area.is_dock_open(placement, cx) { + if !area.is_dock_open(placement) { return false; } - let dock = match placement { - DockPlacement::Left => area.left_dock(), - DockPlacement::Right => area.right_dock(), - _ => area.bottom_dock(), - }; let mut front = Vec::new(); - if let Some(dock) = dock { - frontmost(dock.read(cx).panel(), cx, &mut front); - } + frontmost(area, placement, cx, &mut front); front.contains(&name) } /// Rebuild from what the dock area actually holds. Called after every layout construction: /// `DockArea::load` builds fresh panel entities from the saved names, so any entry made - /// before it would point at an orphan. Only correct immediately after a load or a default - /// build, while the `DockItem` snapshots still match reality. + /// before it would point at an orphan. pub fn sync(dock_area: &Entity, cx: &mut App) { - let mut found: Vec<(DockPlacement, Arc)> = Vec::new(); + let mut found: Vec<(DockPlacement, Arc)> = Vec::new(); { let area = dock_area.read(cx); for placement in [ @@ -124,14 +116,7 @@ impl PanelRegistry { DockPlacement::Right, DockPlacement::Bottom, ] { - let dock = match placement { - DockPlacement::Left => area.left_dock(), - DockPlacement::Right => area.right_dock(), - _ => area.bottom_dock(), - }; - if let Some(dock) = dock { - collect(dock.read(cx).panel(), placement, &mut found); - } + collect(area, placement, &mut found); } } @@ -166,7 +151,7 @@ impl PanelRegistry { crate::toggle_dock_immediately(area, placement, window, cx) }) } else { - let toggled = if !dock_area.read(cx).is_dock_open(placement, cx) { + let toggled = if !dock_area.read(cx).is_dock_open(placement) { dock_area.update(cx, |area, cx| { crate::toggle_dock_immediately(area, placement, window, cx) }) @@ -184,10 +169,10 @@ impl PanelRegistry { /// Make `name` the tab in front of its dock. /// - /// The library exposes no "activate this tab": `TabPanel::set_active_ix` is private and - /// `add_panel` returns early for a panel already in the strip. `DockItem::active_index` is - /// what is public, and it wants an index into the *live* tab order — which `dump` reports and - /// the `DockItem::Tabs` snapshot beside it does not, for the reason this module opens with. + /// A move onto the panel's own group at its own index: the tree edit is a no-op, so the tab + /// order is untouched and `activate` is the whole point. There is still no public "select + /// this tab" on `DockArea` — `TabGroup::select_tab` needs the group entity, which the area + /// keeps to itself. fn bring_to_front( name: &str, placement: DockPlacement, @@ -195,34 +180,47 @@ impl PanelRegistry { window: &mut Window, cx: &mut App, ) { - let area = dock_area.read(cx); - let dock = match placement { - DockPlacement::Left => area.left_dock(), - DockPlacement::Right => area.right_dock(), - _ => area.bottom_dock(), - }; - let Some(dock) = dock.cloned() else { - return; - }; - let item = dock.read(cx).panel().clone(); - if !matches!(item, DockItem::Tabs { .. }) { - return; - } - let Some(index) = item - .view() - .dump(cx) - .children - .iter() - .position(|child| child.panel_name == name) - else { + let Some((node, ix, panel)) = ({ + let area = dock_area.read(cx); + area.layout(placement).and_then(|tree| { + let mut found = None; + tree.root().walk(&mut |node| { + let PaneRef::Tabs { panels, .. } = node.kind() else { + return; + }; + if found.is_some() { + return; + } + found = panels + .iter() + .position(|id| { + area.panel(*id) + .is_some_and(|panel| panel.panel_name(cx) == name) + }) + .map(|ix| (node.id(), ix, panels[ix])); + }); + found + }) + }) else { return; }; - let item = item.active_index(index, cx); - dock.update(cx, |dock, cx| dock.set_panel(item, window, cx)); + dock_area.update(cx, |area, cx| { + area.move_panel( + panel, + InsertTarget::Tabs { + node, + ix: Some(ix), + activate: true, + }, + window, + cx, + ); + }); } - /// Move a panel to another dock, keeping the panel entity — and so its state — alive. The - /// library has no `move_panel`; `remove_panel` + `add_panel` hand the same `Arc` across. + /// Move a panel to another dock, keeping the panel entity — and so its state — alive. + /// `DockArea::move_panel` is built for exactly this: the panel never leaves the dock, so it + /// is never told it was removed, and its active state carries across. pub fn move_panel( name: &str, to: DockPlacement, @@ -241,9 +239,21 @@ impl PanelRegistry { return; } + let panel = view.panel_id(cx); + let Some(node) = tabs_node(dock_area.read(cx), to) else { + return; + }; dock_area.update(cx, |area, cx| { - area.remove_panel(view.clone(), from, window, cx); - area.add_panel(view, to, None, window, cx); + area.move_panel( + panel, + InsertTarget::Tabs { + node, + ix: None, + activate: true, + }, + window, + cx, + ); }); cx.update_global::(|this, _| { if let Some(entry) = this.0.iter_mut().find(|entry| entry.meta.name == name) { @@ -257,10 +267,10 @@ impl PanelRegistry { dock_area.update(cx, |area, cx| { // An emptied dock keeps rendering as a bare tab strip, and a closed target dock would // swallow the panel we just moved into it. - if source_empty && area.is_dock_open(from, cx) { + if source_empty && area.is_dock_open(from) { crate::toggle_dock_immediately(area, from, window, cx); } - if !area.is_dock_open(to, cx) { + if !area.is_dock_open(to) { crate::toggle_dock_immediately(area, to, window, cx); } }); @@ -269,42 +279,49 @@ impl PanelRegistry { } /// The panels a dock is actually showing — one per tab group, since a split shows several at once. -fn frontmost<'a>(item: &DockItem, cx: &'a App, out: &mut Vec<&'a str>) { - match item { - DockItem::Tabs { view, .. } => out.extend( - view.read(cx) - .active_panel(cx) +fn frontmost(area: &DockArea, placement: DockPlacement, cx: &App, out: &mut Vec<&'static str>) { + let Some(tree) = area.layout(placement) else { + return; + }; + tree.root().walk(&mut |node| { + let PaneRef::Tabs { panels, active_ix } = node.kind() else { + return; + }; + out.extend( + panels + .get(active_ix) + .and_then(|id| area.panel(*id)) .map(|panel| panel.panel_name(cx)), - ), - DockItem::Panel { view, .. } => out.push(view.panel_name(cx)), - DockItem::Split { items, .. } => { - for item in items { - frontmost(item, cx, out); - } - } - DockItem::Tiles { .. } => {} - } + ); + }); } -/// Flatten a dock's item tree into the panels it holds. Recursive because a restored layout can -/// come back as a `Split` even though nothing here builds one. +/// The panels a dock holds, in tree order. A restored layout can come back as a split even +/// though nothing here builds one, so this asks the tree rather than assuming one group. fn collect( - item: &DockItem, + area: &DockArea, placement: DockPlacement, - out: &mut Vec<(DockPlacement, Arc)>, + out: &mut Vec<(DockPlacement, Arc)>, ) { - match item { - DockItem::Tabs { items, .. } => { - out.extend(items.iter().map(|view| (placement, view.clone()))) - } - DockItem::Panel { view, .. } => out.push((placement, view.clone())), - DockItem::Split { items, .. } => { - for item in items { - collect(item, placement, out); - } + let Some(tree) = area.layout(placement) else { + return; + }; + out.extend( + tree.panels() + .filter_map(|id| area.panel(id).map(|panel| (placement, panel.clone()))), + ); +} + +/// The first tab group in a dock, which is where a panel moved there belongs. +fn tabs_node(area: &DockArea, placement: DockPlacement) -> Option { + let tree = area.layout(placement)?; + let mut found = None; + tree.root().walk(&mut |node| { + if found.is_none() && matches!(node.kind(), PaneRef::Tabs { .. }) { + found = Some(node.id()); } - DockItem::Tiles { .. } => {} - } + }); + found } #[cfg(test)] @@ -313,15 +330,14 @@ mod tests { use std::sync::Arc; use gpui::{AppContext as _, TestAppContext}; - use gpui_component::dock::{DockArea, DockItem, DockPlacement, PanelView}; + use gpui_component::dock::{BasePanelView, DockArea, DockLayout, DockPlacement, panel_handle}; use crate::panel_registry::PanelRegistry; use crate::panels::{AgentPanel, DetailsPanel}; use diagnostics::ProblemsPanel; - /// The registry is the only record of where a panel is: the library's `DockItem` snapshot - /// goes stale the moment anything moves. So this covers both halves — that `sync` reads the - /// real arrangement, and that a move updates it and takes the emptied dock down with it. + /// Covers both halves of the registry — that `sync` reads the real arrangement out of the + /// pane tree, and that a move updates it and takes the emptied dock down with it. #[gpui::test] fn a_move_updates_the_placement_and_closes_the_dock_it_emptied(cx: &mut TestAppContext) { cx.update(|cx| { @@ -335,21 +351,24 @@ mod tests { cx.update(|window, cx| { dock_area.update(cx, |area, cx| { - let weak = cx.entity().downgrade(); - let details: Arc = - Arc::new(cx.new(|cx| DetailsPanel::new(window, cx))); - let agent: Arc = Arc::new(cx.new(|cx| AgentPanel::new(window, cx))); - let problems: Arc = - Arc::new(cx.new(|cx| ProblemsPanel::new(window, cx))); - let tabs = |panel, cx: &mut gpui::App, window: &mut gpui::Window| { - DockItem::tabs(vec![panel], &weak, window, cx) - }; - let left = tabs(details, cx, window); - area.set_left_dock(left, None, true, window, cx); - let right = tabs(agent, cx, window); - area.set_right_dock(right, None, true, window, cx); - let bottom = tabs(problems, cx, window); - area.set_bottom_dock(bottom, None, true, window, cx); + let details: Arc = + panel_handle(cx.new(|cx| DetailsPanel::new(window, cx))); + let agent: Arc = + panel_handle(cx.new(|cx| AgentPanel::new(window, cx))); + let problems: Arc = + panel_handle(cx.new(|cx| ProblemsPanel::new(window, cx))); + for (placement, panel) in [ + (DockPlacement::Left, details), + (DockPlacement::Right, agent), + (DockPlacement::Bottom, problems), + ] { + area.set_dock( + placement, + DockLayout::tabs().panel_view(panel, cx), + window, + cx, + ); + } }); PanelRegistry::sync(&dock_area, cx); }); @@ -375,8 +394,8 @@ mod tests { Some(DockPlacement::Right) ); let area = dock_area.read(cx); - assert!(!area.is_dock_open(DockPlacement::Left, cx), "left emptied"); - assert!(area.is_dock_open(DockPlacement::Right, cx), "right open"); + assert!(!area.is_dock_open(DockPlacement::Left), "left emptied"); + assert!(area.is_dock_open(DockPlacement::Right), "right open"); }); } } diff --git a/crates/workspace/src/panels/agent.rs b/crates/workspace/src/panels/agent.rs index 7f42cfce..5532f40b 100644 --- a/crates/workspace/src/panels/agent.rs +++ b/crates/workspace/src/panels/agent.rs @@ -462,7 +462,6 @@ impl Panel for AgentPanel { ), ) } - } impl Render for AgentPanel { diff --git a/crates/workspace/src/panels/details.rs b/crates/workspace/src/panels/details.rs index a9644d07..fd286510 100644 --- a/crates/workspace/src/panels/details.rs +++ b/crates/workspace/src/panels/details.rs @@ -104,9 +104,7 @@ impl DetailsPanel { // `multi_line`, like the grid's cell editor: `editor_box` sizes the box to the value's // *wrapped* height, and a single-line input runs the text off the right edge instead. // `submit_on_enter` keeps Enter committing the field (Shift+Enter inserts a newline). - let editor = cx.new(|cx| { - TextareaState::new(window, cx).submit_on_enter(true) - }); + let editor = cx.new(|cx| TextareaState::new(window, cx).submit_on_enter(true)); let _editor_sub = cx.subscribe(&editor, |this, _input, event: &InputEvent, cx| { if matches!(event, InputEvent::PressEnter { .. } | InputEvent::Blur) { this.commit(cx); diff --git a/crates/workspace/src/views/mod.rs b/crates/workspace/src/views/mod.rs index 6d80ce79..735d528a 100644 --- a/crates/workspace/src/views/mod.rs +++ b/crates/workspace/src/views/mod.rs @@ -498,7 +498,7 @@ mod layout { Some(PanelSlot { name: meta.name.to_string(), placement: placement_name(placement).to_string(), - open: area.is_dock_open(placement, cx), + open: area.is_dock_open(placement), }) }) .collect(); @@ -532,7 +532,7 @@ mod layout { }; let to = parse_placement(&slot.placement, meta.default_placement); PanelRegistry::move_panel(meta.name, to, dock_area, window, cx); - let open = dock_area.read(cx).is_dock_open(to, cx); + let open = dock_area.read(cx).is_dock_open(to); if open != slot.open { dock_area.update(cx, |area, cx| { crate::toggle_dock_immediately(area, to, window, cx) @@ -548,7 +548,7 @@ mod tests { use std::sync::Arc; use gpui::{AppContext as _, TestAppContext}; - use gpui_component::dock::{DockArea, DockItem, DockPlacement, PanelView}; + use gpui_component::dock::{BasePanelView, DockArea, DockLayout, DockPlacement, panel_handle}; use crate::panel_registry::PanelRegistry; use crate::views::{ViewMode, ViewsPanel}; @@ -588,12 +588,16 @@ mod tests { let weak = dock_area.downgrade(); let panel = cx.new(|cx| ViewsPanel::new(weak.clone(), window, cx)); dock_area.update(cx, |area, cx| { - let centre: Arc = Arc::new(panel.clone()); - area.set_center(DockItem::panel(centre), window, cx); - let details: Arc = - Arc::new(cx.new(|cx| crate::panels::DetailsPanel::new(window, cx))); - let left = DockItem::tabs(vec![details], &weak, window, cx); - area.set_left_dock(left, None, true, window, cx); + let centre: Arc = panel_handle(panel.clone()); + area.set_center(DockLayout::tabs().panel_view(centre, cx), window, cx); + let details: Arc = + panel_handle(cx.new(|cx| crate::panels::DetailsPanel::new(window, cx))); + area.set_dock( + DockPlacement::Left, + DockLayout::tabs().panel_view(details, cx), + window, + cx, + ); }); PanelRegistry::sync(&dock_area, cx); panel diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..b9a635fa --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1788614874, + "narHash": "sha256-7QYjT2vHLuX9Z1pdxHXDKCbh1CR3D/2rywB9Tx0MPRg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "c043004d1c6985732bcc1cbc5a9c9aecbbb4e0f0", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..59f31f7b --- /dev/null +++ b/flake.nix @@ -0,0 +1,20 @@ +{ + description = "qrate — a spreadsheet for archival cataloguing, built on GPUI"; + + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + outputs = + { self, nixpkgs }: + let + systems = [ + "x86_64-linux" + "aarch64-linux" + ]; + forAllSystems = nixpkgs.lib.genAttrs systems; + in + { + devShells = forAllSystems (system: { + default = import ./nix/devshell.nix { pkgs = nixpkgs.legacyPackages.${system}; }; + }); + }; +} diff --git a/nix/devshell.nix b/nix/devshell.nix new file mode 100644 index 00000000..dc28e570 --- /dev/null +++ b/nix/devshell.nix @@ -0,0 +1,50 @@ +# The dev shell, shared by flake.nix (`nix develop`) and shell.nix (`nix-shell`) +# so the two entry points cannot drift apart. +{ pkgs }: + +let + # Linked at build time, found through pkg-config. + buildDeps = with pkgs; [ + fontconfig + freetype + alsa-lib + openssl + ]; + + # dlopen'd at run time, so they must also be on the loader path — a build that + # links fine still dies at startup without them. + runtimeDeps = with pkgs; [ + libxkbcommon + wayland + vulkan-loader + libGL + xorg.libX11 + xorg.libxcb + xorg.libXcursor + xorg.libXrandr + xorg.libXi + ]; +in +pkgs.mkShell { + packages = + (with pkgs; [ + cargo + rustc + rustfmt + clippy + rust-analyzer + pkg-config + ]) + ++ buildDeps + ++ runtimeDeps; + + # GPUI resolves its Vulkan driver, Wayland and xkb through dlopen, and `cargo + # run` inherits this shell rather than a Nix wrapper, so the loader path has to + # be set here. + LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath runtimeDeps; + + shellHook = '' + echo "qrate dev shell — cargo $(cargo --version | cut -d' ' -f2)" + echo "PDF and video preview need ./scripts/fetch-binaries.sh" + ''; +} diff --git a/shell.nix b/shell.nix new file mode 100644 index 00000000..fcaf2845 --- /dev/null +++ b/shell.nix @@ -0,0 +1,5 @@ +# Non-flake entry point. `nix develop` is preferred — it pins nixpkgs through +# flake.lock, where this takes whatever channel the caller has. +{ pkgs ? import { } }: + +import ./nix/devshell.nix { inherit pkgs; } From 8ec1a513d88894db22f0a099d438c79ba2e88aa9 Mon Sep 17 00:00:00 2001 From: devnull03 <56480041+devnull03@users.noreply.github.com> Date: Sat, 5 Sep 2026 22:54:46 -0700 Subject: [PATCH 03/14] fix(theme,workspace): finish the 0.6 port and add a Nix dev shell MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 0.6 dropped `accordion.hover.background` from `ThemeConfigColors`, so it comes out of our own theme. The embedded-theme schema test is what caught it — the one key, in the one file that set it. Nothing here draws an accordion, so it only ever fed the library's own hover fallback. - `PanelRegistry::move_panel` gives a dock a tab group before moving into it. `move_panel` needs a node to aim at, and a dock that was never built or that an earlier move emptied has none; `add_panel_view` would make one but does not detach the panel from where it already is. - A Nix dev shell, shared by `flake.nix` and `shell.nix` through `nix/devshell.nix` so the two cannot drift. GPUI links fontconfig, freetype, alsa and openssl through pkg-config and dlopens xkbcommon, wayland, vulkan and the X11 libraries, so `LD_LIBRARY_PATH` is set for `cargo run` too — without pkg-config the build stops in yeslogic-fontconfig-sys, which is what this machine did before the shell existed. cargo fmt --check, cargo clippy -D warnings and cargo test --workspace all pass: 348 tests, no failures. --- CONTRIBUTING.md | 11 ++++++++++- assets/themes/qrate.json | 2 -- crates/workspace/src/panel_registry.rs | 17 +++++++++++++---- nix/devshell.nix | 10 +++++----- 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5a8b8041..eeb934cd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,7 +18,16 @@ cargo run The first build downloads Rust dependencies. The [`sample/`](sample) directory contains a sample collection and images for local testing. qrate builds and runs without optional preview binaries; PDF and video preview coverage needs the tools described in [the development setup guide](docs/dev/SETUP.md). -Linux contributors also need the system libraries listed in [`.github/workflows/ci.yml`](.github/workflows/ci.yml). Google Sheets sign-in is optional for local development and needs local credentials; setup details are in [the development setup guide](docs/dev/SETUP.md). +Linux contributors also need the system libraries listed in [`.github/workflows/ci.yml`](.github/workflows/ci.yml). GPUI links `fontconfig`, `freetype`, `alsa-lib` and `openssl` through `pkg-config` at build time, and `dlopen`s `libxkbcommon`, `wayland`, `vulkan-loader`, `libGL` and the X11 client libraries at run time — a build that links cleanly still fails to start without the second group. Without `pkg-config` on `PATH` the build stops in `yeslogic-fontconfig-sys`. + +On NixOS, or with Nix installed anywhere, the shell in this repository supplies all of that and sets `LD_LIBRARY_PATH` so `cargo run` works: + +```sh +nix develop # flake, pins nixpkgs through flake.lock +nix-shell # same shell without flakes, using your own channel +``` + +Both read [`nix/devshell.nix`](nix/devshell.nix), so they cannot drift apart. Google Sheets sign-in is optional for local development and needs local credentials; setup details are in [the development setup guide](docs/dev/SETUP.md). ## Make a contribution diff --git a/assets/themes/qrate.json b/assets/themes/qrate.json index 7f14945a..40293e44 100644 --- a/assets/themes/qrate.json +++ b/assets/themes/qrate.json @@ -25,7 +25,6 @@ "accent.background": "#f1f3f4", "accent.foreground": "#202124", "accordion.background": "#ffffff", - "accordion.hover.background": "#f8f9fa", "muted.background": "#f1f3f4", "muted.foreground": "#5f6368", @@ -158,7 +157,6 @@ "accent.background": "#404040", "accent.foreground": "#bbbbbb", "accordion.background": "#333333", - "accordion.hover.background": "#383838", "muted.background": "#404040", "muted.foreground": "#777777", diff --git a/crates/workspace/src/panel_registry.rs b/crates/workspace/src/panel_registry.rs index 3dc59658..286d0f03 100644 --- a/crates/workspace/src/panel_registry.rs +++ b/crates/workspace/src/panel_registry.rs @@ -13,7 +13,10 @@ use std::sync::Arc; use gpui::*; use gpui_component::{ IconName, - dock::{BasePanelView, DockArea, DockEvent, DockPlacement, InsertTarget, NodeId, PaneRef}, + dock::{ + BasePanelView, DockArea, DockEvent, DockLayout, DockPlacement, InsertTarget, NodeId, + PaneRef, + }, }; use crate::Workspace; @@ -240,10 +243,16 @@ impl PanelRegistry { } let panel = view.panel_id(cx); - let Some(node) = tabs_node(dock_area.read(cx), to) else { - return; - }; dock_area.update(cx, |area, cx| { + // A dock that was never built, or that a previous move emptied, has no group to + // merge into. Give it one first: `move_panel` wants a node to aim at, and unlike + // `add_panel_view` it detaches the panel from wherever it currently sits. + if tabs_node(area, to).is_none() { + area.set_dock(to, DockLayout::tabs(), window, cx); + } + let Some(node) = tabs_node(area, to) else { + return; + }; area.move_panel( panel, InsertTarget::Tabs { diff --git a/nix/devshell.nix b/nix/devshell.nix index dc28e570..488d28b0 100644 --- a/nix/devshell.nix +++ b/nix/devshell.nix @@ -18,11 +18,11 @@ let wayland vulkan-loader libGL - xorg.libX11 - xorg.libxcb - xorg.libXcursor - xorg.libXrandr - xorg.libXi + libx11 + libxcb + libxcursor + libxrandr + libxi ]; in pkgs.mkShell { From a90afc3491ceb59805218518cb630ffe7c141d5d Mon Sep 17 00:00:00 2001 From: devnull03 <56480041+devnull03@users.noreply.github.com> Date: Sun, 6 Sep 2026 13:31:28 -0700 Subject: [PATCH 04/14] test(workspace,app): cover Shift+Esc panel zoom MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two halves of one shortcut, asserted where each half lives. `workspace` builds the real workspace, focuses a docked panel and dispatches `dock::ToggleZoom`, which is the only way to see the whole path: the skin's frame carries the sole handler, so the action has to travel from the focused panel up to its group. A panel that answers `zoomable() -> false` is skipped in silence — no error, no zoom — and every panel here did exactly that before the 0.6 port, so nothing else would catch a regression back to it. `app` asserts the keymap half: shift-escape resolves to `dock::ToggleZoom` in each context Escape already means something else in, and a bare Escape still means what it did. The two keys share a base key and differ only by the modifier, so a binding that captured Escape here would take the grid selection and the viewer's close with it. --- crates/app/src/actions.rs | 33 +++++++++++++++++++++++++ crates/workspace/src/lib.rs | 48 +++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/crates/app/src/actions.rs b/crates/app/src/actions.rs index 5296050b..29e9dadc 100644 --- a/crates/app/src/actions.rs +++ b/crates/app/src/actions.rs @@ -195,4 +195,37 @@ mod tests { ); } } + + /// Shift+Esc zooms the focused panel, and does it in every context Escape means something + /// else in. The two keys share a base key and differ only by the modifier, so a binding that + /// captured plain Escape here would take the selection and the viewer down with it — and the + /// zoom itself is asserted on the real workspace in `workspace`'s own tests. + #[test] + fn shift_escape_zooms_without_disturbing_plain_escape() { + let keymap = Keymap::new(key_bindings()); + let shift_escape = Keystroke::parse("shift-escape").expect("shift-escape parses"); + for contexts in [ + vec!["DataTable"], + vec!["DetailsPanel"], + vec!["ViewsPanel", "Viewer"], + ] { + let stack: Vec = contexts + .iter() + .map(|context| KeyContext::parse(context).expect("test context parses")) + .collect(); + let (bindings, _) = + keymap.bindings_for_input(std::slice::from_ref(&shift_escape), &stack); + assert_eq!( + bindings.first().map(|binding| binding.action().name()), + Some("dock::ToggleZoom"), + "{contexts:?} zooms on shift-escape" + ); + // The modifier is the whole difference, so plain Escape has to be untouched. + assert_ne!( + escape_action(&contexts), + Some("dock::ToggleZoom"), + "{contexts:?} keeps its own meaning for a bare Escape" + ); + } + } } diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index b89c4c1a..aae318cf 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -510,6 +510,54 @@ mod tests { cx.update(|_, cx| assert_eq!(cx.global::().0, px(29.))); } + /// Shift+Esc, end to end on the real workspace. `app` binds the key to `dock::ToggleZoom` + /// globally and handles it nowhere: the skin's frame carries the only handler, so the action + /// has to travel from the focused panel up to the group holding it. + /// + /// Both halves of the zoom gate are load-bearing here. Every panel in this crate answered + /// `zoomable() -> None` before the 0.6 port, and a panel that still refuses is skipped in + /// silence — no error, no zoom — which is exactly what this asserts against. + #[gpui::test] + fn shift_escape_zooms_the_focused_panel(cx: &mut TestAppContext) { + cx.update(|cx| { + gpui_component::init(cx); + cx.set_global(settings::AppSettings::default()); + cx.set_global(settings::SettingsPersistence::default()); + }); + let (workspace, cx) = cx.add_window_view(Workspace::new); + let dock_area = cx.update(|_, cx| workspace.read(cx).dock_area.clone()); + cx.run_until_parked(); + + // Focus a docked panel, so the action starts from inside a tab group rather than at the + // window root, where nothing would answer it. + cx.update(|window, cx| { + let panel = crate::PanelRegistry::entries(cx) + .iter() + .find(|entry| entry.meta.name == "DetailsPanel") + .map(|entry| entry.view.clone()) + .expect("the default layout docks Details"); + panel.focus_handle(cx).focus(window, cx); + }); + cx.run_until_parked(); + + assert!( + !cx.read(|cx| dock_area.read(cx).is_zoomed()), + "nothing is zoomed to begin with" + ); + + cx.dispatch_action(gpui_component::dock::ToggleZoom); + cx.run_until_parked(); + assert!( + cx.read(|cx| dock_area.read(cx).is_zoomed()), + "the focused panel's group fills the window" + ); + + // Zooming out is never refused, so the same key always gets the docks back. + cx.dispatch_action(gpui_component::dock::ToggleZoom); + cx.run_until_parked(); + assert!(!cx.read(|cx| dock_area.read(cx).is_zoomed())); + } + /// The real thing, lifted out of a project whose left dock came back with an "Unnamed" tab /// beside Details on every launch: an empty `TabPanel` sitting in a tab slot, and an /// `active_index` of 1 that points past the end once it is gone. From 971205ef0f9cdd22aecf290b071f74f87f9a886b Mon Sep 17 00:00:00 2001 From: devnull03 <56480041+devnull03@users.noreply.github.com> Date: Sun, 6 Sep 2026 13:31:36 -0700 Subject: [PATCH 05/14] chore(zed): add Nix and CI task variants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The plain cargo tasks stay exactly as they were — on Windows and macOS the toolchain and system libraries are already on PATH and want no wrapper. Added beside them: "(nix)" variants for Linux, where they usually are not. GPUI links fontconfig, freetype, alsa and openssl through pkg-config and dlopens vulkan, wayland and xkbcommon, so a bare `cargo build` there dies in `yeslogic-fontconfig-sys` and a bare `cargo run` can link and still fail to start. They use `nix-shell` rather than `nix develop` so they work whether or not the nix-command/flakes features are enabled. Also a "CI checks" task running fmt, clippy and test in CI's own order, so the pre-PR gate is one keystroke instead of three remembered commands. --- .zed/tasks.json | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/.zed/tasks.json b/.zed/tasks.json index 77c05b36..d3895146 100644 --- a/.zed/tasks.json +++ b/.zed/tasks.json @@ -1,7 +1,8 @@ // Project tasks — Alt+T opens the picker (see keymap: task::Spawn) // // The check/clippy/test tasks set CARGO_TARGET_DIR so they don't invalidate what -// `cargo run` and rust-analyzer built. See CLAUDE.md ▸ Commands. +// `cargo run` and rust-analyzer built. The "(nix)" tasks supply GPUI's Linux libraries through +// `nix/devshell.nix`; with direnv and nix-direnv, the plain tasks work on Linux too. [ { "label": "cargo run (app)", @@ -31,6 +32,27 @@ "label": "CI (fmt + clippy + test)", "command": "./scripts/ci.sh" }, + { + "label": "cargo run (nix)", + "command": "nix-shell --run 'cargo run -p app'", + "reveal": "always", + "use_new_terminal": false + }, + { + "label": "cargo test (nix)", + "command": "nix-shell --run 'CARGO_TARGET_DIR=target/ci cargo test --workspace'" + }, + { + "label": "CI checks (nix)", + "command": "nix-shell --run './scripts/ci.sh'", + "reveal": "always" + }, + { + "label": "nix dev shell", + "command": "nix-shell", + "reveal": "always", + "use_new_terminal": true + }, { "label": "lazygit", "command": "lazygit" From 6ff98bc4b58bb34d2a454c7c14b10d3226a1ef76 Mon Sep 17 00:00:00 2001 From: devnull03 <56480041+devnull03@users.noreply.github.com> Date: Sun, 6 Sep 2026 13:42:20 -0700 Subject: [PATCH 06/14] fix(logging): keep filtering gpui's noise when it logs with no target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first run on 0.6 put 162 "window not found" errors in one session's log and terminal. The filter that used to demote them was still there and still correct; what changed is that it could no longer tell the messages came from gpui. gpui's error helper derives its log target from the caller's file path by splitting on a `crates/` segment. That segment exists in zed's own checkout, where the file is `crates/gpui/src/window.rs`, so the target came through as `gpui::window`. The published `gpui-pre` unpacks to `gpui-pre-0.3.3/src/`, which has no such segment, so the helper falls back to `""` and every error it logs arrives untargeted. Treats the empty target as one of gpui's. Our own modules always have a target, so nothing of ours starts being swallowed — the tests pin both directions, including that an untargeted error still has to say it is a teardown to be demoted. --- crates/app/src/logging.rs | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/crates/app/src/logging.rs b/crates/app/src/logging.rs index 901c5f1a..a1352a38 100644 --- a/crates/app/src/logging.rs +++ b/crates/app/src/logging.rs @@ -91,8 +91,18 @@ fn version() -> &'static str { /// a level that makes every pasted bug report open with three errors nobody can act on. struct QuietGpuiNoise(Box); +/// Whether `target` is one gpui logs under. +/// +/// The empty string is one of them. gpui's error helper builds the target from the caller's file +/// path by splitting on a `crates/` segment — which exists in zed's own checkout and not in the +/// published `gpui-pre`, unpacked as `…/gpui-pre-0.3.3/src/…`, so every error it logs arrives +/// with no target at all. Our own modules always have one, so nothing of ours is caught here. +fn logged_by_gpui(target: &str) -> bool { + target.starts_with("gpui") || target.is_empty() +} + fn is_accessibility_focus_noise(record: &Record) -> bool { - if !record.target().starts_with("gpui::window") { + if !logged_by_gpui(record.target()) { return false; } let message = record.args().to_string(); @@ -100,7 +110,7 @@ fn is_accessibility_focus_noise(record: &Record) -> bool { } fn is_window_teardown(record: &Record) -> bool { - if record.level() != Level::Error || !record.target().starts_with("gpui") { + if record.level() != Level::Error || !logged_by_gpui(record.target()) { return false; } let message = record.args().to_string(); @@ -297,12 +307,18 @@ mod tests { error, "Invalid window handle (0x80040102)" )); + // The published `gpui-pre` logs with no target at all: gpui's error helper derives one + // from a `crates/` path segment that only exists in zed's own checkout. Missing this case + // put 162 of these in one session's log the first time the app ran on 0.6. + assert!(teardown("", error, "window not found")); assert!(!teardown( "gpui_windows::platform", error, "DirectX device lost" )); + // An untargeted error still has to *say* it is a teardown to be demoted. + assert!(!teardown("", error, "DirectX device lost")); assert!(!teardown("app::logging", error, "window not found")); assert!(!teardown( "gpui::window", @@ -335,6 +351,11 @@ mod tests { "app::window::a11y", "a11y: focused element (FocusId(6v1)) has no accessibility node" )); + // Untargeted, for the same reason the teardown filter accepts it. + assert!(noisy( + "", + "a11y: focused element (FocusId(6v1)) has no accessibility node" + )); } /// The dump is only useful if every section is actually in it — a report missing the version From 7ed2f66d45af0b718ee1e03e817caff5a512b126 Mon Sep 17 00:00:00 2001 From: devnull03 <56480041+devnull03@users.noreply.github.com> Date: Sun, 6 Sep 2026 13:51:59 -0700 Subject: [PATCH 07/14] docs(table): correct the cell editor's stale single-line comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The field has been multi-line since long before this branch — `TablePanel` built it with `.multi_line(true)` — so the comment was already wrong; naming the type `TextareaState` only made the contradiction visible. Says what it is and why, since the reason is not obvious from the type: a long value has to wrap inside the editor box rather than run off its right edge, which is what `editor_box` sizes for. Found by Copilot's review on #118. --- crates/table/src/delegate.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/table/src/delegate.rs b/crates/table/src/delegate.rs index 9d8c584b..c8a528f3 100644 --- a/crates/table/src/delegate.rs +++ b/crates/table/src/delegate.rs @@ -63,7 +63,9 @@ pub struct QrateTableDelegate { /// common case: one selected row lives in `selection` alone and never reaches this set. pub(crate) selected_rows: BTreeSet, pub(crate) editing: EditState, - /// Shared single-line editor, reused across whichever cell is being edited. + /// Shared cell editor, reused across whichever cell is being edited. Multi-line so a long + /// value wraps inside the editor box instead of running off its right edge; Enter still + /// commits it, and Shift+Enter is what inserts a newline. pub(crate) editor: Entity, /// Which cell/row/column the note editor is open on, if any. pub(crate) note_edit: Option, From 960a9b6036b9093989e802bf6d3e4ba4b429c729 Mon Sep 17 00:00:00 2001 From: devnull03 <56480041+devnull03@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:24:45 -0700 Subject: [PATCH 08/14] fix(project-wizard): create blank rows from files --- crates/project-wizard/src/data.rs | 24 ++++++++ crates/project-wizard/src/steps/files.rs | 17 +++++- crates/project-wizard/src/steps/review.rs | 70 ++++++++++++++++++----- crates/project-wizard/src/wizard.rs | 7 ++- 4 files changed, 101 insertions(+), 17 deletions(-) diff --git a/crates/project-wizard/src/data.rs b/crates/project-wizard/src/data.rs index 86b5bd94..7e772697 100644 --- a/crates/project-wizard/src/data.rs +++ b/crates/project-wizard/src/data.rs @@ -229,6 +229,16 @@ pub fn list_files(folder: &str, recursive: bool) -> Result, FolderEr Ok(files) } +/// Every file in a folder-backed blank project is new collection material, so all of them become +/// otherwise-empty rows rather than being matched against spreadsheet rows that do not exist. +pub fn inventory_folder(folder: &str, recursive: bool) -> Result { + Ok(FolderMatch { + matched_rows: 0, + total_rows: 0, + extra_files: list_files(folder, recursive)?, + }) +} + /// Matches spreadsheet rows against files in `folder` by looking for any cell whose value names a /// file there — by filename, filename stem, or the id a multi-part item's files are built from /// ([`settings::filenames::keys`], the same rule `table::photos` resolves rows with once the @@ -533,6 +543,20 @@ mod tests { assert_eq!(deep, ["nested.jpg", "top.jpg"]); } + #[test] + fn blank_project_inventory_turns_every_file_into_an_extra() { + let dir = tempdir("qrate-blank-folder"); + fs::create_dir_all(dir.join("batch")).unwrap(); + fs::write(dir.join("one.jpg"), "x").unwrap(); + fs::write(dir.join("batch").join("two.png"), "x").unwrap(); + + let mut inventory = inventory_folder(dir.to_str().unwrap(), true).unwrap(); + inventory.extra_files.sort(); + assert_eq!(inventory.matched_rows, 0); + assert_eq!(inventory.total_rows, 0); + assert_eq!(inventory.extra_files, ["one.jpg", "two.png"]); + } + #[test] fn folder_errors() { let csv = sample_dir().join("aderman_collection.csv"); diff --git a/crates/project-wizard/src/steps/files.rs b/crates/project-wizard/src/steps/files.rs index b8e6b5e1..bd6c2147 100644 --- a/crates/project-wizard/src/steps/files.rs +++ b/crates/project-wizard/src/steps/files.rs @@ -108,7 +108,10 @@ impl ProjectWizard { data::match_folder(preview, &self.folder_path, self.recurse_subfolders) }) } - EntryKind::Blank => None, + EntryKind::Blank => Some(data::inventory_folder( + &self.folder_path, + self.recurse_subfolders, + )), }; match result { Some(Ok(m)) => { @@ -263,7 +266,7 @@ impl ProjectWizard { .text_sm() .text_color(cx.theme().muted_foreground), ) - .child(self.folder_field("browse-folder-blank", false, cx)) + .child(self.folder_field("browse-folder-blank", true, cx)) } fn render_local_files( @@ -389,6 +392,16 @@ impl ProjectWizard { fn render_folder_status(&self) -> AnyElement { match (&self.folder_match, &self.folder_error) { + (Some(m), _) if self.entry_kind == EntryKind::Blank => inline_message( + "folder-status", + format!( + "{} file{} will become table rows", + m.extra_files.len(), + if m.extra_files.len() == 1 { "" } else { "s" }, + ), + MsgKind::Success, + ) + .into_any_element(), (Some(m), _) if m.matched_rows == m.total_rows => inline_message( "folder-status", format!("{} of {} files matched", m.matched_rows, m.total_rows), diff --git a/crates/project-wizard/src/steps/review.rs b/crates/project-wizard/src/steps/review.rs index b598c9b2..840ab17b 100644 --- a/crates/project-wizard/src/steps/review.rs +++ b/crates/project-wizard/src/steps/review.rs @@ -139,6 +139,25 @@ fn project_columns( columns } +fn append_extra_file_rows( + headers: &[String], + rows: &mut Vec>, + file_column: Option<&str>, + folder_match: Option<&crate::data::FolderMatch>, +) { + let Some((files, file_col)) = folder_match + .map(|matched| matched.extra_files.as_slice()) + .zip(file_column.and_then(|name| headers.iter().position(|header| header == name))) + else { + return; + }; + rows.extend(files.iter().map(|file| { + let mut row = vec![String::new(); headers.len()]; + row[file_col] = file.clone(); + row + })); +} + impl ProjectWizard { pub(crate) fn create_project(&mut self, window: &mut Window, cx: &mut Context) { let name = self.project_name(cx); @@ -170,18 +189,13 @@ impl ProjectWizard { // A file in the folder that no row names is still part of the collection. It arrives as // its own row, empty but for the filename, so it can be catalogued in qrate instead of // being noticed only when someone counts the folder. - if !self.skip_files - && let Some(m) = &self.folder_match - && let Some(file_col) = self - .file_column - .as_deref() - .and_then(|name| headers.iter().position(|h| h == name)) - { - rows.extend(m.extra_files.iter().map(|file| { - let mut row = vec![String::new(); headers.len()]; - row[file_col] = file.clone(); - row - })); + if !self.skip_files { + append_extra_file_rows( + &headers, + &mut rows, + self.file_column.as_deref(), + self.folder_match.as_ref(), + ); } // A project with no rows is a grid with nothing to type into — the first row has to be // created before anything else can be, so create it here rather than making the archivist @@ -293,6 +307,13 @@ impl ProjectWizard { let files_line = (!self.skip_files) .then(|| { self.folder_match.as_ref().map(|m| { + if self.entry_kind == EntryKind::Blank { + return format!( + "{} file{} · each becomes a table row", + m.extra_files.len(), + if m.extra_files.len() == 1 { "" } else { "s" }, + ); + } let method = match self.link_method { LinkMethod::ExactFilename => "exact filename", LinkMethod::CustomPattern => "custom pattern", @@ -360,9 +381,9 @@ impl ProjectWizard { mod tests { use settings::columns::ColumnType; - use crate::data::{ColumnConfigEntry, ColumnConfigPreview}; + use crate::data::{ColumnConfigEntry, ColumnConfigPreview, FolderMatch}; - use super::project_columns; + use super::{append_extra_file_rows, project_columns}; fn roles(columns: &[crate::project::ProjectColumn]) -> Vec<(&str, ColumnType)> { columns @@ -421,4 +442,25 @@ mod tests { ] ); } + + #[test] + fn blank_folder_files_become_rows_in_the_file_column() { + let headers = vec!["Title".into(), "File".into()]; + let mut rows = Vec::new(); + let folder = FolderMatch { + matched_rows: 0, + total_rows: 0, + extra_files: vec!["one.jpg".into(), "two.png".into()], + }; + + append_extra_file_rows(&headers, &mut rows, Some("File"), Some(&folder)); + + assert_eq!( + rows, + [ + vec![String::new(), "one.jpg".into()], + vec![String::new(), "two.png".into()], + ] + ); + } } diff --git a/crates/project-wizard/src/wizard.rs b/crates/project-wizard/src/wizard.rs index 723af6a7..23df8599 100644 --- a/crates/project-wizard/src/wizard.rs +++ b/crates/project-wizard/src/wizard.rs @@ -326,7 +326,12 @@ impl ProjectWizard { } Ok(()) } - EntryKind::Blank => Ok(()), + EntryKind::Blank => { + if !self.skip_files && self.folder_match.is_none() { + return Err("Choose a files folder, or say you'll add one later".into()); + } + Ok(()) + } }, WizardStep::Link => { if self.link_method == LinkMethod::CustomPattern From 62304a21fa08fd7a994b917f5251873dd1795e3e Mon Sep 17 00:00:00 2001 From: devnull03 <56480041+devnull03@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:24:45 -0700 Subject: [PATCH 09/14] fix(workspace): constrain tool panel dragging --- crates/workspace/src/lib.rs | 170 +++++++++++++++++++++++-- crates/workspace/src/panel_registry.rs | 82 +++++++++++- 2 files changed, 241 insertions(+), 11 deletions(-) diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index aae318cf..f10051db 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -175,17 +175,26 @@ impl Workspace { // toggle arrows. A skin setting in 0.6, so it survives `load` without being reapplied. skin.set_toggle_button_visible(false, cx); + // Saved layouts from older builds may contain arrangements qrate no longer permits. + PanelRegistry::enforce_edge_tabs(&dock_area, window, cx); // Whichever path ran above built the panels; this is what learns where they landed. PanelRegistry::sync(&dock_area, cx); - // Every dock mutation emits `LayoutChanged`, which persists it and repaints the workspace. - let _layout_sub = cx.subscribe(&dock_area, |_this, area, event: &DockEvent, cx| { - if matches!(event, DockEvent::LayoutChanged) { - Self::persist_layout(&area, cx); - // Re-render the workspace so the bottom-strip crop tracks the dock's open state. - cx.notify(); - } - }); + // Every dock mutation emits `LayoutChanged`, which normalizes user drags, refreshes the + // status-bar placement cache, persists the result and repaints the workspace. + let _layout_sub = cx.subscribe_in( + &dock_area, + window, + |_this, area, event: &DockEvent, window, cx| { + if matches!(event, DockEvent::LayoutChanged) { + PanelRegistry::enforce_edge_tabs(area, window, cx); + PanelRegistry::sync(area, cx); + Self::persist_layout(area, cx); + // Re-render the workspace so the bottom-strip crop tracks the dock's open state. + cx.notify(); + } + }, + ); let _viewer_sub = cx.observe_global::(|_this, cx| cx.notify()); @@ -398,10 +407,15 @@ mod tests { ParentElement as _, Render, Styled as _, TestAppContext, VisualTestContext, actions, div, point, px, }; - use gpui_component::dock::{DockEvent, DockPlacement}; use gpui_component::menu::ContextMenuExt as _; + use gpui_component::{ + Placement, + dock::{DockEvent, DockPlacement, InsertTarget, PaneRef}, + }; - use crate::{BottomDockCrop, Workspace, prune, toggle_dock_immediately}; + use crate::{ + BarSide, BottomDockCrop, PanelRegistry, Workspace, bar_side, prune, toggle_dock_immediately, + }; actions!(workspace_context_menu_test, [CloseMenu]); @@ -510,6 +524,142 @@ mod tests { cx.update(|_, cx| assert_eq!(cx.global::().0, px(29.))); } + #[gpui::test] + fn dragged_tools_become_edge_tabs_and_move_their_status_item(cx: &mut TestAppContext) { + cx.update(|cx| { + gpui_component::init(cx); + cx.set_global(settings::AppSettings::default()); + cx.set_global(settings::SettingsPersistence::default()); + }); + let (workspace, cx) = cx.add_window_view(Workspace::new); + let dock_area = cx.update(|_, cx| workspace.read(cx).dock_area.clone()); + cx.run_until_parked(); + + // Model a drop on the lower half of Details: gpui-kit first creates a split. + cx.update(|window, cx| { + let agent = PanelRegistry::entries(cx) + .iter() + .find(|entry| entry.meta.name == "AgentPanel") + .map(|entry| entry.view.panel_id(cx)) + .expect("Agent is docked"); + let details_group = dock_area + .read(cx) + .layout(DockPlacement::Left) + .expect("left dock") + .root() + .id(); + dock_area.update(cx, |area, cx| { + area.move_panel( + agent, + InsertTarget::Split { + node: details_group, + placement: Placement::Bottom, + size: None, + }, + window, + cx, + ); + }); + }); + cx.run_until_parked(); + + cx.update(|_, cx| { + let area = dock_area.read(cx); + assert!( + matches!( + area.layout(DockPlacement::Left) + .expect("left dock") + .root() + .kind(), + PaneRef::Tabs { .. } + ), + "the split drop is folded into the edge's tab group" + ); + assert_eq!( + PanelRegistry::placement("AgentPanel", cx), + Some(DockPlacement::Left) + ); + }); + + // A normal tab drop into another edge stays there and refreshes the status-bar cache. + cx.update(|window, cx| { + let agent = PanelRegistry::entries(cx) + .iter() + .find(|entry| entry.meta.name == "AgentPanel") + .map(|entry| entry.view.panel_id(cx)) + .expect("Agent is docked"); + let bottom_group = dock_area + .read(cx) + .layout(DockPlacement::Bottom) + .expect("bottom dock") + .root() + .id(); + dock_area.update(cx, |area, cx| { + area.move_panel( + agent, + InsertTarget::Tabs { + node: bottom_group, + ix: None, + activate: true, + }, + window, + cx, + ); + }); + }); + cx.run_until_parked(); + + cx.update(|_, cx| { + let placement = + PanelRegistry::placement("AgentPanel", cx).expect("Agent stays registered"); + assert_eq!(placement, DockPlacement::Bottom); + assert!(matches!(bar_side(placement), BarSide::Centre)); + }); + + // The document centre is not a tool dock; a drop there returns to the previous edge. + cx.update(|window, cx| { + let agent = PanelRegistry::entries(cx) + .iter() + .find(|entry| entry.meta.name == "AgentPanel") + .map(|entry| entry.view.panel_id(cx)) + .expect("Agent is docked"); + let centre_group = dock_area + .read(cx) + .layout(DockPlacement::Center) + .expect("centre layout") + .root() + .id(); + dock_area.update(cx, |area, cx| { + area.move_panel( + agent, + InsertTarget::Split { + node: centre_group, + placement: Placement::Right, + size: None, + }, + window, + cx, + ); + }); + }); + cx.run_until_parked(); + + cx.update(|_, cx| { + assert_eq!( + PanelRegistry::placement("AgentPanel", cx), + Some(DockPlacement::Bottom) + ); + let area = dock_area.read(cx); + let centre_names = area + .layout(DockPlacement::Center) + .expect("centre layout") + .panels() + .filter_map(|id| area.panel(id).map(|panel| panel.panel_name(cx))) + .collect::>(); + assert_eq!(centre_names, ["ViewsPanel"]); + }); + } + /// Shift+Esc, end to end on the real workspace. `app` binds the key to `dock::ToggleZoom` /// globally and handles it nowhere: the skin's frame carries the only handler, so the action /// has to travel from the focused panel up to the group holding it. diff --git a/crates/workspace/src/panel_registry.rs b/crates/workspace/src/panel_registry.rs index 286d0f03..bcf325a8 100644 --- a/crates/workspace/src/panel_registry.rs +++ b/crates/workspace/src/panel_registry.rs @@ -8,7 +8,7 @@ //! dock already knows: a `PaneTree` is the single source of truth for what a dock holds, and //! [`PanelRegistry::sync`] re-reads it rather than tracking moves by hand. -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; use gpui::*; use gpui_component::{ @@ -140,6 +140,86 @@ impl PanelRegistry { cx.set_global(Self(entries)); } + /// Keep qrate's tool panels in one tab group per edge dock. + /// + /// gpui-kit exposes only a whole-layout drag lock. qrate keeps dragging enabled, then folds a + /// split drop back into tabs and returns a centre drop to the panel's previous edge. + pub fn enforce_edge_tabs(dock_area: &Entity, window: &mut Window, cx: &mut App) { + let previous: HashMap<&str, DockPlacement> = Self::entries(cx) + .iter() + .map(|entry| (entry.meta.name, entry.placement)) + .collect(); + let centre_moves = { + let area = dock_area.read(cx); + area.layout(DockPlacement::Center) + .into_iter() + .flat_map(|tree| tree.panels()) + .filter_map(|id| { + let name = area.panel(id)?.panel_name(cx); + let meta = PANELS.iter().find(|meta| meta.name == name)?; + let destination = previous + .get(name) + .copied() + .filter(|placement| *placement != DockPlacement::Center) + .unwrap_or(meta.default_placement); + Some((id, destination)) + }) + .collect::>() + }; + + dock_area.update(cx, |area, cx| { + for (panel, destination) in centre_moves { + if tabs_node(area, destination).is_none() { + area.set_dock(destination, DockLayout::tabs(), window, cx); + } + if let Some(node) = tabs_node(area, destination) { + area.move_panel( + panel, + InsertTarget::Tabs { + node, + ix: None, + activate: true, + }, + window, + cx, + ); + } + } + + for placement in [ + DockPlacement::Left, + DockPlacement::Right, + DockPlacement::Bottom, + ] { + let Some(tree) = area.layout(placement) else { + continue; + }; + if matches!(tree.root().kind(), PaneRef::Tabs { .. }) { + continue; + } + let Some(target) = tabs_node(area, placement) else { + continue; + }; + let panels = tree + .panels() + .filter(|panel| tree.find_panel_node(*panel) != Some(target)) + .collect::>(); + for panel in panels { + area.move_panel( + panel, + InsertTarget::Tabs { + node: target, + ix: None, + activate: true, + }, + window, + cx, + ); + } + } + }); + } + /// Show a panel, or put it away if it is already the one in front. /// /// "In front", not "its dock is open": where two panels share a dock, clicking the hidden From 407e8823fed9c1b341fc04685e2a1810d06ee27f Mon Sep 17 00:00:00 2001 From: devnull03 <56480041+devnull03@users.noreply.github.com> Date: Sun, 6 Sep 2026 20:56:23 -0700 Subject: [PATCH 10/14] fix(workspace): preserve focus across panel transitions --- crates/table/src/panel.rs | 9 +- crates/workspace/src/lib.rs | 76 ++++++++++++++ crates/workspace/src/panel_registry.rs | 69 +++++++++++- crates/workspace/src/panels/details.rs | 20 +++- crates/workspace/src/viewer/mod.rs | 139 ++++++++++++++++++------- crates/workspace/src/views/gallery.rs | 2 +- crates/workspace/src/views/mod.rs | 7 +- 7 files changed, 276 insertions(+), 46 deletions(-) diff --git a/crates/table/src/panel.rs b/crates/table/src/panel.rs index e362dc5d..1e6102a5 100644 --- a/crates/table/src/panel.rs +++ b/crates/table/src/panel.rs @@ -177,12 +177,14 @@ impl TablePanel { // Notes are not `PROJECT_DATA`: `set_note` writes `__notes` itself, so this must not also // trip the table autosave (which would rewrite the whole dataset for a typed comment). let note_state = state.clone(); - let _note_sub = cx.subscribe( + let _note_sub = cx.subscribe_in( ¬e_editor, - move |_this, editor, event: &InputEvent, cx| { + window, + move |this, editor, event: &InputEvent, window, cx| { if !matches!(event, InputEvent::PressEnter { .. } | InputEvent::Blur) { return; } + let by_enter = matches!(event, InputEvent::PressEnter { .. }); let message = editor.read(cx).value(); note_state.update(cx, |state, cx| { let Some(location) = state.delegate_mut().note_edit.take() else { @@ -191,6 +193,9 @@ impl TablePanel { diagnostics::Diagnostics::set_note(location, message, cx); cx.notify(); }); + if by_enter { + this.focus_table(window, cx); + } }, ); diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index f10051db..5056e7da 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -179,6 +179,8 @@ impl Workspace { PanelRegistry::enforce_edge_tabs(&dock_area, window, cx); // Whichever path ran above built the panels; this is what learns where they landed. PanelRegistry::sync(&dock_area, cx); + // A new main window starts in its document, not on the outer action-routing shell. + PanelRegistry::focus_frontmost(DockPlacement::Center, &dock_area, window, cx); // Every dock mutation emits `LayoutChanged`, which normalizes user drags, refreshes the // status-bar placement cache, persists the result and repaints the workspace. @@ -217,9 +219,19 @@ impl Workspace { window: &mut Window, cx: &mut Context, ) { + let was_open = self.dock_area.read(cx).is_dock_open(placement); + let closing_focus = + PanelRegistry::placement_contains_focus(placement, &self.dock_area, window, cx); self.dock_area.update(cx, |area, cx| { toggle_dock_immediately(area, placement, window, cx); }); + if was_open { + if closing_focus { + PanelRegistry::focus_frontmost(DockPlacement::Center, &self.dock_area, window, cx); + } + } else { + PanelRegistry::focus_frontmost(placement, &self.dock_area, window, cx); + } } /// Serializes the dock state into the open project's `.qrate` (debounced, @@ -249,6 +261,7 @@ impl Workspace { if Self::restore_layout(&self.dock_area, window, cx) { // `load` rebuilt every panel from its saved name, so the old entries are orphans. PanelRegistry::sync(&self.dock_area, cx); + PanelRegistry::focus_frontmost(DockPlacement::Center, &self.dock_area, window, cx); } } @@ -524,6 +537,69 @@ mod tests { cx.update(|_, cx| assert_eq!(cx.global::().0, px(29.))); } + #[gpui::test] + fn opening_a_dock_focuses_it_and_closing_it_returns_to_the_document(cx: &mut TestAppContext) { + cx.update(|cx| { + gpui_component::init(cx); + cx.set_global(settings::AppSettings::default()); + cx.set_global(settings::SettingsPersistence::default()); + }); + let (workspace, cx) = cx.add_window_view(Workspace::new); + let dock_area = cx.update(|_, cx| workspace.read(cx).dock_area.clone()); + cx.run_until_parked(); + + cx.update(|window, cx| { + PanelRegistry::focus_frontmost(DockPlacement::Center, &dock_area, window, cx); + workspace.update(cx, |workspace, cx| { + workspace.toggle_dock(DockPlacement::Left, window, cx) + }); + assert!( + PanelRegistry::placement_contains_focus( + DockPlacement::Center, + &dock_area, + window, + cx, + ), + "closing an unfocused dock leaves document focus alone" + ); + workspace.update(cx, |workspace, cx| { + workspace.toggle_dock(DockPlacement::Left, window, cx) + }); + assert!( + PanelRegistry::placement_contains_focus( + DockPlacement::Left, + &dock_area, + window, + cx, + ), + "opening a dock focuses its active panel" + ); + }); + + cx.dispatch_action(gpui_component::dock::ToggleZoom); + cx.run_until_parked(); + assert!( + cx.read(|cx| dock_area.read(cx).is_zoomed()), + "Shift+Esc zooms the dock opened by shortcut" + ); + cx.dispatch_action(gpui_component::dock::ToggleZoom); + + cx.update(|window, cx| { + workspace.update(cx, |workspace, cx| { + workspace.toggle_dock(DockPlacement::Left, window, cx) + }); + assert!( + PanelRegistry::placement_contains_focus( + DockPlacement::Center, + &dock_area, + window, + cx, + ), + "closing the focused dock returns focus to the document" + ); + }); + } + #[gpui::test] fn dragged_tools_become_edge_tabs_and_move_their_status_item(cx: &mut TestAppContext) { cx.update(|cx| { diff --git a/crates/workspace/src/panel_registry.rs b/crates/workspace/src/panel_registry.rs index bcf325a8..f808ab86 100644 --- a/crates/workspace/src/panel_registry.rs +++ b/crates/workspace/src/panel_registry.rs @@ -90,6 +90,39 @@ impl PanelRegistry { .map(|entry| entry.placement) } + pub fn focus_frontmost( + placement: DockPlacement, + dock_area: &Entity, + window: &mut Window, + cx: &mut App, + ) -> bool { + let panel = { + let area = dock_area.read(cx); + frontmost_panel(area, placement) + }; + if let Some(panel) = panel { + panel.focus_handle(cx).focus(window, cx); + true + } else { + false + } + } + + pub fn placement_contains_focus( + placement: DockPlacement, + dock_area: &Entity, + window: &Window, + cx: &App, + ) -> bool { + let area = dock_area.read(cx); + area.layout(placement).is_some_and(|tree| { + tree.panels().any(|id| { + area.panel(id) + .is_some_and(|panel| panel.focus_handle(cx).contains_focused(window, cx)) + }) + }) + } + /// Whether this panel is actually on screen — its dock is open *and* it is the tab in front. /// "Dock is open" alone lights a button up for a panel hidden behind its neighbour, which /// points the reader at something they cannot see. @@ -229,7 +262,13 @@ impl PanelRegistry { let Some(placement) = Self::placement(name, cx) else { return; }; - let toggled = if Self::visible(name, dock_area, cx) { + let was_visible = Self::visible(name, dock_area, cx); + let closing_focus = was_visible + && Self::entries(cx) + .iter() + .find(|entry| entry.meta.name == name) + .is_some_and(|entry| entry.view.focus_handle(cx).contains_focused(window, cx)); + let toggled = if was_visible { dock_area.update(cx, |area, cx| { crate::toggle_dock_immediately(area, placement, window, cx) }) @@ -248,6 +287,16 @@ impl PanelRegistry { if !toggled { dock_area.update(cx, |_, cx| cx.emit(DockEvent::LayoutChanged)); } + if was_visible { + if closing_focus { + Self::focus_frontmost(DockPlacement::Center, dock_area, window, cx); + } + } else if let Some(entry) = Self::entries(cx) + .iter() + .find(|entry| entry.meta.name == name) + { + entry.view.focus_handle(cx).focus(window, cx); + } } /// Make `name` the tab in front of its dock. @@ -385,6 +434,24 @@ fn frontmost(area: &DockArea, placement: DockPlacement, cx: &App, out: &mut Vec< }); } +fn frontmost_panel(area: &DockArea, placement: DockPlacement) -> Option> { + let tree = area.layout(placement)?; + let mut found = None; + tree.root().walk(&mut |node| { + if found.is_some() { + return; + } + let PaneRef::Tabs { panels, active_ix } = node.kind() else { + return; + }; + found = panels + .get(active_ix) + .and_then(|id| area.panel(*id)) + .cloned(); + }); + found +} + /// The panels a dock holds, in tree order. A restored layout can come back as a split even /// though nothing here builds one, so this asks the tree rather than assuming one group. fn collect( diff --git a/crates/workspace/src/panels/details.rs b/crates/workspace/src/panels/details.rs index fd286510..a0e07c78 100644 --- a/crates/workspace/src/panels/details.rs +++ b/crates/workspace/src/panels/details.rs @@ -105,11 +105,20 @@ impl DetailsPanel { // *wrapped* height, and a single-line input runs the text off the right edge instead. // `submit_on_enter` keeps Enter committing the field (Shift+Enter inserts a newline). let editor = cx.new(|cx| TextareaState::new(window, cx).submit_on_enter(true)); - let _editor_sub = cx.subscribe(&editor, |this, _input, event: &InputEvent, cx| { - if matches!(event, InputEvent::PressEnter { .. } | InputEvent::Blur) { + let _editor_sub = cx.subscribe_in( + &editor, + window, + |this, _input, event: &InputEvent, window, cx| { + if !matches!(event, InputEvent::PressEnter { .. } | InputEvent::Blur) { + return; + } + let by_enter = matches!(event, InputEvent::PressEnter { .. }); this.commit(cx); - } - }); + if by_enter { + this.focus_handle.focus(window, cx); + } + }, + ); let mut this = Self { focus_handle: cx.focus_handle(), state: None, @@ -549,10 +558,11 @@ fn render_image_frame( .ghost() .small() .tooltip("View fullscreen") - .on_click(move |_, _, cx| { + .on_click(move |_, window, cx| { crate::open_viewer( path.clone(), crate::ViewerScope::Workspace, + window, cx, ) }), diff --git a/crates/workspace/src/viewer/mod.rs b/crates/workspace/src/viewer/mod.rs index d25465da..df125acd 100644 --- a/crates/workspace/src/viewer/mod.rs +++ b/crates/workspace/src/viewer/mod.rs @@ -75,11 +75,16 @@ pub fn viewer_in(scope: Scope, cx: &App) -> Option> { } /// Opens `path` in the shared viewer overlay, replacing any viewer already open. -pub fn open_viewer(path: PathBuf, scope: Scope, cx: &mut App) { +pub fn open_viewer(path: PathBuf, scope: Scope, window: &mut Window, cx: &mut App) { let document = preview::has_text(&path); let video = preview::has_video(&path); let details = preview::describe(&path); let probe_path = path.clone(); + let return_focus = cx + .try_global::() + .and_then(|active| active.0.as_ref()) + .map(|viewer| viewer.read(cx).return_focus.clone()) + .unwrap_or_else(|| window.focused(cx)); let viewer = cx.new(|cx| Viewer { transport: Transport::new(path.clone(), cx), path, @@ -95,6 +100,7 @@ pub fn open_viewer(path: PathBuf, scope: Scope, cx: &mut App) { offset: Point::default(), drag_from: None, focus_handle: cx.focus_handle(), + return_focus, focused: false, find: Find::default(), find_open: false, @@ -121,10 +127,17 @@ pub fn open_viewer(path: PathBuf, scope: Scope, cx: &mut App) { cx.set_global(ActiveViewer(Some(viewer))); } -pub fn close_viewer(cx: &mut App) { +pub fn close_viewer(window: &mut Window, cx: &mut App) { // Without this the recording plays on over an empty screen, with nothing left to stop it. preview::playback::stop(cx); + let return_focus = cx + .try_global::() + .and_then(|active| active.0.as_ref()) + .and_then(|viewer| viewer.read(cx).return_focus.clone()); cx.set_global(ActiveViewer(None)); + if let Some(return_focus) = return_focus { + return_focus.focus(window, cx); + } } pub struct Viewer { @@ -158,6 +171,8 @@ pub struct Viewer { /// Last pointer position while dragging; `None` when not panning. drag_from: Option>, focus_handle: FocusHandle, + /// Where focus belonged before the overlay took it. + return_focus: Option, /// Grabs focus on first render so Escape reaches [`Self`]; set once so we don't re-focus. focused: bool, find: Find, @@ -387,7 +402,7 @@ impl Render for Viewer { window.focus(&this.focus_handle, cx); cx.notify(); } else { - close_viewer(cx); + close_viewer(window, cx); } })) // Fills whichever overlay slot mounted it; `absolute` so it stacks over that slot's @@ -751,7 +766,7 @@ impl Render for Viewer { .ghost() .small() .tooltip("Close (Esc)") - .on_click(cx.listener(|_, _, _, cx| close_viewer(cx))), + .on_click(cx.listener(|_, _, window, cx| close_viewer(window, cx))), ), ) } @@ -760,42 +775,83 @@ impl Render for Viewer { #[cfg(test)] mod tests { // Never `use super::*` here — the parent's `use gpui::*` would shadow `#[test]`. - use gpui::TestAppContext; + use gpui::{ + Context, FocusHandle, InteractiveElement as _, IntoElement, Render, TestAppContext, + VisualTestContext, Window, div, + }; use crate::viewer::{Scope, close_viewer, open_viewer, viewer_in}; + struct FocusProbe(FocusHandle); + + impl Render for FocusProbe { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div().track_focus(&self.0) + } + } + + fn with_window(cx: &mut TestAppContext) -> &mut VisualTestContext { + let (_, cx) = cx.add_window_view(|_, cx| FocusProbe(cx.focus_handle())); + cx + } + /// One global feeds two mount slots — the workspace overlay and the centre panel. Exactly one /// may claim it: drop the scope check and the viewer paints twice, once in each slot, with two /// sets of live controls over each other. #[gpui::test] fn only_the_slot_it_was_opened_for_mounts_the_viewer(cx: &mut TestAppContext) { + let cx = with_window(cx); let path = std::path::PathBuf::from("/nonexistent/qrate-scope-test.jpg"); - cx.update(|cx| { - open_viewer(path.clone(), Scope::Centre, cx); + cx.update(|window, cx| { + open_viewer(path.clone(), Scope::Centre, window, cx); assert!(viewer_in(Scope::Centre, cx).is_some()); assert!(viewer_in(Scope::Workspace, cx).is_none()); // Opening in the other scope replaces rather than stacks. - open_viewer(path, Scope::Workspace, cx); + open_viewer(path, Scope::Workspace, window, cx); assert!(viewer_in(Scope::Workspace, cx).is_some()); assert!(viewer_in(Scope::Centre, cx).is_none()); - close_viewer(cx); + close_viewer(window, cx); assert!(viewer_in(Scope::Workspace, cx).is_none()); assert!(viewer_in(Scope::Centre, cx).is_none()); }); } + #[gpui::test] + fn closing_the_viewer_restores_its_callers_focus(cx: &mut TestAppContext) { + let cx = with_window(cx); + cx.update(|window, cx| { + let caller = cx.focus_handle(); + caller.focus(window, cx); + open_viewer( + "/nonexistent/qrate-focus-test.jpg".into(), + Scope::Workspace, + window, + cx, + ); + let viewer = viewer_in(Scope::Workspace, cx).expect("just opened"); + let viewer_focus = viewer.read(cx).focus_handle.clone(); + viewer_focus.focus(window, cx); + assert!(viewer.read(cx).focus_handle.is_focused(window)); + + close_viewer(window, cx); + + assert!(caller.is_focused(window)); + }); + } + #[gpui::test] fn file_details_are_kept_without_restatting_during_render(cx: &mut TestAppContext) { + let cx = with_window(cx); let path = std::env::temp_dir().join("qrate-viewer-details.jpg"); std::fs::write(&path, b"abc").unwrap(); - cx.update(|cx| { - open_viewer(path.clone(), Scope::Workspace, cx); + cx.update(|window, cx| { + open_viewer(path.clone(), Scope::Workspace, window, cx); std::fs::remove_file(&path).unwrap(); let viewer = viewer_in(Scope::Workspace, cx).expect("just opened"); assert!(viewer.read(cx).details.is_some()); - close_viewer(cx); + close_viewer(window, cx); }); } @@ -803,9 +859,10 @@ mod tests { /// an underflow on page zero would panic on a `usize` subtraction. #[gpui::test] fn paging_stops_at_both_ends_and_resets_the_view(cx: &mut TestAppContext) { + let cx = with_window(cx); let path = std::path::PathBuf::from("/nonexistent/qrate-paging-test.pdf"); - cx.update(|cx| { - open_viewer(path, Scope::Workspace, cx); + cx.update(|window, cx| { + open_viewer(path, Scope::Workspace, window, cx); let viewer = viewer_in(Scope::Workspace, cx).expect("just opened"); viewer.update(cx, |viewer, _| { @@ -833,16 +890,17 @@ mod tests { assert_eq!(viewer.page, 2, "cannot go past the last page"); }); - close_viewer(cx); + close_viewer(window, cx); }); } /// A photo is a one-page document, so the controls stay hidden and the arrow keys do nothing. #[gpui::test] fn a_single_page_file_never_moves(cx: &mut TestAppContext) { + let cx = with_window(cx); let path = std::path::PathBuf::from("/nonexistent/qrate-single-page.jpg"); - cx.update(|cx| { - open_viewer(path, Scope::Centre, cx); + cx.update(|window, cx| { + open_viewer(path, Scope::Centre, window, cx); let viewer = viewer_in(Scope::Centre, cx).expect("just opened"); viewer.update(cx, |viewer, _| { assert_eq!( @@ -854,7 +912,7 @@ mod tests { viewer.turn_page(-1); assert_eq!(viewer.page, 0); }); - close_viewer(cx); + close_viewer(window, cx); }); } @@ -862,19 +920,20 @@ mod tests { /// a document" — a silent tape and a tape with no readable header both still need playing. #[gpui::test] fn only_a_recording_gets_a_transport(cx: &mut TestAppContext) { - cx.update(|cx| { - open_viewer("/nonexistent/take.wav".into(), Scope::Workspace, cx); + let cx = with_window(cx); + cx.update(|window, cx| { + open_viewer("/nonexistent/take.wav".into(), Scope::Workspace, window, cx); let recording = viewer_in(Scope::Workspace, cx).expect("just opened"); recording.update(cx, |viewer, _| { assert!(viewer.transport.is_some(), "a WAV is a recording"); assert!(!viewer.document); }); - open_viewer("/nonexistent/scan.pdf".into(), Scope::Workspace, cx); + open_viewer("/nonexistent/scan.pdf".into(), Scope::Workspace, window, cx); let document = viewer_in(Scope::Workspace, cx).expect("just opened"); document.update(cx, |viewer, _| assert!(viewer.transport.is_none())); - close_viewer(cx); + close_viewer(window, cx); }); } @@ -886,6 +945,7 @@ mod tests { /// it is the real path. #[gpui::test] fn closing_the_viewer_leaves_nothing_playing(cx: &mut TestAppContext) { + let cx = with_window(cx); // 44-byte canonical WAV header, then a second of 8 kHz 16-bit mono silence. let data = 8000usize * 2; let mut wav = Vec::new(); @@ -906,10 +966,10 @@ mod tests { let path = std::env::temp_dir().join("qrate-viewer-close-stops.wav"); std::fs::write(&path, &wav).unwrap(); - cx.update(|cx| { - open_viewer(path.clone(), Scope::Workspace, cx); + cx.update(|window, cx| { + open_viewer(path.clone(), Scope::Workspace, window, cx); preview::playback::play(&path, cx); - close_viewer(cx); + close_viewer(window, cx); assert!( !preview::playback::position(cx).is_some_and(|(_, playing)| playing), "the recording keeps going after the viewer is gone" @@ -928,6 +988,7 @@ mod tests { fn a_video_scrubs_on_release_and_not_during_the_drag(cx: &mut TestAppContext) { use gpui_component::slider::{SliderEvent, SliderValue}; + let cx = with_window(cx); let path = std::env::temp_dir().join("qrate-viewer-scrub.mp4"); let made = std::process::Command::new("ffmpeg") .args(["-v", "error", "-y", "-f", "lavfi", "-i"]) @@ -940,12 +1001,12 @@ mod tests { } // An emitted event is delivered when the update flushes, so each half is its own update. - let viewer = cx.update(|cx| { - open_viewer(path.clone(), Scope::Workspace, cx); + let viewer = cx.update(|window, cx| { + open_viewer(path.clone(), Scope::Workspace, window, cx); viewer_in(Scope::Workspace, cx).expect("just opened") }); cx.run_until_parked(); - let scrubber = cx.update(|cx| { + let scrubber = cx.update(|_, cx| { let scrubber = viewer .read(cx) .scrubber @@ -955,21 +1016,21 @@ mod tests { scrubber }); - cx.update(|cx| { + cx.update(|_, cx| { scrubber.update(cx, |_, cx| { cx.emit(SliderEvent::Change(SliderValue::Single(4.))); }); }); - cx.update(|cx| { + cx.update(|_, cx| { assert_eq!(viewer.read(cx).scrub, 4, "the readout follows the thumb"); assert_eq!(viewer.read(cx).page, 0, "but nothing has been rendered yet"); scrubber.update(cx, |_, cx| { cx.emit(SliderEvent::Release(SliderValue::Single(4.))); }); }); - cx.update(|cx| { + cx.update(|window, cx| { assert_eq!(viewer.read(cx).page, 4, "letting go lands on that second"); - close_viewer(cx); + close_viewer(window, cx); }); let _ = std::fs::remove_file(&path); @@ -980,19 +1041,25 @@ mod tests { /// "has more than one page". #[gpui::test] fn a_one_page_document_still_says_so(cx: &mut TestAppContext) { - cx.update(|cx| { - open_viewer("/nonexistent/one-page.pdf".into(), Scope::Workspace, cx); + let cx = with_window(cx); + cx.update(|window, cx| { + open_viewer( + "/nonexistent/one-page.pdf".into(), + Scope::Workspace, + window, + cx, + ); let viewer = viewer_in(Scope::Workspace, cx).expect("just opened"); viewer.update(cx, |viewer, _| { assert_eq!(viewer.pages, 1); assert!(viewer.document, "a PDF is a document at any length"); }); - open_viewer("/nonexistent/scan.jpg".into(), Scope::Workspace, cx); + open_viewer("/nonexistent/scan.jpg".into(), Scope::Workspace, window, cx); let photo = viewer_in(Scope::Workspace, cx).expect("just opened"); photo.update(cx, |viewer, _| assert!(!viewer.document)); - close_viewer(cx); + close_viewer(window, cx); }); } } diff --git a/crates/workspace/src/views/gallery.rs b/crates/workspace/src/views/gallery.rs index 042c01fc..851f08e8 100644 --- a/crates/workspace/src/views/gallery.rs +++ b/crates/workspace/src/views/gallery.rs @@ -198,7 +198,7 @@ fn card( return; } if let Some(path) = viewable.clone() { - crate::viewer::open_viewer(path, crate::ViewerScope::Centre, cx); + crate::viewer::open_viewer(path, crate::ViewerScope::Centre, window, cx); } } }) diff --git a/crates/workspace/src/views/mod.rs b/crates/workspace/src/views/mod.rs index 735d528a..ea37c618 100644 --- a/crates/workspace/src/views/mod.rs +++ b/crates/workspace/src/views/mod.rs @@ -241,6 +241,7 @@ impl ViewsPanel { /// inside a Windows window proc: a non-unwinding abort, not a catchable error. So the panel's own /// state changes under one short lease that is released before anything touches the dock. fn switch(panel: &Entity, mode: ViewMode, window: &mut Window, cx: &mut App) { + let had_focus = panel.read(cx).focus_handle.contains_focused(window, cx); let Some((leaving, dock_area)) = panel.update(cx, |this, cx| { (this.view != mode).then(|| { let leaving = this.view; @@ -253,7 +254,7 @@ fn switch(panel: &Entity, mode: ViewMode, window: &mut Window, cx: & }; if crate::viewer::viewer_in(ViewerScope::Centre, cx).is_some() { - crate::viewer::close_viewer(cx); + crate::viewer::close_viewer(window, cx); } let text = SharedString::from(mode.as_str()); if cx.has_global::() { @@ -265,6 +266,10 @@ fn switch(panel: &Entity, mode: ViewMode, window: &mut Window, cx: & layout::capture(leaving, &area, cx); layout::apply(mode, &area, window, cx); } + if had_focus { + let focus = panel.read(cx).focus_handle.clone(); + focus.focus(window, cx); + } // The switcher is drawn by the parent `TabPanel` (via `Panel::title`), which does not observe // this panel — `cx.notify()` above repaints the body but leaves the highlight on the old tab. // Switching views is a rare, deliberate click, so a full redraw is the cheap honest fix. From e0cdf5c3c2d2207b423833ea5fb8454bde6fc9b7 Mon Sep 17 00:00:00 2001 From: devnull03 <56480041+devnull03@users.noreply.github.com> Date: Mon, 7 Sep 2026 19:51:28 -0700 Subject: [PATCH 11/14] fix(workspace): close viewer without re-entering its entity --- crates/workspace/src/viewer/mod.rs | 45 +++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/crates/workspace/src/viewer/mod.rs b/crates/workspace/src/viewer/mod.rs index df125acd..c4ce9aee 100644 --- a/crates/workspace/src/viewer/mod.rs +++ b/crates/workspace/src/viewer/mod.rs @@ -61,16 +61,19 @@ pub enum Scope { Centre, } -/// The currently-open viewer, if any. Both mount slots observe this. +/// The currently-open viewer and the focus to restore. Both mount slots observe this. #[derive(Default)] -pub struct ActiveViewer(pub Option>); +pub struct ActiveViewer { + viewer: Option>, + return_focus: Option, +} impl Global for ActiveViewer {} /// The open viewer, if it belongs in `scope`. `None` tells that slot to mount nothing — which is /// how one global feeds two slots without ever painting itself twice. pub fn viewer_in(scope: Scope, cx: &App) -> Option> { - let viewer = cx.try_global::()?.0.clone()?; + let viewer = cx.try_global::()?.viewer.clone()?; (viewer.read(cx).scope == scope).then_some(viewer) } @@ -82,9 +85,8 @@ pub fn open_viewer(path: PathBuf, scope: Scope, window: &mut Window, cx: &mut Ap let probe_path = path.clone(); let return_focus = cx .try_global::() - .and_then(|active| active.0.as_ref()) - .map(|viewer| viewer.read(cx).return_focus.clone()) - .unwrap_or_else(|| window.focused(cx)); + .and_then(|active| active.return_focus.clone()) + .or_else(|| window.focused(cx)); let viewer = cx.new(|cx| Viewer { transport: Transport::new(path.clone(), cx), path, @@ -100,7 +102,6 @@ pub fn open_viewer(path: PathBuf, scope: Scope, window: &mut Window, cx: &mut Ap offset: Point::default(), drag_from: None, focus_handle: cx.focus_handle(), - return_focus, focused: false, find: Find::default(), find_open: false, @@ -124,7 +125,10 @@ pub fn open_viewer(path: PathBuf, scope: Scope, window: &mut Window, cx: &mut Ap }); }) .detach(); - cx.set_global(ActiveViewer(Some(viewer))); + cx.set_global(ActiveViewer { + viewer: Some(viewer), + return_focus, + }); } pub fn close_viewer(window: &mut Window, cx: &mut App) { @@ -132,9 +136,8 @@ pub fn close_viewer(window: &mut Window, cx: &mut App) { preview::playback::stop(cx); let return_focus = cx .try_global::() - .and_then(|active| active.0.as_ref()) - .and_then(|viewer| viewer.read(cx).return_focus.clone()); - cx.set_global(ActiveViewer(None)); + .and_then(|active| active.return_focus.clone()); + cx.set_global(ActiveViewer::default()); if let Some(return_focus) = return_focus { return_focus.focus(window, cx); } @@ -171,8 +174,6 @@ pub struct Viewer { /// Last pointer position while dragging; `None` when not panning. drag_from: Option>, focus_handle: FocusHandle, - /// Where focus belonged before the overlay took it. - return_focus: Option, /// Grabs focus on first render so Escape reaches [`Self`]; set once so we don't re-focus. focused: bool, find: Find, @@ -841,6 +842,24 @@ mod tests { }); } + #[gpui::test] + fn viewer_can_close_from_its_own_listener(cx: &mut TestAppContext) { + let cx = with_window(cx); + cx.update(|window, cx| { + open_viewer( + "/nonexistent/qrate-reentrant-close-test.jpg".into(), + Scope::Workspace, + window, + cx, + ); + let viewer = viewer_in(Scope::Workspace, cx).expect("just opened"); + + viewer.update(cx, |_, cx| close_viewer(window, cx)); + + assert!(viewer_in(Scope::Workspace, cx).is_none()); + }); + } + #[gpui::test] fn file_details_are_kept_without_restatting_during_render(cx: &mut TestAppContext) { let cx = with_window(cx); From de32a023b93bdd2836b4ca5e14527282906427d0 Mon Sep 17 00:00:00 2001 From: devnull03 <56480041+devnull03@users.noreply.github.com> Date: Mon, 7 Sep 2026 19:51:30 -0700 Subject: [PATCH 12/14] fix(app): keep development startup out of release state --- crates/app/src/logging.rs | 47 ++++++++++++++++++++++++-- crates/app/src/update_check.rs | 62 ++++++++++++++++++++++++++-------- 2 files changed, 92 insertions(+), 17 deletions(-) diff --git a/crates/app/src/logging.rs b/crates/app/src/logging.rs index a1352a38..4aa2c77f 100644 --- a/crates/app/src/logging.rs +++ b/crates/app/src/logging.rs @@ -79,7 +79,7 @@ fn version() -> &'static str { env!("CARGO_PKG_VERSION") } -/// Removes GPUI's unactionable focus-node notice and demotes window-teardown errors to debug. +/// Removes GPUI's unactionable focus notice and demotes expected platform errors to debug. /// /// GPUI reports the missing focus node at info level every time focus reaches an internal element /// without an accessibility role, and narrates every accessibility tree update — one line per @@ -117,6 +117,14 @@ fn is_window_teardown(record: &Record) -> bool { message == "window not found" || message.starts_with("Invalid window handle") } +fn is_missing_dxgi_debug_layer(record: &Record) -> bool { + record.level() == Level::Error + && logged_by_gpui(record.target()) + && record.args().to_string().starts_with( + "The application requested an operation that depends on an SDK component that is missing or mismatched.", + ) +} + impl Log for QuietGpuiNoise { fn enabled(&self, metadata: &Metadata) -> bool { self.0.enabled(metadata) @@ -130,7 +138,7 @@ impl Log for QuietGpuiNoise { if is_accessibility_focus_noise(record) { return; } - if is_window_teardown(record) { + if is_window_teardown(record) || is_missing_dxgi_debug_layer(record) { self.0.log( &Record::builder() .level(Level::Debug) @@ -327,6 +335,41 @@ mod tests { )); } + #[test] + fn only_the_optional_dxgi_debug_layer_error_is_demoted() { + let dxgi = |target: &str, level: log::Level, message: &str| { + super::is_missing_dxgi_debug_layer( + &log::Record::builder() + .level(level) + .target(target) + .args(format_args!("{message}")) + .build(), + ) + }; + let error = log::Level::Error; + + assert!(dxgi( + "", + error, + "The application requested an operation that depends on an SDK component that is missing or mismatched. (0x887A002D)" + )); + assert!(!dxgi( + "gpui_windows::directx_devices", + error, + "Failed to create Direct3D device" + )); + assert!(!dxgi( + "app::logging", + error, + "The application requested an operation that depends on an SDK component that is missing or mismatched. (0x887A002D)" + )); + assert!(!dxgi( + "", + log::Level::Warn, + "The application requested an operation that depends on an SDK component that is missing or mismatched. (0x887A002D)" + )); + } + #[test] fn only_gpui_missing_focus_nodes_are_suppressed() { let noisy = |target: &str, message: &str| { diff --git a/crates/app/src/update_check.rs b/crates/app/src/update_check.rs index a4bb7e7c..d3b597fc 100644 --- a/crates/app/src/update_check.rs +++ b/crates/app/src/update_check.rs @@ -73,6 +73,21 @@ fn no_update_status(manual: bool) -> UpdateStatus { } } +fn initial_status(development_build: bool, kind: Option) -> UpdateStatus { + match (development_build, kind) { + (true, _) => { + UpdateStatus::Disabled("Development builds do not update automatically".into()) + } + (false, Some(InstallKind::WindowsMsi)) => { + UpdateStatus::Disabled("Updates are managed by your administrator".into()) + } + (false, Some(_)) => UpdateStatus::Idle, + (false, None) => { + UpdateStatus::Disabled("This source or unmarked build updates manually".into()) + } + } +} + pub struct AutoUpdater { status: UpdateStatus, installation: Option, @@ -95,13 +110,12 @@ enum DownloadEvent { impl AutoUpdater { fn new(cx: &mut Context) -> Self { let installation = updater::detect_installation().ok(); - let status = match &installation { - Some(installation) if installation.marker.kind == InstallKind::WindowsMsi => { - UpdateStatus::Disabled("Updates are managed by your administrator".into()) - } - Some(_) => UpdateStatus::Idle, - None => UpdateStatus::Disabled("This source or unmarked build updates manually".into()), - }; + let status = initial_status( + cfg!(debug_assertions), + installation + .as_ref() + .map(|installation| installation.marker.kind), + ); let _settings_sub = cx.observe_global::(|this, cx| { if automatic_updates(cx) && matches!(this.status, UpdateStatus::Idle) { this.poll(false, cx); @@ -138,7 +152,8 @@ impl AutoUpdater { } pub fn poll(&mut self, manual: bool, cx: &mut Context) { - if self.pending + if matches!(self.status, UpdateStatus::Disabled(_)) + || self.pending || matches!( self.status, UpdateStatus::Ready { .. } | UpdateStatus::Restarting @@ -255,6 +270,9 @@ impl AutoUpdater { } fn start_polling(&mut self, cx: &mut Context) { + if matches!(self.status, UpdateStatus::Disabled(_)) { + return; + } if automatic_updates(cx) { self.poll(false, cx); } @@ -331,13 +349,17 @@ fn helper_path(installation: &Installation) -> PathBuf { } pub fn init(cx: &mut App) { - let current = Version::parse(env!("CARGO_PKG_VERSION")).expect("package version is SemVer"); - match updater::mark_healthy(¤t) { - Ok(Some(receipt)) if receipt.status == updater::ReceiptStatus::Failed => { - log::error!("previous update failed: {}", receipt.message) + if cfg!(debug_assertions) { + log::info!("automatic updates are disabled in development builds"); + } else { + let current = Version::parse(env!("CARGO_PKG_VERSION")).expect("package version is SemVer"); + match updater::mark_healthy(¤t) { + Ok(Some(receipt)) if receipt.status == updater::ReceiptStatus::Failed => { + log::error!("previous update failed: {}", receipt.message) + } + Err(error) => log::warn!("could not finalize previous update: {error:#}"), + _ => {} } - Err(error) => log::warn!("could not finalize previous update: {error:#}"), - _ => {} } let updater = cx.new(AutoUpdater::new); cx.set_global(GlobalUpdater(updater.clone())); @@ -353,8 +375,18 @@ pub fn check_now(cx: &mut App) { #[cfg(test)] mod tests { use semver::Version; + use updater::InstallKind; + + use super::{UpdateStatus, initial_status, no_update_status}; - use super::{UpdateStatus, no_update_status}; + #[test] + fn development_builds_ignore_packaged_install_markers() { + assert!(matches!( + initial_status(true, Some(InstallKind::WindowsNsis)), + UpdateStatus::Disabled(reason) + if reason.as_ref() == "Development builds do not update automatically" + )); + } #[test] fn no_update_is_only_visible_after_a_manual_check() { From 1f71746e10581a6be2d83b30a689b95c3a08b96f Mon Sep 17 00:00:00 2001 From: devnull03 <56480041+devnull03@users.noreply.github.com> Date: Mon, 7 Sep 2026 19:51:32 -0700 Subject: [PATCH 13/14] chore(preview): add a native Windows binary fetcher --- CLAUDE.md | 2 + scripts/fetch-binaries.ps1 | 78 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 scripts/fetch-binaries.ps1 diff --git a/CLAUDE.md b/CLAUDE.md index 6cc5dbd8..647cb02d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -135,6 +135,8 @@ executable** first and then on the system. A checkout without them builds, launc ./scripts/fetch-binaries.sh target/release # before building an installer ``` +On Windows, use `./scripts/fetch-binaries.ps1` from PowerShell. + Consequence for tests: `preview`'s PDF and media tests assert *behaviour that depends on what is installed*. With the binary present they require a real render; without it they require a clean decline. Both directions are real assertions — if a test says "skipping", that is the no-binary diff --git a/scripts/fetch-binaries.ps1 b/scripts/fetch-binaries.ps1 new file mode 100644 index 00000000..86a465ee --- /dev/null +++ b/scripts/fetch-binaries.ps1 @@ -0,0 +1,78 @@ +param( + [string] $Destination = "target/debug" +) + +$ErrorActionPreference = "Stop" +$destinationPath = [System.IO.Path]::GetFullPath($Destination) +$temp = Join-Path ([System.IO.Path]::GetTempPath()) ("qrate-binaries-" + [guid]::NewGuid()) +$strict = -not [string]::IsNullOrEmpty($env:QRATE_STRICT_BINARIES) +$skipFfmpeg = -not [string]::IsNullOrEmpty($env:QRATE_SKIP_FFMPEG) +$architecture = switch ($env:PROCESSOR_ARCHITECTURE) { + "AMD64" { "x64" } + "ARM64" { "arm64" } + default { throw "Unsupported Windows architecture: $env:PROCESSOR_ARCHITECTURE" } +} + +New-Item -ItemType Directory -Force -Path $destinationPath, $temp | Out-Null +Write-Host "==> platform: win-$architecture, destination: $destinationPath" + +try { + $pdfiumUrl = "https://github.com/bblanchon/pdfium-binaries/releases/latest/download/pdfium-win-$architecture.tgz" + $pdfiumArchive = Join-Path $temp "pdfium.tgz" + Write-Host "==> pdfium: $pdfiumUrl" + try { + Invoke-WebRequest -UseBasicParsing $pdfiumUrl -OutFile $pdfiumArchive + tar -xzf $pdfiumArchive -C $temp + if ($LASTEXITCODE -ne 0) { + throw "tar exited with code $LASTEXITCODE" + } + $pdfium = Get-ChildItem $temp -Filter "pdfium.dll" -File -Recurse | + Select-Object -First 1 + if ($null -eq $pdfium) { + throw "No PDFium library was found in $pdfiumUrl; the archive layout may have changed" + } + Copy-Item $pdfium.FullName $destinationPath -Force + Write-Host " installed pdfium.dll" + } + catch { + if ($strict) { + throw + } + Write-Warning "PDFium download failed; PDF previews will fall back to an icon: $_" + } + + if ($skipFfmpeg) { + Write-Host "==> ffmpeg: skipped (QRATE_SKIP_FFMPEG set)" + } + else { + $ffmpegUrl = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip" + $ffmpegArchive = Join-Path $temp "ffmpeg.zip" + Write-Host "==> ffmpeg: $ffmpegUrl" + try { + Invoke-WebRequest -UseBasicParsing $ffmpegUrl -OutFile $ffmpegArchive + $ffmpegDirectory = Join-Path $temp "ffmpeg" + Expand-Archive $ffmpegArchive -DestinationPath $ffmpegDirectory -Force + $ffmpeg = Get-ChildItem $ffmpegDirectory -Filter "ffmpeg.exe" -File -Recurse | + Select-Object -First 1 + if ($null -eq $ffmpeg) { + throw "No ffmpeg.exe was found in $ffmpegUrl; the archive layout may have changed" + } + Copy-Item $ffmpeg.FullName $destinationPath -Force + Write-Host " installed ffmpeg.exe" + } + catch { + if ($strict) { + throw + } + Write-Warning "ffmpeg download failed; video previews will fall back to an icon: $_" + } + } + + Write-Host "==> done. Installed preview binaries:" + Get-ChildItem $destinationPath -File | + Where-Object Name -Match "^(pdfium\.dll|ffmpeg\.exe)$" | + ForEach-Object { Write-Host " $($_.Name)" } +} +finally { + Remove-Item -LiteralPath $temp -Recurse -Force -ErrorAction SilentlyContinue +} From a703d427873f33459ba4008e14e803255cba6606 Mon Sep 17 00:00:00 2001 From: devnull03 <56480041+devnull03@users.noreply.github.com> Date: Mon, 7 Sep 2026 20:36:19 -0700 Subject: [PATCH 14/14] chore(deps): minimize the merged lockfile update --- Cargo.lock | 85 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 68 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 493eb918..4a58a934 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -537,6 +537,15 @@ dependencies = [ "syn", ] +[[package]] +name = "atoi_simd" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a49e05797ca52e312a0c658938b7d00693ef037799ef7187678f212d7684cf" +dependencies = [ + "debug_unsafe", +] + [[package]] name = "atomic" version = "0.5.3" @@ -922,7 +931,25 @@ dependencies = [ "log", "quick-xml 0.31.0", "serde", - "zip", + "zip 2.4.2", +] + +[[package]] +name = "calamine" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da56b262e8a827c6b12c3dde4ea4622e0ff542bd2e9ea5855e4cb523481d77b7" +dependencies = [ + "atoi_simd", + "byteorder", + "chrono", + "codepage", + "encoding_rs", + "fast-float2", + "log", + "quick-xml 0.38.4", + "serde", + "zip 4.6.1", ] [[package]] @@ -1637,7 +1664,7 @@ name = "data-exchange" version = "0.4.0-beta.2" dependencies = [ "base64", - "calamine", + "calamine 0.26.1", "csv", "getrandom 0.3.4", "log", @@ -1648,7 +1675,7 @@ dependencies = [ "settings", "sha2", "thiserror 2.0.18", - "zip", + "zip 2.4.2", ] [[package]] @@ -1686,6 +1713,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "debug_unsafe" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eed2c4702fa172d1ce21078faa7c5203e69f5394d48cc436d25928394a867a2" + [[package]] name = "deflate64" version = "0.1.11" @@ -2134,6 +2167,12 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "fast-float2" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6e8948ce679d00a02a94739ea185595dca7118ed04feb991127e443bd3d761f" + [[package]] name = "fastrand" version = "2.3.0" @@ -2220,6 +2259,7 @@ checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", + "zlib-rs", ] [[package]] @@ -3608,7 +3648,7 @@ dependencies = [ "tokio", "tower-service", "tracing", - "windows-registry 0.5.3", + "windows-registry 0.6.1", ] [[package]] @@ -5995,7 +6035,7 @@ dependencies = [ "symphonia", "tiff", "windows 0.62.2", - "zip", + "zip 2.4.2", ] [[package]] @@ -6040,6 +6080,7 @@ name = "project-wizard" version = "0.4.0-beta.2" dependencies = [ "anyhow", + "calamine 0.31.0", "csv", "data-exchange", "diagnostics", @@ -6147,6 +6188,7 @@ version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" dependencies = [ + "encoding_rs", "memchr", "serde", ] @@ -8648,7 +8690,7 @@ dependencies = [ "tar", "tempfile", "windows 0.62.2", - "zip", + "zip 2.4.2", ] [[package]] @@ -9643,17 +9685,6 @@ dependencies = [ "windows-targets 0.53.5", ] -[[package]] -name = "windows-registry" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" -dependencies = [ - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - [[package]] name = "windows-registry" version = "0.6.1" @@ -10753,6 +10784,26 @@ dependencies = [ "zstd", ] +[[package]] +name = "zip" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" +dependencies = [ + "arbitrary", + "crc32fast", + "flate2", + "indexmap", + "memchr", + "zopfli", +] + +[[package]] +name = "zlib-rs" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" + [[package]] name = "zmij" version = "1.0.21"