diff --git a/Cargo.lock b/Cargo.lock index 38dcf369b3739f9087b574489666f4f1dfa012e0..bfd80726843695dbfcb4baf1db4fe3e6ca9a4682 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -111,22 +111,13 @@ dependencies = [ "workspace", ] -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli 0.31.1", -] - [[package]] name = "addr2line" version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ - "gimli 0.32.3", + "gimli", ] [[package]] @@ -674,7 +665,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" dependencies = [ - "object 0.37.3", + "object", ] [[package]] @@ -1821,11 +1812,11 @@ version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" dependencies = [ - "addr2line 0.25.1", + "addr2line", "cfg-if", "libc", "miniz_oxide", - "object 0.37.3", + "object", "rustc-demangle", "windows-link 0.2.1", ] @@ -3818,36 +3809,36 @@ dependencies = [ [[package]] name = "cranelift-assembler-x64" -version = "0.120.2" +version = "0.123.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5023e06632d8f351c2891793ccccfe4aef957954904392434038745fb6f1f68" +checksum = "ba33ddc4e157cb1abe9da6c821e8824f99e56d057c2c22536850e0141f281d61" dependencies = [ "cranelift-assembler-x64-meta", ] [[package]] name = "cranelift-assembler-x64-meta" -version = "0.120.2" +version = "0.123.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c4012b4c8c1f6eb05c0a0a540e3e1ee992631af51aa2bbb3e712903ce4fd65" +checksum = "69b23dd6ea360e6fb28a3f3b40b7f126509668f58076a4729b2cfd656f26a0ad" dependencies = [ "cranelift-srcgen", ] [[package]] name = "cranelift-bforest" -version = "0.120.2" +version = "0.123.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d6d883b4942ef3a7104096b8bc6f2d1a41393f159ac8de12aed27b25d67f895" +checksum = "a9d81afcee8fe27ee2536987df3fadcb2e161af4edb7dbe3ef36838d0ce74382" dependencies = [ "cranelift-entity", ] [[package]] name = "cranelift-bitset" -version = "0.120.2" +version = "0.123.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db7b2ee9eec6ca8a716d900d5264d678fb2c290c58c46c8da7f94ee268175d17" +checksum = "fb33595f1279fe7af03b28245060e9085caf98b10ed3137461a85796eb83972a" dependencies = [ "serde", "serde_derive", @@ -3855,9 +3846,9 @@ dependencies = [ [[package]] name = "cranelift-codegen" -version = "0.120.2" +version = "0.123.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aeda0892577afdce1ac2e9a983a55f8c5b87a59334e1f79d8f735a2d7ba4f4b4" +checksum = "0230a6ac0660bfe31eb244cbb43dcd4f2b3c1c4e0addc3e0348c6053ea60272e" dependencies = [ "bumpalo", "cranelift-assembler-x64", @@ -3868,7 +3859,7 @@ dependencies = [ "cranelift-control", "cranelift-entity", "cranelift-isle", - "gimli 0.31.1", + "gimli", "hashbrown 0.15.5", "log", "postcard", @@ -3880,40 +3871,42 @@ dependencies = [ "sha2", "smallvec", "target-lexicon 0.13.3", + "wasmtime-internal-math", ] [[package]] name = "cranelift-codegen-meta" -version = "0.120.2" +version = "0.123.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e461480d87f920c2787422463313326f67664e68108c14788ba1676f5edfcd15" +checksum = "96d6817fdc15cb8f236fc9d8e610767d3a03327ceca4abff7a14d8e2154c405e" dependencies = [ "cranelift-assembler-x64-meta", "cranelift-codegen-shared", "cranelift-srcgen", + "heck 0.5.0", "pulley-interpreter", ] [[package]] name = "cranelift-codegen-shared" -version = "0.120.2" +version = "0.123.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976584d09f200c6c84c4b9ff7af64fc9ad0cb64dffa5780991edd3fe143a30a1" +checksum = "0403796328e9e2e7df2b80191cdbb473fd9ea3889eb45ef5632d0fef168ea032" [[package]] name = "cranelift-control" -version = "0.120.2" +version = "0.123.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46d43d70f4e17c545aa88dbf4c84d4200755d27c6e3272ebe4de65802fa6a955" +checksum = "188f04092279a3814e0b6235c2f9c2e34028e4beb72da7bfed55cbd184702bcc" dependencies = [ "arbitrary", ] [[package]] name = "cranelift-entity" -version = "0.120.2" +version = "0.123.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75418674520cb400c8772bfd6e11a62736c78fc1b6e418195696841d1bf91f1" +checksum = "43f5e7391167605d505fe66a337e1a69583b3f34b63d359ffa5a430313c555e8" dependencies = [ "cranelift-bitset", "serde", @@ -3922,9 +3915,9 @@ dependencies = [ [[package]] name = "cranelift-frontend" -version = "0.120.2" +version = "0.123.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c8b1a91c86687a344f3c52dd6dfb6e50db0dfa7f2e9c7711b060b3623e1fdeb" +checksum = "ea5440792eb2b5ba0a0976df371b9f94031bd853ae56f389de610bca7128a7cb" dependencies = [ "cranelift-codegen", "log", @@ -3934,15 +3927,15 @@ dependencies = [ [[package]] name = "cranelift-isle" -version = "0.120.2" +version = "0.123.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711baa4e3432d4129295b39ec2b4040cc1b558874ba0a37d08e832e857db7285" +checksum = "1e5c05fab6fce38d729088f3fa1060eaa1ad54eefd473588887205ed2ab2f79e" [[package]] name = "cranelift-native" -version = "0.120.2" +version = "0.123.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41c83e8666e3bcc5ffeaf6f01f356f0e1f9dcd69ce5511a1efd7ca5722001a3f" +checksum = "9c9a0607a028edf5ba5bba7e7cf5ca1b7f0a030e3ae84dcd401e8b9b05192280" dependencies = [ "cranelift-codegen", "libc", @@ -3951,9 +3944,9 @@ dependencies = [ [[package]] name = "cranelift-srcgen" -version = "0.120.2" +version = "0.123.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e3f4d783a55c64266d17dc67d2708852235732a100fc40dd9f1051adc64d7b" +checksum = "cb0f2da72eb2472aaac6cfba4e785af42b1f2d82f5155f30c9c30e8cce351e17" [[package]] name = "crash-context" @@ -7054,21 +7047,15 @@ dependencies = [ [[package]] name = "gimli" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" dependencies = [ "fallible-iterator", "indexmap", "stable_deref_trait", ] -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - [[package]] name = "gio-sys" version = "0.21.5" @@ -7614,6 +7601,7 @@ dependencies = [ "media", "metal", "objc", + "objc2-app-kit", "parking_lot", "pathfinder_geometry", "raw-window-handle", @@ -11224,6 +11212,16 @@ dependencies = [ "objc2-encode", ] +[[package]] +name = "objc2-app-kit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" +dependencies = [ + "objc2", + "objc2-foundation", +] + [[package]] name = "objc2-audio-toolbox" version = "0.3.2" @@ -11361,9 +11359,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.7" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ "crc32fast", "hashbrown 0.15.5", @@ -11371,15 +11369,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - [[package]] name = "ollama" version = "0.1.0" @@ -13518,13 +13507,25 @@ checksum = "bd348ff538bc9caeda7ee8cad2d1d48236a1f443c1fa3913c6a02fe0043b1dd3" [[package]] name = "pulley-interpreter" -version = "33.0.2" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "986beaef947a51d17b42b0ea18ceaa88450d35b6994737065ed505c39172db71" +checksum = "499d922aa0f9faac8d92351416664f1b7acd914008a90fce2f0516d31efddf67" dependencies = [ "cranelift-bitset", "log", - "wasmtime-math", + "pulley-macros", + "wasmtime-internal-math", +] + +[[package]] +name = "pulley-macros" +version = "36.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3848fb193d6dffca43a21f24ca9492f22aab88af1223d06bac7f8a0ef405b81" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -16194,12 +16195,6 @@ dependencies = [ "der 0.7.10", ] -[[package]] -name = "sptr" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a" - [[package]] name = "sqlez" version = "0.1.0" @@ -18242,17 +18237,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "trait-variant" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "transpose" version = "0.2.3" @@ -18265,9 +18249,9 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.26.3" +version = "0.26.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "974d205cc395652cfa8b37daa053fe56eebd429acf8dc055503fee648dae981e" +checksum = "887bd495d0582c5e3e0d8ece2233666169fa56a9644d172fc22ad179ab2d0538" dependencies = [ "cc", "regex", @@ -19360,12 +19344,12 @@ dependencies = [ [[package]] name = "wasm-encoder" -version = "0.229.0" +version = "0.236.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38ba1d491ecacb085a2552025c10a675a6fddcbd03b1fc9b36c536010ce265d2" +checksum = "724fccfd4f3c24b7e589d333fc0429c68042897a7e8a5f8694f31792471841e7" dependencies = [ "leb128fmt", - "wasmparser 0.229.0", + "wasmparser 0.236.1", ] [[package]] @@ -19488,9 +19472,9 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.229.0" +version = "0.236.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc3b1f053f5d41aa55640a1fa9b6d1b8a9e4418d118ce308d20e24ff3575a8c" +checksum = "a9b1e81f3eb254cf7404a82cee6926a4a3ccc5aad80cc3d43608a070c67aa1d7" dependencies = [ "bitflags 2.10.0", "hashbrown 0.15.5", @@ -19513,22 +19497,22 @@ dependencies = [ [[package]] name = "wasmprinter" -version = "0.229.0" +version = "0.236.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d25dac01892684a99b8fbfaf670eb6b56edea8a096438c75392daeb83156ae2e" +checksum = "2df225df06a6df15b46e3f73ca066ff92c2e023670969f7d50ce7d5e695abbb1" dependencies = [ "anyhow", "termcolor", - "wasmparser 0.229.0", + "wasmparser 0.236.1", ] [[package]] name = "wasmtime" -version = "33.0.2" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57373e1d8699662fb791270ac5dfac9da5c14f618ecf940cdb29dc3ad9472a3c" +checksum = "6a2f8736ddc86e03a9d0e4c477a37939cfc53cd1b052ee38a3133679b87ef830" dependencies = [ - "addr2line 0.24.2", + "addr2line", "anyhow", "async-trait", "bitflags 2.10.0", @@ -19542,10 +19526,9 @@ dependencies = [ "log", "mach2 0.4.3", "memfd", - "object 0.36.7", + "object", "once_cell", "postcard", - "psm", "pulley-interpreter", "rayon", "rustix 1.1.2", @@ -19553,82 +19536,109 @@ dependencies = [ "serde", "serde_derive", "smallvec", - "sptr", "target-lexicon 0.13.3", - "trait-variant", - "wasmparser 0.229.0", - "wasmtime-asm-macros", - "wasmtime-component-macro", - "wasmtime-component-util", - "wasmtime-cranelift", + "wasmparser 0.236.1", "wasmtime-environ", - "wasmtime-fiber", - "wasmtime-jit-icache-coherence", - "wasmtime-math", - "wasmtime-slab", - "wasmtime-versioned-export-macros", - "wasmtime-winch", - "windows-sys 0.59.0", + "wasmtime-internal-asm-macros", + "wasmtime-internal-component-macro", + "wasmtime-internal-component-util", + "wasmtime-internal-cranelift", + "wasmtime-internal-fiber", + "wasmtime-internal-jit-debug", + "wasmtime-internal-jit-icache-coherence", + "wasmtime-internal-math", + "wasmtime-internal-slab", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", + "wasmtime-internal-winch", + "windows-sys 0.60.2", ] [[package]] -name = "wasmtime-asm-macros" -version = "33.0.2" +name = "wasmtime-c-api-impl" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0fc91372865167a695dc98d0d6771799a388a7541d3f34e939d0539d6583de" +checksum = "f3c62ea3fa30e6b0cf61116b3035121b8f515c60ac118ebfdab2ee56d028ed1e" dependencies = [ - "cfg-if", + "anyhow", + "log", + "tracing", + "wasmtime", + "wasmtime-internal-c-api-macros", ] [[package]] -name = "wasmtime-c-api-impl" -version = "33.0.2" +name = "wasmtime-environ" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46db556f1dccdd88e0672bd407162ab0036b72e5eccb0f4398d8251cba32dba1" +checksum = "733682a327755c77153ac7455b1ba8f2db4d9946c1738f8002fe1fbda1d52e83" dependencies = [ "anyhow", + "cpp_demangle", + "cranelift-bitset", + "cranelift-entity", + "gimli", + "indexmap", "log", - "tracing", - "wasmtime", - "wasmtime-c-api-macros", + "object", + "postcard", + "rustc-demangle", + "semver", + "serde", + "serde_derive", + "smallvec", + "target-lexicon 0.13.3", + "wasm-encoder 0.236.1", + "wasmparser 0.236.1", + "wasmprinter", + "wasmtime-internal-component-util", ] [[package]] -name = "wasmtime-c-api-macros" -version = "33.0.2" +name = "wasmtime-internal-asm-macros" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "315cc6bc8cdc66f296accb26d7625ae64c1c7b6da6f189e8a72ce6594bf7bd36" +checksum = "68288980a2e02bcb368d436da32565897033ea21918007e3f2bae18843326cf9" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "wasmtime-internal-c-api-macros" +version = "36.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8c61294155a6d23c202f08cf7a2f9392a866edd50517508208818be626ce9f" dependencies = [ "proc-macro2", "quote", ] [[package]] -name = "wasmtime-component-macro" -version = "33.0.2" +name = "wasmtime-internal-component-macro" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25c9c7526675ff9a9794b115023c4af5128e3eb21389bfc3dc1fd344d549258f" +checksum = "5dea846da68f8e776c8a43bde3386022d7bb74e713b9654f7c0196e5ff2e4684" dependencies = [ "anyhow", "proc-macro2", "quote", "syn 2.0.117", - "wasmtime-component-util", - "wasmtime-wit-bindgen", - "wit-parser 0.229.0", + "wasmtime-internal-component-util", + "wasmtime-internal-wit-bindgen", + "wit-parser 0.236.1", ] [[package]] -name = "wasmtime-component-util" -version = "33.0.2" +name = "wasmtime-internal-component-util" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc42ec8b078875804908d797cb4950fec781d9add9684c9026487fd8eb3f6291" +checksum = "fe1e5735b3c8251510d2a55311562772d6c6fca9438a3d0329eb6e38af4957d6" [[package]] -name = "wasmtime-cranelift" -version = "33.0.2" +name = "wasmtime-internal-cranelift" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2bd72f0a6a0ffcc6a184ec86ac35c174e48ea0e97bbae277c8f15f8bf77a566" +checksum = "e89bb9ef571288e2be6b8a3c4763acc56c348dcd517500b1679d3ffad9e4a757" dependencies = [ "anyhow", "cfg-if", @@ -19637,104 +19647,132 @@ dependencies = [ "cranelift-entity", "cranelift-frontend", "cranelift-native", - "gimli 0.31.1", + "gimli", "itertools 0.14.0", "log", - "object 0.36.7", + "object", "pulley-interpreter", "smallvec", "target-lexicon 0.13.3", "thiserror 2.0.17", - "wasmparser 0.229.0", + "wasmparser 0.236.1", "wasmtime-environ", - "wasmtime-versioned-export-macros", + "wasmtime-internal-math", + "wasmtime-internal-versioned-export-macros", ] [[package]] -name = "wasmtime-environ" -version = "33.0.2" +name = "wasmtime-internal-fiber" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6187bb108a23eb25d2a92aa65d6c89fb5ed53433a319038a2558567f3011ff2" +checksum = "b698d004b15ea1f1ae2d06e5e8b80080cbd684fd245220ce2fac3cdd5ecf87f2" dependencies = [ "anyhow", - "cpp_demangle", - "cranelift-bitset", - "cranelift-entity", - "gimli 0.31.1", - "indexmap", - "log", - "object 0.36.7", - "postcard", - "rustc-demangle", - "semver", - "serde", - "serde_derive", - "smallvec", - "target-lexicon 0.13.3", - "wasm-encoder 0.229.0", - "wasmparser 0.229.0", - "wasmprinter", - "wasmtime-component-util", + "cc", + "cfg-if", + "libc", + "rustix 1.1.2", + "wasmtime-internal-asm-macros", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.60.2", ] [[package]] -name = "wasmtime-fiber" -version = "33.0.2" +name = "wasmtime-internal-jit-debug" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc8965d2128c012329f390e24b8b2758dd93d01bf67e1a1a0dd3d8fd72f56873" +checksum = "c803a9fec05c3d7fa03474d4595079d546e77a3c71c1d09b21f74152e2165c17" dependencies = [ - "anyhow", "cc", - "cfg-if", - "rustix 1.1.2", - "wasmtime-asm-macros", - "wasmtime-versioned-export-macros", - "windows-sys 0.59.0", + "wasmtime-internal-versioned-export-macros", ] [[package]] -name = "wasmtime-jit-icache-coherence" -version = "33.0.2" +name = "wasmtime-internal-jit-icache-coherence" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7af0e940cb062a45c0b3f01a926f77da5947149e99beb4e3dd9846d5b8f11619" +checksum = "d3866909d37f7929d902e6011847748147e8734e9d7e0353e78fb8b98f586aee" dependencies = [ "anyhow", "cfg-if", "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] -name = "wasmtime-math" -version = "33.0.2" +name = "wasmtime-internal-math" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acfca360e719dda9a27e26944f2754ff2fd5bad88e21919c42c5a5f38ddd93cb" +checksum = "5a23b03fb14c64bd0dfcaa4653101f94ade76c34a3027ed2d6b373267536e45b" dependencies = [ "libm", ] [[package]] -name = "wasmtime-slab" -version = "33.0.2" +name = "wasmtime-internal-slab" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e240559cada55c4b24af979d5f6c95e0029f5772f32027ec3c62b258aaff65" +checksum = "fbff220b88cdb990d34a20b13344e5da2e7b99959a5b1666106bec94b58d6364" [[package]] -name = "wasmtime-versioned-export-macros" -version = "33.0.2" +name = "wasmtime-internal-unwinder" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0963c1438357a3d8c0efe152b4ef5259846c1cf8b864340270744fe5b3bae5e" +checksum = "13e1ad30e88988b20c0d1c56ea4b4fbc01a8c614653cbf12ca50c0dcc695e2f7" +dependencies = [ + "anyhow", + "cfg-if", + "cranelift-codegen", + "log", + "object", +] + +[[package]] +name = "wasmtime-internal-versioned-export-macros" +version = "36.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "549aefdaa1398c2fcfbf69a7b882956bb5b6e8e5b600844ecb91a3b5bf658ca7" dependencies = [ "proc-macro2", "quote", "syn 2.0.117", ] +[[package]] +name = "wasmtime-internal-winch" +version = "36.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc96a84c5700171aeecf96fa9a9ab234f333f5afb295dabf3f8a812b70fe832" +dependencies = [ + "anyhow", + "cranelift-codegen", + "gimli", + "object", + "target-lexicon 0.13.3", + "wasmparser 0.236.1", + "wasmtime-environ", + "wasmtime-internal-cranelift", + "winch-codegen", +] + +[[package]] +name = "wasmtime-internal-wit-bindgen" +version = "36.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28dc9efea511598c88564ac1974e0825c07d9c0de902dbf68f227431cd4ff8c" +dependencies = [ + "anyhow", + "bitflags 2.10.0", + "heck 0.5.0", + "indexmap", + "wit-parser 0.236.1", +] + [[package]] name = "wasmtime-wasi" -version = "33.0.2" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae951b72c7c6749a1c15dcdfb6d940a2614c932b4a54f474636e78e2c744b4c" +checksum = "c3c2e99fbaa0c26b4680e0c9af07e3f7b25f5fbc1ad97dd34067980bd027d3e5" dependencies = [ "anyhow", "async-trait", @@ -19758,14 +19796,14 @@ dependencies = [ "wasmtime", "wasmtime-wasi-io", "wiggle", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "wasmtime-wasi-io" -version = "33.0.2" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a835790dcecc3d7051ec67da52ba9e04af25e1bc204275b9391e3f0042b10797" +checksum = "de2dc367052562c228ce51ee4426330840433c29c0ea3349eca5ddeb475ecdb9" dependencies = [ "anyhow", "async-trait", @@ -19774,35 +19812,6 @@ dependencies = [ "wasmtime", ] -[[package]] -name = "wasmtime-winch" -version = "33.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc3b117d03d6eeabfa005a880c5c22c06503bb8820f3aa2e30f0e8d87b6752f" -dependencies = [ - "anyhow", - "cranelift-codegen", - "gimli 0.31.1", - "object 0.36.7", - "target-lexicon 0.13.3", - "wasmparser 0.229.0", - "wasmtime-cranelift", - "wasmtime-environ", - "winch-codegen", -] - -[[package]] -name = "wasmtime-wit-bindgen" -version = "33.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1382f4f09390eab0d75d4994d0c3b0f6279f86a571807ec67a8253c87cf6a145" -dependencies = [ - "anyhow", - "heck 0.5.0", - "indexmap", - "wit-parser 0.229.0", -] - [[package]] name = "wast" version = "35.0.2" @@ -20257,9 +20266,9 @@ dependencies = [ [[package]] name = "wiggle" -version = "33.0.2" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "649c1aca13ef9e9dccf2d5efbbebf12025bc5521c3fb7754355ef60f5eb810be" +checksum = "c13d1ae265bd6e5e608827d2535665453cae5cb64950de66e2d5767d3e32c43a" dependencies = [ "anyhow", "async-trait", @@ -20272,9 +20281,9 @@ dependencies = [ [[package]] name = "wiggle-generate" -version = "33.0.2" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "164870fc34214ee42bd81b8ce9e7c179800fa1a7d4046d17a84e7f7bf422c8ad" +checksum = "607c4966f6b30da20d24560220137cbd09df722f0558eac81c05624700af5e05" dependencies = [ "anyhow", "heck 0.5.0", @@ -20286,9 +20295,9 @@ dependencies = [ [[package]] name = "wiggle-macro" -version = "33.0.2" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d873bb5b59ca703b5e41562e96a4796d1af61bf4cf80bf8a7abda755a380ec1c" +checksum = "fc36e39412fa35f7cc86b3705dbe154168721dd3e71f6dc4a726b266d5c60c55" dependencies = [ "proc-macro2", "quote", @@ -20329,21 +20338,22 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "winch-codegen" -version = "33.0.2" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7914c296fbcef59d1b89a15e82384d34dc9669bc09763f2ef068a28dd3a64ebf" +checksum = "06c0ec09e8eb5e850e432da6271ed8c4a9d459a9db3850c38e98a3ee9d015e79" dependencies = [ "anyhow", "cranelift-assembler-x64", "cranelift-codegen", - "gimli 0.31.1", + "gimli", "regalloc2", "smallvec", "target-lexicon 0.13.3", "thiserror 2.0.17", - "wasmparser 0.229.0", - "wasmtime-cranelift", + "wasmparser 0.236.1", "wasmtime-environ", + "wasmtime-internal-cranelift", + "wasmtime-internal-math", ] [[package]] @@ -21369,9 +21379,9 @@ dependencies = [ [[package]] name = "wit-parser" -version = "0.229.0" +version = "0.236.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459c6ba62bf511d6b5f2a845a2a736822e38059c1cfa0b644b467bbbfae4efa6" +checksum = "16e4833a20cd6e85d6abfea0e63a399472d6f88c6262957c17f546879a80ba15" dependencies = [ "anyhow", "id-arena", @@ -21382,7 +21392,7 @@ dependencies = [ "serde_derive", "serde_json", "unicode-xid", - "wasmparser 0.229.0", + "wasmparser 0.236.1", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 6c8f2a78a401cc2adebb712cd8ce739c696af878..3a393237ab9f5a5a8cd4b02517f6d22382ff51ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -604,6 +604,7 @@ nbformat = "1.2.0" nix = "0.29" num-format = "0.4.4" objc = "0.2" +objc2-app-kit = { version = "0.3", default-features = false, features = [ "NSGraphics" ] } objc2-foundation = { version = "=0.3.2", default-features = false, features = [ "NSArray", "NSAttributedString", @@ -732,7 +733,7 @@ toml_edit = { version = "0.22", default-features = false, features = [ "serde", ] } tower-http = "0.4.4" -tree-sitter = { version = "0.26", features = ["wasm"] } +tree-sitter = { version = "0.26.8", features = ["wasm"] } tree-sitter-bash = "0.25.1" tree-sitter-c = "0.24.1" tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "5cb9b693cfd7bfacab1d9ff4acac1a4150700609" } @@ -767,7 +768,7 @@ uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] } walkdir = "2.5" wasm-encoder = "0.221" wasmparser = "0.221" -wasmtime = { version = "33", default-features = false, features = [ +wasmtime = { version = "36", default-features = false, features = [ "async", "demangle", "runtime", @@ -776,7 +777,7 @@ wasmtime = { version = "33", default-features = false, features = [ "incremental-cache", "parallel-compilation", ] } -wasmtime-wasi = "33" +wasmtime-wasi = "36" wax = "0.7" which = "6.0.0" wasm-bindgen = "0.2.113" @@ -821,6 +822,7 @@ features = [ "Win32_System_Com", "Win32_System_Com_StructuredStorage", "Win32_System_Console", + "Win32_System_Diagnostics_Debug", "Win32_System_DataExchange", "Win32_System_IO", "Win32_System_LibraryLoader", diff --git a/assets/icons/maximize_alt.svg b/assets/icons/maximize_alt.svg new file mode 100644 index 0000000000000000000000000000000000000000..b8b8705f902c2469ed959f93f89ca3caf3b8fc51 --- /dev/null +++ b/assets/icons/maximize_alt.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/thread_import.svg b/assets/icons/thread_import.svg new file mode 100644 index 0000000000000000000000000000000000000000..a56b5a7cccc09c5795bfadff06f06d15833232f3 --- /dev/null +++ b/assets/icons/thread_import.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/settings/default.json b/assets/settings/default.json index 57bad245474b9469a0a9b9d5674c692059f039af..2e0ddc2da70af5516d14a2fa8418a759bec62eb1 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1117,8 +1117,8 @@ "expand_terminal_card": true, // How thinking blocks should be displayed by default in the agent panel. // - // Default: automatic - "thinking_display": "automatic", + // Default: auto + "thinking_display": "auto", // Whether clicking the stop button on a running terminal tool should also cancel the agent's generation. // Note that this only applies to the stop button, not to ctrl+c inside the terminal. // diff --git a/crates/acp_tools/src/acp_tools.rs b/crates/acp_tools/src/acp_tools.rs index 52a9d03f893d0b82bf6395b4c96bc9ebe14d3afe..ae8a39c8df4f73ae8be6b748694dbde5d2a0c102 100644 --- a/crates/acp_tools/src/acp_tools.rs +++ b/crates/acp_tools/src/acp_tools.rs @@ -13,7 +13,7 @@ use gpui::{ StyleRefinement, Subscription, Task, TextStyleRefinement, Window, actions, list, prelude::*, }; use language::LanguageRegistry; -use markdown::{CodeBlockRenderer, Markdown, MarkdownElement, MarkdownStyle}; +use markdown::{CodeBlockRenderer, CopyButtonVisibility, Markdown, MarkdownElement, MarkdownStyle}; use project::{AgentId, Project}; use settings::Settings; use theme_settings::ThemeSettings; @@ -384,8 +384,11 @@ impl AcpTools { ) .code_block_renderer( CodeBlockRenderer::Default { - copy_button: false, - copy_button_on_hover: expanded, + copy_button_visibility: if expanded { + CopyButtonVisibility::VisibleOnHover + } else { + CopyButtonVisibility::Hidden + }, border: false, }, ), diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index e6ef267a95110e745534010bae32b1b1fd6c0f0c..0ed0aeb78bf8889136a479ed2dac5caba633db55 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -21,8 +21,8 @@ use settings::{LanguageModelProviderSetting, LanguageModelSelection}; use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt as _}; use zed_actions::agent::{ - AddSelectionToThread, ConflictContent, OpenClaudeAgentOnboardingModal, ReauthenticateAgent, - ResolveConflictedFilesWithAgent, ResolveConflictsWithAgent, ReviewBranchDiff, + AddSelectionToThread, ConflictContent, ReauthenticateAgent, ResolveConflictedFilesWithAgent, + ResolveConflictsWithAgent, ReviewBranchDiff, }; use crate::{ @@ -40,7 +40,7 @@ use crate::{ }; use crate::{ DEFAULT_THREAD_TITLE, - ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal, HoldForDefault}, + ui::{AcpOnboardingModal, HoldForDefault}, }; use crate::{ExpandMessageEditor, ThreadHistoryView}; use crate::{ManageProfiles, ThreadHistoryViewEvent}; @@ -66,7 +66,10 @@ use project::project_settings::ProjectSettings; use project::{Project, ProjectPath, Worktree}; use prompt_store::{PromptStore, UserPromptId}; use rules_library::{RulesLibrary, open_rules_library}; +use settings::TerminalDockPosition; use settings::{Settings, update_settings_file}; +use terminal::terminal_settings::TerminalSettings; +use terminal_view::{TerminalView, terminal_panel::TerminalPanel}; use theme_settings::ThemeSettings; use ui::{ Button, Callout, CommonAnimationExt, ContextMenu, ContextMenuEntry, DocumentationSide, @@ -86,6 +89,30 @@ use zed_actions::{ const AGENT_PANEL_KEY: &str = "agent_panel"; const RECENTLY_UPDATED_MENU_LIMIT: usize = 6; +const LAST_USED_AGENT_KEY: &str = "agent_panel__last_used_external_agent"; + +#[derive(Serialize, Deserialize)] +struct LastUsedAgent { + agent: Agent, +} + +/// Reads the most recently used agent across all workspaces. Used as a fallback +/// when opening a workspace that has no per-workspace agent preference yet. +fn read_global_last_used_agent(kvp: &KeyValueStore) -> Option { + kvp.read_kvp(LAST_USED_AGENT_KEY) + .log_err() + .flatten() + .and_then(|json| serde_json::from_str::(&json).log_err()) + .map(|entry| entry.agent) +} + +async fn write_global_last_used_agent(kvp: KeyValueStore, agent: Agent) { + if let Some(json) = serde_json::to_string(&LastUsedAgent { agent }).log_err() { + kvp.write_kvp(LAST_USED_AGENT_KEY.to_string(), json) + .await + .log_err(); + } +} fn read_serialized_panel( workspace_id: workspace::WorkspaceId, @@ -245,11 +272,6 @@ pub fn init(cx: &mut App) { .register_action(|workspace, _: &OpenAcpOnboardingModal, window, cx| { AcpOnboardingModal::toggle(workspace, window, cx) }) - .register_action( - |workspace, _: &OpenClaudeAgentOnboardingModal, window, cx| { - ClaudeCodeOnboardingModal::toggle(workspace, window, cx) - }, - ) .register_action(|_workspace, _: &ResetOnboarding, window, cx| { window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx); window.refresh(); @@ -404,6 +426,48 @@ pub fn init(cx: &mut App) { }) .register_action( |workspace: &mut Workspace, _: &AddSelectionToThread, window, cx| { + let active_editor = workspace + .active_item(cx) + .and_then(|item| item.act_as::(cx)); + let has_editor_selection = active_editor.is_some_and(|editor| { + editor.update(cx, |editor, cx| { + editor.has_non_empty_selection(&editor.display_snapshot(cx)) + }) + }); + + let has_terminal_selection = workspace + .active_item(cx) + .and_then(|item| item.act_as::(cx)) + .is_some_and(|terminal_view| { + terminal_view + .read(cx) + .terminal() + .read(cx) + .last_content + .selection_text + .as_ref() + .is_some_and(|text| !text.is_empty()) + }); + + let has_terminal_panel_selection = + workspace.panel::(cx).is_some_and(|panel| { + let position = match TerminalSettings::get_global(cx).dock { + TerminalDockPosition::Left => DockPosition::Left, + TerminalDockPosition::Bottom => DockPosition::Bottom, + TerminalDockPosition::Right => DockPosition::Right, + }; + let dock_is_open = + workspace.dock_at_position(position).read(cx).is_open(); + dock_is_open && !panel.read(cx).terminal_selections(cx).is_empty() + }); + + if !has_editor_selection + && !has_terminal_selection + && !has_terminal_panel_selection + { + return; + } + let Some(panel) = workspace.panel::(cx) else { return; }; @@ -670,13 +734,18 @@ impl AgentPanel { .ok() .flatten(); - let serialized_panel = cx + let (serialized_panel, global_last_used_agent) = cx .background_spawn(async move { - kvp.and_then(|kvp| { - workspace_id - .and_then(|id| read_serialized_panel(id, &kvp)) - .or_else(|| read_legacy_serialized_panel(&kvp)) - }) + match kvp { + Some(kvp) => { + let panel = workspace_id + .and_then(|id| read_serialized_panel(id, &kvp)) + .or_else(|| read_legacy_serialized_panel(&kvp)); + let global_agent = read_global_last_used_agent(&kvp); + (panel, global_agent) + } + None => (None, None), + } }) .await; @@ -715,10 +784,21 @@ impl AgentPanel { let panel = cx.new(|cx| Self::new(workspace, prompt_store, window, cx)); - if let Some(serialized_panel) = &serialized_panel { - panel.update(cx, |panel, cx| { + panel.update(cx, |panel, cx| { + let is_via_collab = panel.project.read(cx).is_via_collab(); + + // Only apply a non-native global fallback to local projects. + // Collab workspaces only support NativeAgent, so inheriting a + // custom agent would cause set_active → new_agent_thread_inner + // to bypass the collab guard in external_thread. + let global_fallback = global_last_used_agent + .filter(|agent| !is_via_collab || agent.is_native()); + + if let Some(serialized_panel) = &serialized_panel { if let Some(selected_agent) = serialized_panel.selected_agent.clone() { panel.selected_agent = selected_agent; + } else if let Some(agent) = global_fallback { + panel.selected_agent = agent; } if let Some(start_thread_in) = serialized_panel.start_thread_in { let is_worktree_flag_enabled = @@ -739,9 +819,11 @@ impl AgentPanel { ); } } - cx.notify(); - }); - } + } else if let Some(agent) = global_fallback { + panel.selected_agent = agent; + } + cx.notify(); + }); if let Some(thread_info) = last_active_thread { let agent = thread_info.agent_type.clone(); @@ -1074,85 +1156,30 @@ impl AgentPanel { let workspace = self.workspace.clone(); let project = self.project.clone(); let fs = self.fs.clone(); - let is_via_collab = self.project.read(cx).is_via_collab(); - - const LAST_USED_EXTERNAL_AGENT_KEY: &str = "agent_panel__last_used_external_agent"; - - #[derive(Serialize, Deserialize)] - struct LastUsedExternalAgent { - agent: crate::Agent, - } - let thread_store = self.thread_store.clone(); - let kvp = KeyValueStore::global(cx); - - if let Some(agent) = agent_choice { - cx.background_spawn({ - let agent = agent.clone(); - let kvp = kvp; - async move { - if let Some(serialized) = - serde_json::to_string(&LastUsedExternalAgent { agent }).log_err() - { - kvp.write_kvp(LAST_USED_EXTERNAL_AGENT_KEY.to_string(), serialized) - .await - .log_err(); - } - } - }) - .detach(); - - let server = agent.server(fs, thread_store); - self.create_agent_thread( - server, - resume_session_id, - work_dirs, - title, - initial_content, - workspace, - project, - agent, - focus, - window, - cx, - ); - } else { - cx.spawn_in(window, async move |this, cx| { - let ext_agent = if is_via_collab { - Agent::NativeAgent - } else { - cx.background_spawn(async move { kvp.read_kvp(LAST_USED_EXTERNAL_AGENT_KEY) }) - .await - .log_err() - .flatten() - .and_then(|value| { - serde_json::from_str::(&value).log_err() - }) - .map(|agent| agent.agent) - .unwrap_or(Agent::NativeAgent) - }; - let server = ext_agent.server(fs, thread_store); - this.update_in(cx, |agent_panel, window, cx| { - agent_panel.create_agent_thread( - server, - resume_session_id, - work_dirs, - title, - initial_content, - workspace, - project, - ext_agent, - focus, - window, - cx, - ); - })?; + let agent = agent_choice.unwrap_or_else(|| { + if self.project.read(cx).is_via_collab() { + Agent::NativeAgent + } else { + self.selected_agent.clone() + } + }); - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } + let server = agent.server(fs, thread_store); + self.create_agent_thread( + server, + resume_session_id, + work_dirs, + title, + initial_content, + workspace, + project, + agent, + focus, + window, + cx, + ); } fn deploy_rules_library( @@ -2107,15 +2134,25 @@ impl AgentPanel { initial_content: Option, workspace: WeakEntity, project: Entity, - ext_agent: Agent, + agent: Agent, focus: bool, window: &mut Window, cx: &mut Context, ) { - if self.selected_agent != ext_agent { - self.selected_agent = ext_agent.clone(); + if self.selected_agent != agent { + self.selected_agent = agent.clone(); self.serialize(cx); } + + cx.background_spawn({ + let kvp = KeyValueStore::global(cx); + let agent = agent.clone(); + async move { + write_global_last_used_agent(kvp, agent).await; + } + }) + .detach(); + let thread_store = server .clone() .downcast::() @@ -2128,7 +2165,7 @@ impl AgentPanel { crate::ConversationView::new( server, connection_store, - ext_agent, + agent, resume_session_id, work_dirs, title, @@ -5616,4 +5653,211 @@ mod tests { "Thread A work_dirs should revert to only /project_a after removing /project_b" ); } + + #[gpui::test] + async fn test_new_workspace_inherits_global_last_used_agent(cx: &mut TestAppContext) { + init_test(cx); + cx.update(|cx| { + cx.update_flags(true, vec!["agent-v2".to_string()]); + agent::ThreadStore::init_global(cx); + language_model::LanguageModelRegistry::test(cx); + // Use an isolated DB so parallel tests can't overwrite our global key. + cx.set_global(db::AppDatabase::test_new()); + }); + + let custom_agent = Agent::Custom { + id: "my-preferred-agent".into(), + }; + + // Write a known agent to the global KVP to simulate a user who has + // previously used this agent in another workspace. + let kvp = cx.update(|cx| KeyValueStore::global(cx)); + write_global_last_used_agent(kvp, custom_agent.clone()).await; + + let fs = FakeFs::new(cx.executor()); + let project = Project::test(fs.clone(), [], cx).await; + + let multi_workspace = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + + let workspace = multi_workspace + .read_with(cx, |multi_workspace, _cx| { + multi_workspace.workspace().clone() + }) + .unwrap(); + + workspace.update(cx, |workspace, _cx| { + workspace.set_random_database_id(); + }); + + let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx); + + // Load the panel via `load()`, which reads the global fallback + // asynchronously when no per-workspace state exists. + let async_cx = cx.update(|window, cx| window.to_async(cx)); + let panel = AgentPanel::load(workspace.downgrade(), async_cx) + .await + .expect("panel load should succeed"); + cx.run_until_parked(); + + panel.read_with(cx, |panel, _cx| { + assert_eq!( + panel.selected_agent, custom_agent, + "new workspace should inherit the global last-used agent" + ); + }); + } + + #[gpui::test] + async fn test_workspaces_maintain_independent_agent_selection(cx: &mut TestAppContext) { + init_test(cx); + cx.update(|cx| { + cx.update_flags(true, vec!["agent-v2".to_string()]); + agent::ThreadStore::init_global(cx); + language_model::LanguageModelRegistry::test(cx); + }); + + let fs = FakeFs::new(cx.executor()); + let project_a = Project::test(fs.clone(), [], cx).await; + let project_b = Project::test(fs, [], cx).await; + + let multi_workspace = + cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx)); + + let workspace_a = multi_workspace + .read_with(cx, |multi_workspace, _cx| { + multi_workspace.workspace().clone() + }) + .unwrap(); + + let workspace_b = multi_workspace + .update(cx, |multi_workspace, window, cx| { + multi_workspace.test_add_workspace(project_b.clone(), window, cx) + }) + .unwrap(); + + workspace_a.update(cx, |workspace, _cx| { + workspace.set_random_database_id(); + }); + workspace_b.update(cx, |workspace, _cx| { + workspace.set_random_database_id(); + }); + + let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx); + + let agent_a = Agent::Custom { + id: "agent-alpha".into(), + }; + let agent_b = Agent::Custom { + id: "agent-beta".into(), + }; + + // Set up workspace A with agent_a + let panel_a = workspace_a.update_in(cx, |workspace, window, cx| { + cx.new(|cx| AgentPanel::new(workspace, None, window, cx)) + }); + panel_a.update(cx, |panel, _cx| { + panel.selected_agent = agent_a.clone(); + }); + + // Set up workspace B with agent_b + let panel_b = workspace_b.update_in(cx, |workspace, window, cx| { + cx.new(|cx| AgentPanel::new(workspace, None, window, cx)) + }); + panel_b.update(cx, |panel, _cx| { + panel.selected_agent = agent_b.clone(); + }); + + // Serialize both panels + panel_a.update(cx, |panel, cx| panel.serialize(cx)); + panel_b.update(cx, |panel, cx| panel.serialize(cx)); + cx.run_until_parked(); + + // Load fresh panels from serialized state and verify independence + let async_cx = cx.update(|window, cx| window.to_async(cx)); + let loaded_a = AgentPanel::load(workspace_a.downgrade(), async_cx) + .await + .expect("panel A load should succeed"); + cx.run_until_parked(); + + let async_cx = cx.update(|window, cx| window.to_async(cx)); + let loaded_b = AgentPanel::load(workspace_b.downgrade(), async_cx) + .await + .expect("panel B load should succeed"); + cx.run_until_parked(); + + loaded_a.read_with(cx, |panel, _cx| { + assert_eq!( + panel.selected_agent, agent_a, + "workspace A should restore agent-alpha, not agent-beta" + ); + }); + + loaded_b.read_with(cx, |panel, _cx| { + assert_eq!( + panel.selected_agent, agent_b, + "workspace B should restore agent-beta, not agent-alpha" + ); + }); + } + + #[gpui::test] + async fn test_new_thread_uses_workspace_selected_agent(cx: &mut TestAppContext) { + init_test(cx); + cx.update(|cx| { + cx.update_flags(true, vec!["agent-v2".to_string()]); + agent::ThreadStore::init_global(cx); + language_model::LanguageModelRegistry::test(cx); + }); + + let fs = FakeFs::new(cx.executor()); + let project = Project::test(fs.clone(), [], cx).await; + + let multi_workspace = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + + let workspace = multi_workspace + .read_with(cx, |multi_workspace, _cx| { + multi_workspace.workspace().clone() + }) + .unwrap(); + + workspace.update(cx, |workspace, _cx| { + workspace.set_random_database_id(); + }); + + let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx); + + let custom_agent = Agent::Custom { + id: "my-custom-agent".into(), + }; + + let panel = workspace.update_in(cx, |workspace, window, cx| { + let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx)); + workspace.add_panel(panel.clone(), window, cx); + panel + }); + + // Set selected_agent to a custom agent + panel.update(cx, |panel, _cx| { + panel.selected_agent = custom_agent.clone(); + }); + + // Call new_thread, which internally calls external_thread(None, ...) + // This resolves the agent from self.selected_agent + panel.update_in(cx, |panel, window, cx| { + panel.new_thread(&NewThread, window, cx); + }); + + panel.read_with(cx, |panel, _cx| { + assert_eq!( + panel.selected_agent, custom_agent, + "selected_agent should remain the custom agent after new_thread" + ); + assert!( + panel.active_conversation_view().is_some(), + "a thread should have been created" + ); + }); + } } diff --git a/crates/agent_ui/src/completion_provider.rs b/crates/agent_ui/src/completion_provider.rs index b6be6502b152847822a79bc8c486195345c0a195..6259269834b0add5b87fd9d397e17671d30adb9f 100644 --- a/crates/agent_ui/src/completion_provider.rs +++ b/crates/agent_ui/src/completion_provider.rs @@ -28,7 +28,7 @@ use prompt_store::{PromptStore, UserPromptId}; use rope::Point; use settings::{Settings, TerminalDockPosition}; use terminal::terminal_settings::TerminalSettings; -use terminal_view::terminal_panel::TerminalPanel; +use terminal_view::{TerminalView, terminal_panel::TerminalPanel}; use text::{Anchor, ToOffset as _, ToPoint as _}; use ui::IconName; use ui::prelude::*; @@ -562,8 +562,7 @@ impl PromptCompletionProvider { .collect(); // Collect terminal selections from all terminal views if the terminal panel is visible - let terminal_selections: Vec = - terminal_selections_if_panel_open(workspace, cx); + let terminal_selections: Vec = terminal_selections(workspace, cx); const EDITOR_PLACEHOLDER: &str = "selection "; const TERMINAL_PLACEHOLDER: &str = "terminal "; @@ -1198,7 +1197,7 @@ impl PromptCompletionProvider { }) }); - let has_terminal_selection = !terminal_selections_if_panel_open(workspace, cx).is_empty(); + let has_terminal_selection = !terminal_selections(workspace, cx).is_empty(); if has_editor_selection || has_terminal_selection { entries.push(PromptContextEntry::Action( @@ -2169,28 +2168,45 @@ fn build_code_label_for_path( label.build() } -/// Returns terminal selections from all terminal views if the terminal panel is open. -fn terminal_selections_if_panel_open(workspace: &Entity, cx: &App) -> Vec { - let Some(panel) = workspace.read(cx).panel::(cx) else { - return Vec::new(); - }; +fn terminal_selections(workspace: &Entity, cx: &App) -> Vec { + let mut selections = Vec::new(); - // Check if the dock containing this panel is open - let position = match TerminalSettings::get_global(cx).dock { - TerminalDockPosition::Left => DockPosition::Left, - TerminalDockPosition::Bottom => DockPosition::Bottom, - TerminalDockPosition::Right => DockPosition::Right, - }; - let dock_is_open = workspace + // Check if the active item is a terminal (in a panel or not) + if let Some(terminal_view) = workspace .read(cx) - .dock_at_position(position) - .read(cx) - .is_open(); - if !dock_is_open { - return Vec::new(); + .active_item(cx) + .and_then(|item| item.act_as::(cx)) + { + if let Some(text) = terminal_view + .read(cx) + .terminal() + .read(cx) + .last_content + .selection_text + .clone() + .filter(|text| !text.is_empty()) + { + selections.push(text); + } } - panel.read(cx).terminal_selections(cx) + if let Some(panel) = workspace.read(cx).panel::(cx) { + let position = match TerminalSettings::get_global(cx).dock { + TerminalDockPosition::Left => DockPosition::Left, + TerminalDockPosition::Bottom => DockPosition::Bottom, + TerminalDockPosition::Right => DockPosition::Right, + }; + let dock_is_open = workspace + .read(cx) + .dock_at_position(position) + .read(cx) + .is_open(); + if dock_is_open { + selections.extend(panel.read(cx).terminal_selections(cx)); + } + } + + selections } fn selection_ranges( @@ -2213,17 +2229,8 @@ fn selection_ranges( selections .into_iter() - .map(|s| { - let (start, end) = if s.is_empty() { - let row = multi_buffer::MultiBufferRow(s.start.row); - let line_start = text::Point::new(s.start.row, 0); - let line_end = text::Point::new(s.start.row, snapshot.line_len(row)); - (line_start, line_end) - } else { - (s.start, s.end) - }; - snapshot.anchor_after(start)..snapshot.anchor_before(end) - }) + .filter(|s| !s.is_empty()) + .map(|s| snapshot.anchor_after(s.start)..snapshot.anchor_before(s.end)) .flat_map(|range| { let (start_buffer, start) = buffer.text_anchor_for_position(range.start, cx)?; let (end_buffer, end) = buffer.text_anchor_for_position(range.end, cx)?; diff --git a/crates/agent_ui/src/config_options.rs b/crates/agent_ui/src/config_options.rs index b8cf7e5d57921c7710392911829fc2b5045a0f90..44c0baa232222c0ba7c1d54acdecaabacfa85f12 100644 --- a/crates/agent_ui/src/config_options.rs +++ b/crates/agent_ui/src/config_options.rs @@ -650,7 +650,7 @@ impl PickerDelegate for ConfigOptionPickerDelegate { .end_slot(div().pr_2().when(is_selected, |this| { this.child(Icon::new(IconName::Check).color(Color::Accent)) })) - .end_hover_slot(div().pr_1p5().child({ + .end_slot_on_hover(div().pr_1p5().child({ let (icon, color, tooltip) = if is_favorite { (IconName::StarFilled, Color::Accent, "Unfavorite") } else { diff --git a/crates/agent_ui/src/conversation_view/thread_view.rs b/crates/agent_ui/src/conversation_view/thread_view.rs index 63aa8b8529655a26b99ba74062f8d0a6a4812c5f..b25769eadbe31c35a6261cc9433349a2943617be 100644 --- a/crates/agent_ui/src/conversation_view/thread_view.rs +++ b/crates/agent_ui/src/conversation_view/thread_view.rs @@ -5152,9 +5152,12 @@ impl ThreadView { } pub(crate) fn auto_expand_streaming_thought(&mut self, cx: &mut Context) { - // Only auto-expand thinking blocks in Automatic mode. - // AlwaysExpanded shows them open by default; AlwaysCollapsed keeps them closed. - if AgentSettings::get_global(cx).thinking_display != ThinkingBlockDisplay::Automatic { + let thinking_display = AgentSettings::get_global(cx).thinking_display; + + if !matches!( + thinking_display, + ThinkingBlockDisplay::Auto | ThinkingBlockDisplay::Preview + ) { return; } @@ -5183,6 +5186,13 @@ impl ThreadView { cx.notify(); } } else if self.auto_expanded_thinking_block.is_some() { + if thinking_display == ThinkingBlockDisplay::Auto { + if let Some(key) = self.auto_expanded_thinking_block { + if !self.user_toggled_thinking_blocks.contains(&key) { + self.expanded_thinking_blocks.remove(&key); + } + } + } self.auto_expanded_thinking_block = None; cx.notify(); } @@ -5196,7 +5206,16 @@ impl ThreadView { let thinking_display = AgentSettings::get_global(cx).thinking_display; match thinking_display { - ThinkingBlockDisplay::Automatic => { + ThinkingBlockDisplay::Auto => { + if self.expanded_thinking_blocks.contains(&key) { + self.expanded_thinking_blocks.remove(&key); + self.user_toggled_thinking_blocks.insert(key); + } else { + self.expanded_thinking_blocks.insert(key); + self.user_toggled_thinking_blocks.insert(key); + } + } + ThinkingBlockDisplay::Preview => { let is_user_expanded = self.user_toggled_thinking_blocks.contains(&key); let is_in_expanded_set = self.expanded_thinking_blocks.contains(&key); @@ -5249,7 +5268,11 @@ impl ThreadView { let is_in_expanded_set = self.expanded_thinking_blocks.contains(&key); let (is_open, is_constrained) = match thinking_display { - ThinkingBlockDisplay::Automatic => { + ThinkingBlockDisplay::Auto => { + let is_open = is_user_toggled || is_in_expanded_set; + (is_open, false) + } + ThinkingBlockDisplay::Preview => { let is_open = is_user_toggled || is_in_expanded_set; let is_constrained = is_in_expanded_set && !is_user_toggled; (is_open, is_constrained) diff --git a/crates/agent_ui/src/thread_import.rs b/crates/agent_ui/src/thread_import.rs index 9dd6b5efa0ae1cd3bc19dc6ae6a287218de8c668..f5fc89d3df4991ff5186e2af6d73ad6a840c09a1 100644 --- a/crates/agent_ui/src/thread_import.rs +++ b/crates/agent_ui/src/thread_import.rs @@ -121,18 +121,6 @@ impl ThreadImportModal { .collect() } - fn set_agent_checked(&mut self, agent_id: AgentId, state: ToggleState, cx: &mut Context) { - match state { - ToggleState::Selected => { - self.unchecked_agents.remove(&agent_id); - } - ToggleState::Unselected | ToggleState::Indeterminate => { - self.unchecked_agents.insert(agent_id); - } - } - cx.notify(); - } - fn toggle_agent_checked(&mut self, agent_id: AgentId, cx: &mut Context) { if self.unchecked_agents.contains(&agent_id) { self.unchecked_agents.remove(&agent_id); @@ -283,6 +271,11 @@ impl ModalView for ThreadImportModal {} impl Render for ThreadImportModal { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let has_agents = !self.agent_entries.is_empty(); + let disabled_import_thread = self.is_importing + || !has_agents + || self.unchecked_agents.len() == self.agent_entries.len(); + let agent_rows = self .agent_entries .iter() @@ -295,6 +288,7 @@ impl Render for ThreadImportModal { .rounded() .spacing(ListItemSpacing::Sparse) .focused(is_focused) + .disabled(self.is_importing) .child( h_flex() .w_full() @@ -311,22 +305,14 @@ impl Render for ThreadImportModal { }) .child(Label::new(entry.display_name.clone())), ) - .end_slot( - Checkbox::new( - ("thread-import-agent-checkbox", ix), - if is_checked { - ToggleState::Selected - } else { - ToggleState::Unselected - }, - ) - .on_click({ - let agent_id = entry.agent_id.clone(); - cx.listener(move |this, state: &ToggleState, _window, cx| { - this.set_agent_checked(agent_id.clone(), *state, cx); - }) - }), - ) + .end_slot(Checkbox::new( + ("thread-import-agent-checkbox", ix), + if is_checked { + ToggleState::Selected + } else { + ToggleState::Unselected + }, + )) .on_click({ let agent_id = entry.agent_id.clone(); cx.listener(move |this, _event, _window, cx| { @@ -336,11 +322,6 @@ impl Render for ThreadImportModal { }) .collect::>(); - let has_agents = !self.agent_entries.is_empty(); - let disabled_import_thread = self.is_importing - || !has_agents - || self.unchecked_agents.len() == self.agent_entries.len(); - v_flex() .id("thread-import-modal") .key_context("ThreadImportModal") @@ -373,7 +354,7 @@ impl Render for ThreadImportModal { v_flex() .id("thread-import-agent-list") .max_h(rems_from_px(320.)) - .pb_2() + .pb_1() .overflow_y_scroll() .when(has_agents, |this| this.children(agent_rows)) .when(!has_agents, |this| { diff --git a/crates/agent_ui/src/thread_metadata_store.rs b/crates/agent_ui/src/thread_metadata_store.rs index d4e8e6d37aabe98dc41bf39575b77fd28a3bed08..4c66d57bcfafe98432319a173e7736a581f1d986 100644 --- a/crates/agent_ui/src/thread_metadata_store.rs +++ b/crates/agent_ui/src/thread_metadata_store.rs @@ -55,7 +55,7 @@ fn migrate_thread_metadata(cx: &mut App) { .read(cx) .entries() .filter_map(|entry| { - if existing_entries.contains(&entry.id.0) || entry.folder_paths.is_empty() { + if existing_entries.contains(&entry.id.0) { return None; } @@ -81,6 +81,9 @@ fn migrate_thread_metadata(cx: &mut App) { if is_first_migration { let mut per_project: HashMap> = HashMap::default(); for entry in &mut to_migrate { + if entry.folder_paths.is_empty() { + continue; + } per_project .entry(entry.folder_paths.clone()) .or_default() @@ -316,6 +319,25 @@ impl ThreadMetadataStore { .log_err(); } + pub fn update_working_directories( + &mut self, + session_id: &acp::SessionId, + work_dirs: PathList, + cx: &mut Context, + ) { + if !cx.has_flag::() { + return; + } + + if let Some(thread) = self.threads.get(session_id) { + self.save_internal(ThreadMetadata { + folder_paths: work_dirs, + ..thread.clone() + }); + cx.notify(); + } + } + pub fn archive(&mut self, session_id: &acp::SessionId, cx: &mut Context) { self.update_archived(session_id, true, cx); } @@ -495,7 +517,13 @@ impl ThreadMetadataStore { PathList::new(&paths) }; - let archived = existing_thread.map(|t| t.archived).unwrap_or(false); + // Threads without a folder path (e.g. started in an empty + // window) are archived by default so they don't get lost, + // because they won't show up in the sidebar. Users can reload + // them from the archive. + let archived = existing_thread + .map(|t| t.archived) + .unwrap_or(folder_paths.is_empty()); let metadata = ThreadMetadata { session_id, @@ -994,7 +1022,7 @@ mod tests { store.read(cx).entries().cloned().collect::>() }); - assert_eq!(list.len(), 3); + assert_eq!(list.len(), 4); assert!( list.iter() .all(|metadata| metadata.agent_id.as_ref() == agent::ZED_AGENT_ID.as_ref()) @@ -1013,17 +1041,12 @@ mod tests { .collect::>(); assert!(migrated_session_ids.contains(&"a-session-1")); assert!(migrated_session_ids.contains(&"b-session-0")); - assert!(!migrated_session_ids.contains(&"projectless")); + assert!(migrated_session_ids.contains(&"projectless")); let migrated_entries = list .iter() .filter(|metadata| metadata.session_id.0.as_ref() != "a-session-0") .collect::>(); - assert!( - migrated_entries - .iter() - .all(|metadata| !metadata.folder_paths.is_empty()) - ); assert!(migrated_entries.iter().all(|metadata| metadata.archived)); } @@ -1269,6 +1292,84 @@ mod tests { assert_eq!(metadata_ids, vec![session_id]); } + #[gpui::test] + async fn test_threads_without_project_association_are_archived_by_default( + cx: &mut TestAppContext, + ) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + let project_without_worktree = Project::test(fs.clone(), None::<&Path>, cx).await; + let project_with_worktree = Project::test(fs, [Path::new("/project-a")], cx).await; + let connection = Rc::new(StubAgentConnection::new()); + + let thread_without_worktree = cx + .update(|cx| { + connection.clone().new_session( + project_without_worktree.clone(), + PathList::default(), + cx, + ) + }) + .await + .unwrap(); + let session_without_worktree = + cx.read(|cx| thread_without_worktree.read(cx).session_id().clone()); + + cx.update(|cx| { + thread_without_worktree.update(cx, |thread, cx| { + thread.set_title("No Project Thread".into(), cx).detach(); + }); + }); + cx.run_until_parked(); + + let thread_with_worktree = cx + .update(|cx| { + connection.clone().new_session( + project_with_worktree.clone(), + PathList::default(), + cx, + ) + }) + .await + .unwrap(); + let session_with_worktree = + cx.read(|cx| thread_with_worktree.read(cx).session_id().clone()); + + cx.update(|cx| { + thread_with_worktree.update(cx, |thread, cx| { + thread.set_title("Project Thread".into(), cx).detach(); + }); + }); + cx.run_until_parked(); + + cx.update(|cx| { + let store = ThreadMetadataStore::global(cx); + let store = store.read(cx); + + let without_worktree = store + .entry(&session_without_worktree) + .expect("missing metadata for thread without project association"); + assert!(without_worktree.folder_paths.is_empty()); + assert!( + without_worktree.archived, + "expected thread without project association to be archived" + ); + + let with_worktree = store + .entry(&session_with_worktree) + .expect("missing metadata for thread with project association"); + assert_eq!( + with_worktree.folder_paths, + PathList::new(&[Path::new("/project-a")]) + ); + assert!( + !with_worktree.archived, + "expected thread with project association to remain unarchived" + ); + }); + } + #[gpui::test] async fn test_subagent_threads_excluded_from_sidebar_metadata(cx: &mut TestAppContext) { init_test(cx); diff --git a/crates/agent_ui/src/threads_archive_view.rs b/crates/agent_ui/src/threads_archive_view.rs index 445d86c9ad4e37fa8b2502a754a5264cd1d4dc45..9aca31e1edbe729fccecfc0dd8f0530d2aed2564 100644 --- a/crates/agent_ui/src/threads_archive_view.rs +++ b/crates/agent_ui/src/threads_archive_view.rs @@ -1,5 +1,8 @@ +use std::collections::HashSet; +use std::sync::Arc; + use crate::agent_connection_store::AgentConnectionStore; -use crate::thread_import::{AcpThreadImportOnboarding, ThreadImportModal}; + use crate::thread_metadata_store::{ThreadMetadata, ThreadMetadataStore}; use crate::{Agent, RemoveSelectedThread}; @@ -9,21 +12,32 @@ use agent_settings::AgentSettings; use chrono::{DateTime, Datelike as _, Local, NaiveDate, TimeDelta, Utc}; use editor::Editor; use fs::Fs; +use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, ListState, Render, - SharedString, Subscription, Task, WeakEntity, Window, list, prelude::*, px, + AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, + ListState, Render, SharedString, Subscription, Task, WeakEntity, Window, list, prelude::*, px, }; use itertools::Itertools as _; use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious}; -use project::{AgentId, AgentRegistryStore, AgentServerStore}; +use picker::{ + Picker, PickerDelegate, + highlighted_match_with_paths::{HighlightedMatch, HighlightedMatchWithPaths}, +}; +use project::{AgentId, AgentServerStore}; use settings::Settings as _; use theme::ActiveTheme; use ui::ThreadItem; use ui::{ - Divider, KeyBinding, Tooltip, WithScrollbar, prelude::*, utils::platform_title_bar_height, + Divider, KeyBinding, ListItem, ListItemSpacing, ListSubHeader, Tooltip, WithScrollbar, + prelude::*, utils::platform_title_bar_height, }; +use ui_input::ErasedEditor; use util::ResultExt; -use workspace::{MultiWorkspace, Workspace}; +use util::paths::PathExt; +use workspace::{ + ModalView, PathList, SerializedWorkspaceLocation, Workspace, WorkspaceDb, WorkspaceId, + resolve_worktree_workspaces, +}; use zed_actions::agents_sidebar::FocusSidebarFilter; use zed_actions::editor::{MoveDown, MoveUp}; @@ -112,20 +126,16 @@ pub struct ThreadsArchiveView { filter_editor: Entity, _subscriptions: Vec, _refresh_history_task: Task<()>, + workspace: WeakEntity, agent_connection_store: WeakEntity, agent_server_store: WeakEntity, - agent_registry_store: WeakEntity, - workspace: WeakEntity, - multi_workspace: WeakEntity, } impl ThreadsArchiveView { pub fn new( + workspace: WeakEntity, agent_connection_store: WeakEntity, agent_server_store: WeakEntity, - agent_registry_store: WeakEntity, - workspace: WeakEntity, - multi_workspace: WeakEntity, window: &mut Window, cx: &mut Context, ) -> Self { @@ -184,11 +194,9 @@ impl ThreadsArchiveView { thread_metadata_store_subscription, ], _refresh_history_task: Task::ready(()), - agent_registry_store, + workspace, agent_connection_store, agent_server_store, - workspace, - multi_workspace, }; this.update_items(cx); @@ -265,7 +273,14 @@ impl ThreadsArchiveView { self.list_state.reset(items.len()); self.items = items; - self.hovered_index = None; + + if !preserve { + self.hovered_index = None; + } else if let Some(ix) = self.hovered_index { + if ix >= self.items.len() || !self.is_selectable_item(ix) { + self.hovered_index = None; + } + } if let Some(scroll_top) = saved_scroll { self.list_state.scroll_to(scroll_top); @@ -299,11 +314,57 @@ impl ThreadsArchiveView { window: &mut Window, cx: &mut Context, ) { + if thread.folder_paths.is_empty() { + self.show_project_picker_for_thread(thread, window, cx); + return; + } + self.selection = None; self.reset_filter_editor_text(window, cx); cx.emit(ThreadsArchiveViewEvent::Unarchive { thread }); } + fn show_project_picker_for_thread( + &mut self, + thread: ThreadMetadata, + window: &mut Window, + cx: &mut Context, + ) { + let Some(workspace) = self.workspace.upgrade() else { + return; + }; + + let archive_view = cx.weak_entity(); + let fs = workspace.read(cx).app_state().fs.clone(); + let current_workspace_id = workspace.read(cx).database_id(); + let sibling_workspace_ids: HashSet = workspace + .read(cx) + .multi_workspace() + .and_then(|mw| mw.upgrade()) + .map(|mw| { + mw.read(cx) + .workspaces() + .iter() + .filter_map(|ws| ws.read(cx).database_id()) + .collect() + }) + .unwrap_or_default(); + + workspace.update(cx, |workspace, cx| { + workspace.toggle_modal(window, cx, |window, cx| { + ProjectPickerModal::new( + thread, + fs, + archive_view, + current_workspace_id, + sibling_workspace_ids, + window, + cx, + ) + }); + }); + } + fn is_selectable_item(&self, ix: usize) -> bool { matches!(self.items.get(ix), Some(ArchiveListItem::Entry { .. })) } @@ -391,10 +452,6 @@ impl ThreadsArchiveView { return; }; - if thread.folder_paths.is_empty() { - return; - } - self.unarchive_thread(thread.clone(), window, cx); } @@ -482,6 +539,7 @@ impl ThreadsArchiveView { let agent = thread.agent_id.clone(); let session_id = thread.session_id.clone(); cx.listener(move |this, _, _, cx| { + this.preserve_selection_on_next_update = true; this.delete_thread(session_id.clone(), agent.clone(), cx); cx.stop_propagation(); }) @@ -550,43 +608,6 @@ impl ThreadsArchiveView { .detach_and_log_err(cx); } - fn should_render_acp_import_onboarding(&self, cx: &App) -> bool { - let has_external_agents = self - .agent_server_store - .upgrade() - .map(|store| store.read(cx).has_external_agents()) - .unwrap_or(false); - - has_external_agents && !AcpThreadImportOnboarding::dismissed(cx) - } - - fn show_thread_import_modal(&mut self, window: &mut Window, cx: &mut Context) { - let Some(agent_server_store) = self.agent_server_store.upgrade() else { - return; - }; - let Some(agent_registry_store) = self.agent_registry_store.upgrade() else { - return; - }; - - let workspace_handle = self.workspace.clone(); - let multi_workspace = self.multi_workspace.clone(); - - self.workspace - .update(cx, |workspace, cx| { - workspace.toggle_modal(window, cx, |window, cx| { - ThreadImportModal::new( - agent_server_store, - agent_registry_store, - workspace_handle.clone(), - multi_workspace.clone(), - window, - cx, - ) - }); - }) - .log_err(); - } - fn render_header(&self, window: &Window, cx: &mut Context) -> impl IntoElement { let has_query = !self.filter_editor.read(cx).text(cx).is_empty(); let sidebar_on_left = matches!( @@ -729,28 +750,536 @@ impl Render for ThreadsArchiveView { .size_full() .child(self.render_header(window, cx)) .child(content) - .when(!self.should_render_acp_import_onboarding(cx), |this| { - this.child( - div() - .w_full() - .p_1p5() - .border_t_1() - .border_color(cx.theme().colors().border) + } +} + +struct ProjectPickerModal { + picker: Entity>, + _subscription: Subscription, +} + +impl ProjectPickerModal { + fn new( + thread: ThreadMetadata, + fs: Arc, + archive_view: WeakEntity, + current_workspace_id: Option, + sibling_workspace_ids: HashSet, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let delegate = ProjectPickerDelegate { + thread, + archive_view, + workspaces: Vec::new(), + filtered_entries: Vec::new(), + selected_index: 0, + current_workspace_id, + sibling_workspace_ids, + focus_handle: cx.focus_handle(), + }; + + let picker = cx.new(|cx| { + Picker::list(delegate, window, cx) + .list_measure_all() + .modal(false) + }); + + let picker_focus_handle = picker.focus_handle(cx); + picker.update(cx, |picker, _| { + picker.delegate.focus_handle = picker_focus_handle; + }); + + let _subscription = + cx.subscribe(&picker, |_this: &mut Self, _, _event: &DismissEvent, cx| { + cx.emit(DismissEvent); + }); + + let db = WorkspaceDb::global(cx); + cx.spawn_in(window, async move |this, cx| { + let workspaces = db + .recent_workspaces_on_disk(fs.as_ref()) + .await + .log_err() + .unwrap_or_default(); + let workspaces = resolve_worktree_workspaces(workspaces, fs.as_ref()).await; + this.update_in(cx, move |this, window, cx| { + this.picker.update(cx, move |picker, cx| { + picker.delegate.workspaces = workspaces; + picker.update_matches(picker.query(cx), window, cx) + }) + }) + .ok(); + }) + .detach(); + + picker.focus_handle(cx).focus(window, cx); + + Self { + picker, + _subscription, + } + } +} + +impl EventEmitter for ProjectPickerModal {} + +impl Focusable for ProjectPickerModal { + fn focus_handle(&self, cx: &App) -> FocusHandle { + self.picker.focus_handle(cx) + } +} + +impl ModalView for ProjectPickerModal {} + +impl Render for ProjectPickerModal { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + v_flex() + .key_context("ProjectPickerModal") + .elevation_3(cx) + .w(rems(34.)) + .on_action(cx.listener(|this, _: &workspace::Open, window, cx| { + this.picker.update(cx, |picker, cx| { + picker.delegate.open_local_folder(window, cx) + }) + })) + .child(self.picker.clone()) + } +} + +enum ProjectPickerEntry { + Header(SharedString), + Workspace(StringMatch), +} + +struct ProjectPickerDelegate { + thread: ThreadMetadata, + archive_view: WeakEntity, + current_workspace_id: Option, + sibling_workspace_ids: HashSet, + workspaces: Vec<( + WorkspaceId, + SerializedWorkspaceLocation, + PathList, + DateTime, + )>, + filtered_entries: Vec, + selected_index: usize, + focus_handle: FocusHandle, +} + +impl ProjectPickerDelegate { + fn update_working_directories_and_unarchive( + &mut self, + paths: PathList, + window: &mut Window, + cx: &mut Context>, + ) { + self.thread.folder_paths = paths.clone(); + ThreadMetadataStore::global(cx).update(cx, |store, cx| { + store.update_working_directories(&self.thread.session_id, paths, cx); + }); + + self.archive_view + .update(cx, |view, cx| { + view.selection = None; + view.reset_filter_editor_text(window, cx); + cx.emit(ThreadsArchiveViewEvent::Unarchive { + thread: self.thread.clone(), + }); + }) + .log_err(); + } + + fn is_current_workspace(&self, workspace_id: WorkspaceId) -> bool { + self.current_workspace_id == Some(workspace_id) + } + + fn is_sibling_workspace(&self, workspace_id: WorkspaceId) -> bool { + self.sibling_workspace_ids.contains(&workspace_id) + && !self.is_current_workspace(workspace_id) + } + + fn selected_match(&self) -> Option<&StringMatch> { + match self.filtered_entries.get(self.selected_index)? { + ProjectPickerEntry::Workspace(hit) => Some(hit), + ProjectPickerEntry::Header(_) => None, + } + } + + fn open_local_folder(&mut self, window: &mut Window, cx: &mut Context>) { + let paths_receiver = cx.prompt_for_paths(gpui::PathPromptOptions { + files: false, + directories: true, + multiple: false, + prompt: None, + }); + cx.spawn_in(window, async move |this, cx| { + let Ok(Ok(Some(paths))) = paths_receiver.await else { + return; + }; + if paths.is_empty() { + return; + } + + let work_dirs = PathList::new(&paths); + + this.update_in(cx, |this, window, cx| { + this.delegate + .update_working_directories_and_unarchive(work_dirs, window, cx); + cx.emit(DismissEvent); + }) + .log_err(); + }) + .detach(); + } +} + +impl EventEmitter for ProjectPickerDelegate {} + +impl PickerDelegate for ProjectPickerDelegate { + type ListItem = AnyElement; + + fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { + format!("Associate the \"{}\" thread with...", self.thread.title).into() + } + + fn render_editor( + &self, + editor: &Arc, + window: &mut Window, + cx: &mut Context>, + ) -> Div { + h_flex() + .flex_none() + .h_9() + .px_2p5() + .justify_between() + .border_b_1() + .border_color(cx.theme().colors().border_variant) + .child(editor.render(window, cx)) + } + + fn match_count(&self) -> usize { + self.filtered_entries.len() + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index( + &mut self, + ix: usize, + _window: &mut Window, + _cx: &mut Context>, + ) { + self.selected_index = ix; + } + + fn can_select(&self, ix: usize, _window: &mut Window, _cx: &mut Context>) -> bool { + matches!( + self.filtered_entries.get(ix), + Some(ProjectPickerEntry::Workspace(_)) + ) + } + + fn update_matches( + &mut self, + query: String, + _window: &mut Window, + cx: &mut Context>, + ) -> Task<()> { + let query = query.trim_start(); + let smart_case = query.chars().any(|c| c.is_uppercase()); + let is_empty_query = query.is_empty(); + + let sibling_candidates: Vec<_> = self + .workspaces + .iter() + .enumerate() + .filter(|(_, (id, _, _, _))| self.is_sibling_workspace(*id)) + .map(|(id, (_, _, paths, _))| { + let combined_string = paths + .ordered_paths() + .map(|path| path.compact().to_string_lossy().into_owned()) + .collect::>() + .join(""); + StringMatchCandidate::new(id, &combined_string) + }) + .collect(); + + let mut sibling_matches = smol::block_on(fuzzy::match_strings( + &sibling_candidates, + query, + smart_case, + true, + 100, + &Default::default(), + cx.background_executor().clone(), + )); + + sibling_matches.sort_unstable_by(|a, b| { + b.score + .partial_cmp(&a.score) + .unwrap_or(std::cmp::Ordering::Equal) + .then_with(|| a.candidate_id.cmp(&b.candidate_id)) + }); + + let recent_candidates: Vec<_> = self + .workspaces + .iter() + .enumerate() + .filter(|(_, (id, _, _, _))| { + !self.is_current_workspace(*id) && !self.is_sibling_workspace(*id) + }) + .map(|(id, (_, _, paths, _))| { + let combined_string = paths + .ordered_paths() + .map(|path| path.compact().to_string_lossy().into_owned()) + .collect::>() + .join(""); + StringMatchCandidate::new(id, &combined_string) + }) + .collect(); + + let mut recent_matches = smol::block_on(fuzzy::match_strings( + &recent_candidates, + query, + smart_case, + true, + 100, + &Default::default(), + cx.background_executor().clone(), + )); + + recent_matches.sort_unstable_by(|a, b| { + b.score + .partial_cmp(&a.score) + .unwrap_or(std::cmp::Ordering::Equal) + .then_with(|| a.candidate_id.cmp(&b.candidate_id)) + }); + + let mut entries = Vec::new(); + + let has_siblings_to_show = if is_empty_query { + !sibling_candidates.is_empty() + } else { + !sibling_matches.is_empty() + }; + + if has_siblings_to_show { + entries.push(ProjectPickerEntry::Header("This Window".into())); + + if is_empty_query { + for (id, (workspace_id, _, _, _)) in self.workspaces.iter().enumerate() { + if self.is_sibling_workspace(*workspace_id) { + entries.push(ProjectPickerEntry::Workspace(StringMatch { + candidate_id: id, + score: 0.0, + positions: Vec::new(), + string: String::new(), + })); + } + } + } else { + for m in sibling_matches { + entries.push(ProjectPickerEntry::Workspace(m)); + } + } + } + + let has_recent_to_show = if is_empty_query { + !recent_candidates.is_empty() + } else { + !recent_matches.is_empty() + }; + + if has_recent_to_show { + entries.push(ProjectPickerEntry::Header("Recent Projects".into())); + + if is_empty_query { + for (id, (workspace_id, _, _, _)) in self.workspaces.iter().enumerate() { + if !self.is_current_workspace(*workspace_id) + && !self.is_sibling_workspace(*workspace_id) + { + entries.push(ProjectPickerEntry::Workspace(StringMatch { + candidate_id: id, + score: 0.0, + positions: Vec::new(), + string: String::new(), + })); + } + } + } else { + for m in recent_matches { + entries.push(ProjectPickerEntry::Workspace(m)); + } + } + } + + self.filtered_entries = entries; + + self.selected_index = self + .filtered_entries + .iter() + .position(|e| matches!(e, ProjectPickerEntry::Workspace(_))) + .unwrap_or(0); + + Task::ready(()) + } + + fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context>) { + let candidate_id = match self.filtered_entries.get(self.selected_index) { + Some(ProjectPickerEntry::Workspace(hit)) => hit.candidate_id, + _ => return, + }; + let Some((_workspace_id, _location, paths, _)) = self.workspaces.get(candidate_id) else { + return; + }; + + self.update_working_directories_and_unarchive(paths.clone(), window, cx); + cx.emit(DismissEvent); + } + + fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context>) {} + + fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option { + let text = if self.workspaces.is_empty() { + "No recent projects found" + } else { + "No matches" + }; + Some(text.into()) + } + + fn render_match( + &self, + ix: usize, + selected: bool, + window: &mut Window, + cx: &mut Context>, + ) -> Option { + match self.filtered_entries.get(ix)? { + ProjectPickerEntry::Header(title) => Some( + v_flex() + .w_full() + .gap_1() + .when(ix > 0, |this| this.mt_1().child(Divider::horizontal())) + .child(ListSubHeader::new(title.clone()).inset(true)) + .into_any_element(), + ), + ProjectPickerEntry::Workspace(hit) => { + let (_, location, paths, _) = self.workspaces.get(hit.candidate_id)?; + + let ordered_paths: Vec<_> = paths + .ordered_paths() + .map(|p| p.compact().to_string_lossy().to_string()) + .collect(); + + let tooltip_path: SharedString = ordered_paths.join("\n").into(); + + let mut path_start_offset = 0; + let match_labels: Vec<_> = paths + .ordered_paths() + .map(|p| p.compact()) + .map(|path| { + let path_string = path.to_string_lossy(); + let path_text = path_string.to_string(); + let path_byte_len = path_text.len(); + + let path_positions: Vec = hit + .positions + .iter() + .copied() + .skip_while(|pos| *pos < path_start_offset) + .take_while(|pos| *pos < path_start_offset + path_byte_len) + .map(|pos| pos - path_start_offset) + .collect(); + + let file_name_match = path.file_name().map(|file_name| { + let file_name_text = file_name.to_string_lossy().into_owned(); + let file_name_start = path_byte_len - file_name_text.len(); + let highlight_positions: Vec = path_positions + .iter() + .copied() + .skip_while(|pos| *pos < file_name_start) + .take_while(|pos| *pos < file_name_start + file_name_text.len()) + .map(|pos| pos - file_name_start) + .collect(); + HighlightedMatch { + text: file_name_text, + highlight_positions, + color: Color::Default, + } + }); + + path_start_offset += path_byte_len; + file_name_match + }) + .collect(); + + let highlighted_match = HighlightedMatchWithPaths { + prefix: match location { + SerializedWorkspaceLocation::Remote(options) => { + Some(SharedString::from(options.display_name())) + } + _ => None, + }, + match_label: HighlightedMatch::join(match_labels.into_iter().flatten(), ", "), + paths: Vec::new(), + }; + + Some( + ListItem::new(ix) + .toggle_state(selected) + .inset(true) + .spacing(ListItemSpacing::Sparse) .child( - Button::new("import-acp", "Import ACP Threads") - .full_width() - .style(ButtonStyle::OutlinedCustom(cx.theme().colors().border)) - .label_size(LabelSize::Small) - .start_icon( - Icon::new(IconName::ArrowDown) - .size(IconSize::XSmall) - .color(Color::Muted), - ) - .on_click(cx.listener(|this, _, window, cx| { - this.show_thread_import_modal(window, cx); - })), - ), + h_flex() + .gap_3() + .flex_grow() + .child(highlighted_match.render(window, cx)), + ) + .tooltip(Tooltip::text(tooltip_path)) + .into_any_element(), ) - }) + } + } + } + + fn render_footer(&self, _: &mut Window, cx: &mut Context>) -> Option { + let has_selection = self.selected_match().is_some(); + let focus_handle = self.focus_handle.clone(); + + Some( + h_flex() + .flex_1() + .p_1p5() + .gap_1() + .justify_end() + .border_t_1() + .border_color(cx.theme().colors().border_variant) + .child( + Button::new("open_local_folder", "Choose from Local Folders") + .key_binding(KeyBinding::for_action_in( + &workspace::Open::default(), + &focus_handle, + cx, + )) + .on_click(cx.listener(|this, _, window, cx| { + this.delegate.open_local_folder(window, cx); + })), + ) + .child( + Button::new("select_project", "Select") + .disabled(!has_selection) + .key_binding(KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx)) + .on_click(cx.listener(move |picker, _, window, cx| { + picker.delegate.confirm(false, window, cx); + })), + ) + .into_any(), + ) } } diff --git a/crates/agent_ui/src/ui.rs b/crates/agent_ui/src/ui.rs index 16732951ce67d76ca8d65259e309c4b81df30c3b..d43b7e4b043bcd1b155699c5eea3ca695585b94b 100644 --- a/crates/agent_ui/src/ui.rs +++ b/crates/agent_ui/src/ui.rs @@ -1,6 +1,5 @@ mod acp_onboarding_modal; mod agent_notification; -mod claude_agent_onboarding_modal; mod end_trial_upsell; mod hold_for_default; mod mention_crease; @@ -9,7 +8,6 @@ mod undo_reject_toast; pub use acp_onboarding_modal::*; pub use agent_notification::*; -pub use claude_agent_onboarding_modal::*; pub use end_trial_upsell::*; pub use hold_for_default::*; pub use mention_crease::*; diff --git a/crates/agent_ui/src/ui/claude_agent_onboarding_modal.rs b/crates/agent_ui/src/ui/claude_agent_onboarding_modal.rs deleted file mode 100644 index 5b7e58eb4fd79a5075446dad997c2642fedf32a6..0000000000000000000000000000000000000000 --- a/crates/agent_ui/src/ui/claude_agent_onboarding_modal.rs +++ /dev/null @@ -1,261 +0,0 @@ -use agent_servers::CLAUDE_AGENT_ID; -use gpui::{ - ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render, - linear_color_stop, linear_gradient, -}; -use ui::{TintColor, Vector, VectorName, prelude::*}; -use workspace::{ModalView, Workspace}; - -use crate::{Agent, agent_panel::AgentPanel}; - -macro_rules! claude_agent_onboarding_event { - ($name:expr) => { - telemetry::event!($name, source = "ACP Claude Code Onboarding"); - }; - ($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => { - telemetry::event!($name, source = "ACP Claude Code Onboarding", $($key $(= $value)?),+); - }; -} - -pub struct ClaudeCodeOnboardingModal { - focus_handle: FocusHandle, - workspace: Entity, -} - -impl ClaudeCodeOnboardingModal { - pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context) { - let workspace_entity = cx.entity(); - workspace.toggle_modal(window, cx, |_window, cx| Self { - workspace: workspace_entity, - focus_handle: cx.focus_handle(), - }); - } - - fn open_panel(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { - self.workspace.update(cx, |workspace, cx| { - workspace.focus_panel::(window, cx); - - if let Some(panel) = workspace.panel::(cx) { - panel.update(cx, |panel, cx| { - panel.new_agent_thread( - Agent::Custom { - id: CLAUDE_AGENT_ID.into(), - }, - window, - cx, - ); - }); - } - }); - - cx.emit(DismissEvent); - - claude_agent_onboarding_event!("Open Panel Clicked"); - } - - fn view_docs(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { - window.dispatch_action(Box::new(zed_actions::AcpRegistry), cx); - cx.notify(); - - claude_agent_onboarding_event!("Documentation Link Clicked"); - } - - fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context) { - cx.emit(DismissEvent); - } -} - -impl EventEmitter for ClaudeCodeOnboardingModal {} - -impl Focusable for ClaudeCodeOnboardingModal { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl ModalView for ClaudeCodeOnboardingModal {} - -impl Render for ClaudeCodeOnboardingModal { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let illustration_element = |icon: IconName, label: Option, opacity: f32| { - h_flex() - .px_1() - .py_0p5() - .gap_1() - .rounded_sm() - .bg(cx.theme().colors().element_active.opacity(0.05)) - .border_1() - .border_color(cx.theme().colors().border) - .border_dashed() - .child( - Icon::new(icon) - .size(IconSize::Small) - .color(Color::Custom(cx.theme().colors().text_muted.opacity(0.15))), - ) - .map(|this| { - if let Some(label_text) = label { - this.child( - Label::new(label_text) - .size(LabelSize::Small) - .color(Color::Muted), - ) - } else { - this.child( - div().w_16().h_1().rounded_full().bg(cx - .theme() - .colors() - .element_active - .opacity(0.6)), - ) - } - }) - .opacity(opacity) - }; - - let illustration = h_flex() - .relative() - .h(rems_from_px(126.)) - .bg(cx.theme().colors().editor_background) - .border_b_1() - .border_color(cx.theme().colors().border_variant) - .justify_center() - .gap_8() - .rounded_t_md() - .overflow_hidden() - .child( - div().absolute().inset_0().w(px(515.)).h(px(126.)).child( - Vector::new(VectorName::AcpGrid, rems_from_px(515.), rems_from_px(126.)) - .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.02))), - ), - ) - .child(div().absolute().inset_0().size_full().bg(linear_gradient( - 0., - linear_color_stop( - cx.theme().colors().elevated_surface_background.opacity(0.1), - 0.9, - ), - linear_color_stop( - cx.theme().colors().elevated_surface_background.opacity(0.), - 0., - ), - ))) - .child( - div() - .absolute() - .inset_0() - .size_full() - .bg(gpui::black().opacity(0.15)), - ) - .child( - Vector::new( - VectorName::AcpLogoSerif, - rems_from_px(257.), - rems_from_px(47.), - ) - .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))), - ) - .child( - v_flex() - .gap_1p5() - .child(illustration_element(IconName::Stop, None, 0.15)) - .child(illustration_element( - IconName::AiGemini, - Some("New Gemini CLI Thread".into()), - 0.3, - )) - .child( - h_flex() - .pl_1() - .pr_2() - .py_0p5() - .gap_1() - .rounded_sm() - .bg(cx.theme().colors().element_active.opacity(0.2)) - .border_1() - .border_color(cx.theme().colors().border) - .child( - Icon::new(IconName::AiClaude) - .size(IconSize::Small) - .color(Color::Muted), - ) - .child(Label::new("New Claude Agent Thread").size(LabelSize::Small)), - ) - .child(illustration_element( - IconName::Stop, - Some("Your Agent Here".into()), - 0.3, - )) - .child(illustration_element(IconName::Stop, None, 0.15)), - ); - - let heading = v_flex() - .w_full() - .gap_1() - .child( - Label::new("Beta Release") - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child(Headline::new("Claude Agent: Natively in Zed").size(HeadlineSize::Large)); - - let copy = "Powered by the Agent Client Protocol, you can now run Claude Agent as\na first-class citizen in Zed's agent panel."; - - let open_panel_button = Button::new("open-panel", "Start with Claude Agent") - .style(ButtonStyle::Tinted(TintColor::Accent)) - .full_width() - .on_click(cx.listener(Self::open_panel)); - - let docs_button = Button::new("add-other-agents", "Add Other Agents") - .end_icon( - Icon::new(IconName::ArrowUpRight) - .size(IconSize::Indicator) - .color(Color::Muted), - ) - .full_width() - .on_click(cx.listener(Self::view_docs)); - - let close_button = h_flex().absolute().top_2().right_2().child( - IconButton::new("cancel", IconName::Close).on_click(cx.listener( - |_, _: &ClickEvent, _window, cx| { - claude_agent_onboarding_event!("Canceled", trigger = "X click"); - cx.emit(DismissEvent); - }, - )), - ); - - v_flex() - .id("acp-onboarding") - .key_context("AcpOnboardingModal") - .relative() - .w(rems(34.)) - .h_full() - .elevation_3(cx) - .track_focus(&self.focus_handle(cx)) - .overflow_hidden() - .on_action(cx.listener(Self::cancel)) - .on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| { - claude_agent_onboarding_event!("Canceled", trigger = "Action"); - cx.emit(DismissEvent); - })) - .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, cx| { - this.focus_handle.focus(window, cx); - })) - .child(illustration) - .child( - v_flex() - .p_4() - .gap_2() - .child(heading) - .child(Label::new(copy).color(Color::Muted)) - .child( - v_flex() - .w_full() - .mt_2() - .gap_1() - .child(open_panel_button) - .child(docs_button), - ), - ) - .child(close_button) - } -} diff --git a/crates/agent_ui/src/ui/model_selector_components.rs b/crates/agent_ui/src/ui/model_selector_components.rs index 01ba6c4511854e83b97b1fc053e41e5d0e82ff1e..88bf546a0e7beef53c8043fd04f8e6e9e5e92c88 100644 --- a/crates/agent_ui/src/ui/model_selector_components.rs +++ b/crates/agent_ui/src/ui/model_selector_components.rs @@ -160,7 +160,7 @@ impl RenderOnce for ModelSelectorListItem { .end_slot(div().pr_2().when(self.is_selected, |this| { this.child(Icon::new(IconName::Check).color(Color::Accent)) })) - .end_hover_slot(div().pr_1p5().when_some(self.on_toggle_favorite, { + .end_slot_on_hover(div().pr_1p5().when_some(self.on_toggle_favorite, { |this, handle_click| { let (icon, color, tooltip) = if is_favorite { (IconName::StarFilled, Color::Accent, "Unfavorite Model") diff --git a/crates/csv_preview/src/csv_preview.rs b/crates/csv_preview/src/csv_preview.rs index b0b6ad4186758fd33693d5ee29bd2f0d4d28b816..c38cefb2456b3f44e3cac61b02294ab1ed1e79f4 100644 --- a/crates/csv_preview/src/csv_preview.rs +++ b/crates/csv_preview/src/csv_preview.rs @@ -9,7 +9,10 @@ use std::{ }; use crate::table_data_engine::TableDataEngine; -use ui::{SharedString, TableColumnWidths, TableInteractionState, prelude::*}; +use ui::{ + AbsoluteLength, DefiniteLength, RedistributableColumnsState, SharedString, + TableInteractionState, TableResizeBehavior, prelude::*, +}; use workspace::{Item, SplitDirection, Workspace}; use crate::{parser::EditorState, settings::CsvPreviewSettings, types::TableLikeContent}; @@ -52,6 +55,32 @@ pub fn init(cx: &mut App) { } impl CsvPreviewView { + pub(crate) fn sync_column_widths(&self, cx: &mut Context) { + // plus 1 for the rows column + let cols = self.engine.contents.headers.cols() + 1; + let remaining_col_number = cols.saturating_sub(1); + let fraction = if remaining_col_number > 0 { + 1. / remaining_col_number as f32 + } else { + 1. + }; + let mut widths = vec![DefiniteLength::Fraction(fraction); cols]; + let line_number_width = self.calculate_row_identifier_column_width(); + widths[0] = DefiniteLength::Absolute(AbsoluteLength::Pixels(line_number_width.into())); + + let mut resize_behaviors = vec![TableResizeBehavior::Resizable; cols]; + resize_behaviors[0] = TableResizeBehavior::None; + + self.column_widths.widths.update(cx, |state, _cx| { + if state.cols() != cols + || state.initial_widths().as_slice() != widths.as_slice() + || state.resize_behavior().as_slice() != resize_behaviors.as_slice() + { + *state = RedistributableColumnsState::new(cols, widths, resize_behaviors); + } + }); + } + pub fn register(workspace: &mut Workspace) { workspace.register_action_renderer(|div, _, _, cx| { div.when(cx.has_flag::(), |div| { @@ -286,18 +315,19 @@ impl PerformanceMetrics { /// Holds state of column widths for a table component in CSV preview. pub(crate) struct ColumnWidths { - pub widths: Entity, + pub widths: Entity, } impl ColumnWidths { pub(crate) fn new(cx: &mut Context, cols: usize) -> Self { Self { - widths: cx.new(|cx| TableColumnWidths::new(cols, cx)), + widths: cx.new(|_cx| { + RedistributableColumnsState::new( + cols, + vec![ui::DefiniteLength::Fraction(1.0 / cols as f32); cols], + vec![ui::TableResizeBehavior::Resizable; cols], + ) + }), } } - /// Replace the current `TableColumnWidths` entity with a new one for the given column count. - pub(crate) fn replace(&self, cx: &mut Context, cols: usize) { - self.widths - .update(cx, |entity, cx| *entity = TableColumnWidths::new(cols, cx)); - } } diff --git a/crates/csv_preview/src/parser.rs b/crates/csv_preview/src/parser.rs index b087404e0ebbd13cdaf20cab692f5470ea6ce292..efa3573d7aa53d97e2801ff00feb4665072830f4 100644 --- a/crates/csv_preview/src/parser.rs +++ b/crates/csv_preview/src/parser.rs @@ -80,11 +80,8 @@ impl CsvPreviewView { .insert("Parsing", (parse_duration, Instant::now())); log::debug!("Parsed {} rows", parsed_csv.rows.len()); - // Update table width so it can be rendered properly - let cols = parsed_csv.headers.cols(); - view.column_widths.replace(cx, cols + 1); // Add 1 for the line number column - view.engine.contents = parsed_csv; + view.sync_column_widths(cx); view.last_parse_end_time = Some(parse_end_time); view.apply_filter_sort(); diff --git a/crates/csv_preview/src/renderer/render_table.rs b/crates/csv_preview/src/renderer/render_table.rs index 0cc3bc3c46fb24570b3c99c9121dff3860c6b820..fb3d7e5fc603ba5b109319cfb19466dc3ad7652f 100644 --- a/crates/csv_preview/src/renderer/render_table.rs +++ b/crates/csv_preview/src/renderer/render_table.rs @@ -1,11 +1,9 @@ use crate::types::TableCell; use gpui::{AnyElement, Entity}; use std::ops::Range; -use ui::Table; -use ui::TableColumnWidths; -use ui::TableResizeBehavior; -use ui::UncheckedTableRow; -use ui::{DefiniteLength, div, prelude::*}; +use ui::{ + ColumnWidthConfig, RedistributableColumnsState, Table, UncheckedTableRow, div, prelude::*, +}; use crate::{ CsvPreviewView, @@ -15,44 +13,22 @@ use crate::{ impl CsvPreviewView { /// Creates a new table. - /// Column number is derived from the `TableColumnWidths` entity. + /// Column number is derived from the `RedistributableColumnsState` entity. pub(crate) fn create_table( &self, - current_widths: &Entity, + current_widths: &Entity, cx: &mut Context, ) -> AnyElement { - let cols = current_widths.read(cx).cols(); - let remaining_col_number = cols - 1; - let fraction = if remaining_col_number > 0 { - 1. / remaining_col_number as f32 - } else { - 1. // only column with line numbers is present. Put 100%, but it will be overwritten anyways :D - }; - let mut widths = vec![DefiniteLength::Fraction(fraction); cols]; - let line_number_width = self.calculate_row_identifier_column_width(); - widths[0] = DefiniteLength::Absolute(AbsoluteLength::Pixels(line_number_width.into())); - - let mut resize_behaviors = vec![TableResizeBehavior::Resizable; cols]; - resize_behaviors[0] = TableResizeBehavior::None; - - self.create_table_inner( - self.engine.contents.rows.len(), - widths, - resize_behaviors, - current_widths, - cx, - ) + self.create_table_inner(self.engine.contents.rows.len(), current_widths, cx) } fn create_table_inner( &self, row_count: usize, - widths: UncheckedTableRow, - resize_behaviors: UncheckedTableRow, - current_widths: &Entity, + current_widths: &Entity, cx: &mut Context, ) -> AnyElement { - let cols = widths.len(); + let cols = current_widths.read(cx).cols(); // Create headers array with interactive elements let mut headers = Vec::with_capacity(cols); @@ -78,8 +54,7 @@ impl CsvPreviewView { Table::new(cols) .interactable(&self.table_interaction_state) .striped() - .column_widths(widths) - .resizable_columns(resize_behaviors, current_widths, cx) + .width_config(ColumnWidthConfig::redistributable(current_widths.clone())) .header(headers) .disable_base_style() .map(|table| { diff --git a/crates/csv_preview/src/renderer/row_identifiers.rs b/crates/csv_preview/src/renderer/row_identifiers.rs index a122aa9bf3d803b9deb9c6211e117ba4aa593d93..fc8bf68845fd41917e7d60bf5f9276295534c902 100644 --- a/crates/csv_preview/src/renderer/row_identifiers.rs +++ b/crates/csv_preview/src/renderer/row_identifiers.rs @@ -139,6 +139,7 @@ impl CsvPreviewView { RowIdentifiers::SrcLines => RowIdentifiers::RowNum, RowIdentifiers::RowNum => RowIdentifiers::SrcLines, }; + this.sync_column_widths(cx); cx.notify(); }); }), diff --git a/crates/csv_preview/src/renderer/table_cell.rs b/crates/csv_preview/src/renderer/table_cell.rs index 32900ab77708936e218e9af10a4de5fba796e6a7..733488110fbcdb39761b150a74c135426ca6514a 100644 --- a/crates/csv_preview/src/renderer/table_cell.rs +++ b/crates/csv_preview/src/renderer/table_cell.rs @@ -53,7 +53,6 @@ fn create_table_cell( .px_1() .bg(cx.theme().colors().editor_background) .border_b_1() - .border_r_1() .border_color(cx.theme().colors().border_variant) .map(|div| match vertical_alignment { VerticalAlignment::Top => div.items_start(), diff --git a/crates/diagnostics/src/diagnostic_renderer.rs b/crates/diagnostics/src/diagnostic_renderer.rs index 27e1cbbac9c779056ecd9da00dd7a56ff3536f17..62b7f4eadf322da1c57a9f1da60b412d7b0dcd68 100644 --- a/crates/diagnostics/src/diagnostic_renderer.rs +++ b/crates/diagnostics/src/diagnostic_renderer.rs @@ -8,7 +8,7 @@ use editor::{ use gpui::{AppContext, Entity, Focusable, WeakEntity}; use language::{BufferId, Diagnostic, DiagnosticEntryRef, LanguageRegistry}; use lsp::DiagnosticSeverity; -use markdown::{Markdown, MarkdownElement}; +use markdown::{CopyButtonVisibility, Markdown, MarkdownElement}; use settings::Settings; use text::{AnchorRangeExt, Point}; use theme_settings::ThemeSettings; @@ -239,8 +239,7 @@ impl DiagnosticBlock { diagnostics_markdown_style(bcx.window, cx), ) .code_block_renderer(markdown::CodeBlockRenderer::Default { - copy_button: false, - copy_button_on_hover: false, + copy_button_visibility: CopyButtonVisibility::Hidden, border: false, }) .on_url_click({ diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index 3fc6080b4da8ca85d258d04de29d603ea7097623..5d6c037d9b67034423dda9f119a1e78fb1e5b9b2 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -9,7 +9,7 @@ use itertools::Itertools; use language::CodeLabel; use language::{Buffer, LanguageName, LanguageRegistry}; use lsp::CompletionItemTag; -use markdown::{Markdown, MarkdownElement}; +use markdown::{CopyButtonVisibility, Markdown, MarkdownElement}; use multi_buffer::{Anchor, ExcerptId}; use ordered_float::OrderedFloat; use project::lsp_store::CompletionDocumentation; @@ -1118,8 +1118,7 @@ impl CompletionsMenu { div().child( MarkdownElement::new(markdown, hover_markdown_style(window, cx)) .code_block_renderer(markdown::CodeBlockRenderer::Default { - copy_button: false, - copy_button_on_hover: false, + copy_button_visibility: CopyButtonVisibility::Hidden, border: false, }) .on_url_click(open_markdown_url), diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 65a872e6035565bb01fdd78e00d6cf0f35d35ef8..7e397507eda0d800ee9ed6b204ed95e71d50234b 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -32309,6 +32309,91 @@ async fn test_scroll_by_clicking_sticky_header(cx: &mut TestAppContext) { assert_eq!(selections, vec![empty_range(4, 5)]); } +#[gpui::test] +async fn test_clicking_sticky_header_sets_character_select_mode(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + cx.update(|cx| { + SettingsStore::update_global(cx, |store, cx| { + store.update_user_settings(cx, |settings| { + settings.editor.sticky_scroll = Some(settings::StickyScrollContent { + enabled: Some(true), + }) + }); + }); + }); + let mut cx = EditorTestContext::new(cx).await; + + let line_height = cx.update_editor(|editor, window, cx| { + editor + .style(cx) + .text + .line_height_in_pixels(window.rem_size()) + }); + + let buffer = indoc! {" + fn foo() { + let abc = 123; + } + ˇstruct Bar; + "}; + cx.set_state(&buffer); + + cx.update_editor(|editor, _, cx| { + editor + .buffer() + .read(cx) + .as_singleton() + .unwrap() + .update(cx, |buffer, cx| { + buffer.set_language(Some(rust_lang()), cx); + }) + }); + + let text_origin_x = cx.update_editor(|editor, _, _| { + editor + .last_position_map + .as_ref() + .unwrap() + .text_hitbox + .bounds + .origin + .x + }); + + cx.update_editor(|editor, window, cx| { + // Double click on `struct` to select it + editor.begin_selection(DisplayPoint::new(DisplayRow(3), 1), false, 2, window, cx); + editor.end_selection(window, cx); + + // Scroll down one row to make `fn foo() {` a sticky header + editor.scroll(gpui::Point { x: 0., y: 1. }, None, window, cx); + }); + cx.run_until_parked(); + + // Click at the start of the `fn foo() {` sticky header + cx.simulate_click( + gpui::Point { + x: text_origin_x, + y: 0.5 * line_height, + }, + Modifiers::none(), + ); + cx.run_until_parked(); + + // Shift-click at the end of `fn foo() {` to select the whole row + cx.update_editor(|editor, window, cx| { + editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx); + editor.end_selection(window, cx); + }); + cx.run_until_parked(); + + let selections = cx.update_editor(|editor, _, cx| display_ranges(editor, cx)); + assert_eq!( + selections, + vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 10)] + ); +} + #[gpui::test] async fn test_next_prev_reference(cx: &mut TestAppContext) { const CYCLE_POSITIONS: &[&'static str] = &[ diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 9ce080c87bf82ec1098e2a4b1db6bc6a65d22828..2fdb2686ee00ea2fc27881b0c18a54fa85466d9a 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1289,7 +1289,9 @@ impl EditorElement { cx.notify(); } - if let Some((bounds, buffer_id, blame_entry)) = &position_map.inline_blame_bounds { + if text_hovered + && let Some((bounds, buffer_id, blame_entry)) = &position_map.inline_blame_bounds + { let mouse_over_inline_blame = bounds.contains(&event.position); let mouse_over_popover = editor .inline_blame_popover @@ -6732,7 +6734,13 @@ impl EditorElement { SelectionEffects::scroll(Autoscroll::top_relative(line_index)), window, cx, - |selections| selections.select_ranges([anchor..anchor]), + |selections| { + selections.clear_disjoint(); + selections.set_pending_anchor_range( + anchor..anchor, + crate::SelectMode::Character, + ); + }, ); cx.stop_propagation(); }); diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 9b127a8f1bc089d9cee28254c6b8ffc181677765..3bad6c97b6bcba4015331257a5b9a476dd0d3fd3 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -17,7 +17,7 @@ use gpui::{ use itertools::Itertools; use language::{DiagnosticEntry, Language, LanguageRegistry}; use lsp::DiagnosticSeverity; -use markdown::{Markdown, MarkdownElement, MarkdownStyle}; +use markdown::{CopyButtonVisibility, Markdown, MarkdownElement, MarkdownStyle}; use multi_buffer::{MultiBufferOffset, ToOffset, ToPoint}; use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart}; use settings::Settings; @@ -1040,8 +1040,7 @@ impl InfoPopover { .child( MarkdownElement::new(markdown, hover_markdown_style(window, cx)) .code_block_renderer(markdown::CodeBlockRenderer::Default { - copy_button: false, - copy_button_on_hover: false, + copy_button_visibility: CopyButtonVisibility::Hidden, border: false, }) .on_url_click(open_markdown_url) @@ -1155,8 +1154,7 @@ impl DiagnosticPopover { diagnostics_markdown_style(window, cx), ) .code_block_renderer(markdown::CodeBlockRenderer::Default { - copy_button: false, - copy_button_on_hover: false, + copy_button_visibility: CopyButtonVisibility::Hidden, border: false, }) .on_url_click( diff --git a/crates/editor/src/signature_help.rs b/crates/editor/src/signature_help.rs index 27c26d4691686c16bcbafbf74bba6b5f1156b835..6305fc73e44d745e943c1d4c8ec573e0cce7d9ed 100644 --- a/crates/editor/src/signature_help.rs +++ b/crates/editor/src/signature_help.rs @@ -7,7 +7,7 @@ use gpui::{ }; use language::BufferSnapshot; -use markdown::{Markdown, MarkdownElement}; +use markdown::{CopyButtonVisibility, Markdown, MarkdownElement}; use multi_buffer::{Anchor, MultiBufferOffset, ToOffset}; use settings::Settings; use std::ops::Range; @@ -408,9 +408,8 @@ impl SignatureHelpPopover { hover_markdown_style(window, cx), ) .code_block_renderer(markdown::CodeBlockRenderer::Default { - copy_button: false, + copy_button_visibility: CopyButtonVisibility::Hidden, border: false, - copy_button_on_hover: false, }) .on_url_click(open_markdown_url), ) @@ -421,9 +420,8 @@ impl SignatureHelpPopover { .child( MarkdownElement::new(description, hover_markdown_style(window, cx)) .code_block_renderer(markdown::CodeBlockRenderer::Default { - copy_button: false, + copy_button_visibility: CopyButtonVisibility::Hidden, border: false, - copy_button_on_hover: false, }) .on_url_click(open_markdown_url), ) diff --git a/crates/extension_host/src/wasm_host.rs b/crates/extension_host/src/wasm_host.rs index 286639cdd67d716b1137290baf269670ecddebe7..87a2032e831fc942f6848428a901a9fe3f613fc8 100644 --- a/crates/extension_host/src/wasm_host.rs +++ b/crates/extension_host/src/wasm_host.rs @@ -42,7 +42,7 @@ use wasmtime::{ CacheStore, Engine, Store, component::{Component, ResourceTable}, }; -use wasmtime_wasi::p2::{self as wasi, IoView as _}; +use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView}; use wit::Extension; pub struct WasmHost { @@ -93,7 +93,7 @@ impl extension::Extension for WasmExtension { ) -> Result { self.call(|extension, store| { async move { - let resource = store.data_mut().table().push(worktree)?; + let resource = store.data_mut().table.push(worktree)?; let command = extension .call_language_server_command( store, @@ -119,7 +119,7 @@ impl extension::Extension for WasmExtension { ) -> Result> { self.call(|extension, store| { async move { - let resource = store.data_mut().table().push(worktree)?; + let resource = store.data_mut().table.push(worktree)?; let options = extension .call_language_server_initialization_options( store, @@ -143,7 +143,7 @@ impl extension::Extension for WasmExtension { ) -> Result> { self.call(|extension, store| { async move { - let resource = store.data_mut().table().push(worktree)?; + let resource = store.data_mut().table.push(worktree)?; let options = extension .call_language_server_workspace_configuration( store, @@ -166,7 +166,7 @@ impl extension::Extension for WasmExtension { ) -> Result> { self.call(|extension, store| { async move { - let resource = store.data_mut().table().push(worktree)?; + let resource = store.data_mut().table.push(worktree)?; extension .call_language_server_initialization_options_schema( store, @@ -187,7 +187,7 @@ impl extension::Extension for WasmExtension { ) -> Result> { self.call(|extension, store| { async move { - let resource = store.data_mut().table().push(worktree)?; + let resource = store.data_mut().table.push(worktree)?; extension .call_language_server_workspace_configuration_schema( store, @@ -209,7 +209,7 @@ impl extension::Extension for WasmExtension { ) -> Result> { self.call(|extension, store| { async move { - let resource = store.data_mut().table().push(worktree)?; + let resource = store.data_mut().table.push(worktree)?; let options = extension .call_language_server_additional_initialization_options( store, @@ -234,7 +234,7 @@ impl extension::Extension for WasmExtension { ) -> Result> { self.call(|extension, store| { async move { - let resource = store.data_mut().table().push(worktree)?; + let resource = store.data_mut().table.push(worktree)?; let options = extension .call_language_server_additional_workspace_configuration( store, @@ -331,7 +331,7 @@ impl extension::Extension for WasmExtension { self.call(|extension, store| { async move { let resource = if let Some(delegate) = delegate { - Some(store.data_mut().table().push(delegate)?) + Some(store.data_mut().table.push(delegate)?) } else { None }; @@ -355,7 +355,7 @@ impl extension::Extension for WasmExtension { ) -> Result { self.call(|extension, store| { async move { - let project_resource = store.data_mut().table().push(project)?; + let project_resource = store.data_mut().table.push(project)?; let command = extension .call_context_server_command(store, context_server_id.clone(), project_resource) .await? @@ -374,7 +374,7 @@ impl extension::Extension for WasmExtension { ) -> Result> { self.call(|extension, store| { async move { - let project_resource = store.data_mut().table().push(project)?; + let project_resource = store.data_mut().table.push(project)?; let Some(configuration) = extension .call_context_server_configuration( store, @@ -417,7 +417,7 @@ impl extension::Extension for WasmExtension { ) -> Result<()> { self.call(|extension, store| { async move { - let kv_store_resource = store.data_mut().table().push(kv_store)?; + let kv_store_resource = store.data_mut().table.push(kv_store)?; extension .call_index_docs( store, @@ -444,7 +444,7 @@ impl extension::Extension for WasmExtension { ) -> Result { self.call(|extension, store| { async move { - let resource = store.data_mut().table().push(worktree)?; + let resource = store.data_mut().table.push(worktree)?; let dap_binary = extension .call_get_dap_binary(store, dap_name, config, user_installed_path, resource) .await? @@ -532,7 +532,7 @@ impl extension::Extension for WasmExtension { pub struct WasmState { manifest: Arc, pub table: ResourceTable, - ctx: wasi::WasiCtx, + ctx: WasiCtx, pub host: Arc, pub(crate) capability_granter: CapabilityGranter, } @@ -726,7 +726,7 @@ impl WasmHost { }) } - async fn build_wasi_ctx(&self, manifest: &Arc) -> Result { + async fn build_wasi_ctx(&self, manifest: &Arc) -> Result { let extension_work_dir = self.work_dir.join(manifest.id.as_ref()); self.fs .create_dir(&extension_work_dir) @@ -739,7 +739,7 @@ impl WasmHost { #[cfg(target_os = "windows")] let path = path.replace('\\', "/"); - let mut ctx = wasi::WasiCtxBuilder::new(); + let mut ctx = WasiCtxBuilder::new(); ctx.inherit_stdio() .env("PWD", &path) .env("RUST_BACKTRACE", "full"); @@ -947,15 +947,16 @@ impl WasmState { } } -impl wasi::IoView for WasmState { - fn table(&mut self) -> &mut ResourceTable { - &mut self.table - } +impl wasmtime::component::HasData for WasmState { + type Data<'a> = &'a mut WasmState; } -impl wasi::WasiView for WasmState { - fn ctx(&mut self) -> &mut wasi::WasiCtx { - &mut self.ctx +impl WasiView for WasmState { + fn ctx(&mut self) -> WasiCtxView<'_> { + WasiCtxView { + ctx: &mut self.ctx, + table: &mut self.table, + } } } diff --git a/crates/extension_host/src/wasm_host/wit.rs b/crates/extension_host/src/wasm_host/wit.rs index 9c4d3aa298c366ae91d0f8195ed090d74099c6d0..27847422f01680240119877e0864491dd7660d68 100644 --- a/crates/extension_host/src/wasm_host/wit.rs +++ b/crates/extension_host/src/wasm_host/wit.rs @@ -42,18 +42,14 @@ pub use since_v0_0_4::LanguageServerConfig; pub fn new_linker( executor: &BackgroundExecutor, - f: impl Fn(&mut Linker, fn(&mut WasmState) -> &mut WasmState) -> Result<()>, + f: impl FnOnce(&mut Linker) -> Result<()>, ) -> Linker { let mut linker = Linker::new(&wasm_engine(executor)); wasmtime_wasi::p2::add_to_linker_async(&mut linker).unwrap(); - f(&mut linker, wasi_view).unwrap(); + f(&mut linker).unwrap(); linker } -fn wasi_view(state: &mut WasmState) -> &mut WasmState { - state -} - /// Returns whether the given Wasm API version is supported by the Wasm host. pub fn is_supported_wasm_api_version(release_channel: ReleaseChannel, version: Version) -> bool { wasm_api_version_range(release_channel).contains(&version) diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs b/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs index fa7539eec9f454c95782cd0249664693074abfba..c231b7e5d69157d523973455b2437a576392a00d 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs @@ -12,8 +12,12 @@ use wasmtime::component::{Linker, Resource}; pub const MIN_VERSION: Version = Version::new(0, 0, 1); wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, + imports: { + default: async | trappable, + }, + exports: { + default: async, + }, path: "../extension_api/wit/since_v0.0.1", with: { "worktree": ExtensionWorktree, @@ -26,7 +30,11 @@ pub type ExtensionWorktree = Arc; pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) + LINKER.get_or_init(|| { + super::new_linker(executor, |linker| { + Extension::add_to_linker::<_, WasmState>(linker, |s| s) + }) + }) } impl From for latest::DownloadedFileType { diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_0_4.rs b/crates/extension_host/src/wasm_host/wit/since_v0_0_4.rs index 6d7db749f0cd021bfb084eba1bc20ce72780f3d8..41d652cec3087e8e5458a048689be4494de63356 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_0_4.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_0_4.rs @@ -10,8 +10,12 @@ use wasmtime::component::{Linker, Resource}; pub const MIN_VERSION: Version = Version::new(0, 0, 4); wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, + imports: { + default: async | trappable, + }, + exports: { + default: async, + }, path: "../extension_api/wit/since_v0.0.4", with: { "worktree": ExtensionWorktree, @@ -24,7 +28,11 @@ pub type ExtensionWorktree = Arc; pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) + LINKER.get_or_init(|| { + super::new_linker(executor, |linker| { + Extension::add_to_linker::<_, WasmState>(linker, |s| s) + }) + }) } impl From for latest::DownloadedFileType { diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_0_6.rs b/crates/extension_host/src/wasm_host/wit/since_v0_0_6.rs index e5ff0322088470d47e903c4a83794b654bbba531..e1dfdf8248b41de2de5e9faff3d212d06f1349c4 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_0_6.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_0_6.rs @@ -10,8 +10,12 @@ use wasmtime::component::{Linker, Resource}; pub const MIN_VERSION: Version = Version::new(0, 0, 6); wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, + imports: { + default: async | trappable, + }, + exports: { + default: async, + }, path: "../extension_api/wit/since_v0.0.6", with: { "worktree": ExtensionWorktree, @@ -31,7 +35,11 @@ pub type ExtensionWorktree = Arc; pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) + LINKER.get_or_init(|| { + super::new_linker(executor, |linker| { + Extension::add_to_linker::<_, WasmState>(linker, |s| s) + }) + }) } impl From for latest::Command { diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs index 0caaa86c2413f1b279319eeea4d8577d1ed4b5a5..4cd034d4d6af02971468ba8e57e1eebf9078353f 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs @@ -26,8 +26,12 @@ use super::{latest, since_v0_6_0}; pub const MIN_VERSION: Version = Version::new(0, 1, 0); wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, + imports: { + default: async | trappable, + }, + exports: { + default: async, + }, path: "../extension_api/wit/since_v0.1.0", with: { "worktree": ExtensionWorktree, @@ -52,7 +56,11 @@ pub type ExtensionHttpResponseStream = Arc &'static Linker { static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) + LINKER.get_or_init(|| { + super::new_linker(executor, |linker| { + Extension::add_to_linker::<_, WasmState>(linker, |s| s) + }) + }) } impl From for latest::Command { diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs index 074cce73c22d547cd3198a672e6f8cdc5f750d49..691e6d2dd549b64c3783406af210b6b48f4a1dbc 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs @@ -11,8 +11,12 @@ use super::{latest, since_v0_6_0}; pub const MIN_VERSION: Version = Version::new(0, 2, 0); wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, + imports: { + default: async | trappable, + }, + exports: { + default: async, + }, path: "../extension_api/wit/since_v0.2.0", with: { "worktree": ExtensionWorktree, @@ -40,7 +44,11 @@ pub type ExtensionKeyValueStore = Arc; pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) + LINKER.get_or_init(|| { + super::new_linker(executor, |linker| { + Extension::add_to_linker::<_, WasmState>(linker, |s| s) + }) + }) } impl From for latest::Command { diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_3_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_3_0.rs index 072ad42f2b9c2f5b3a8556b237f3907052665370..53aa65d5187663ea86fa465af76cf3aebc7844e4 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_3_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_3_0.rs @@ -11,8 +11,12 @@ use super::{latest, since_v0_6_0}; pub const MIN_VERSION: Version = Version::new(0, 3, 0); wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, + imports: { + default: async | trappable, + }, + exports: { + default: async, + }, path: "../extension_api/wit/since_v0.3.0", with: { "worktree": ExtensionWorktree, @@ -40,7 +44,11 @@ pub type ExtensionKeyValueStore = Arc; pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) + LINKER.get_or_init(|| { + super::new_linker(executor, |linker| { + Extension::add_to_linker::<_, WasmState>(linker, |s| s) + }) + }) } impl From for latest::CodeLabel { diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_4_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_4_0.rs index 4f1d5c6a48c13ff09a5c81e2b43683fa50a7ccec..44b7d7ba1ad4e3235e8772a051bb906f87c64325 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_4_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_4_0.rs @@ -11,8 +11,12 @@ use super::{latest, since_v0_6_0}; pub const MIN_VERSION: Version = Version::new(0, 4, 0); wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, + imports: { + default: async | trappable, + }, + exports: { + default: async, + }, path: "../extension_api/wit/since_v0.4.0", with: { "worktree": ExtensionWorktree, @@ -40,7 +44,11 @@ pub type ExtensionKeyValueStore = Arc; pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) + LINKER.get_or_init(|| { + super::new_linker(executor, |linker| { + Extension::add_to_linker::<_, WasmState>(linker, |s| s) + }) + }) } impl From for latest::CodeLabel { diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_5_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_5_0.rs index 84f73f567750081d406b20025f0b4598cfd0f9af..4dff0d90a94fe1128c6182592093b38cf43fe573 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_5_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_5_0.rs @@ -11,8 +11,12 @@ use super::{latest, since_v0_6_0}; pub const MIN_VERSION: Version = Version::new(0, 5, 0); wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, + imports: { + default: async | trappable, + }, + exports: { + default: async, + }, path: "../extension_api/wit/since_v0.5.0", with: { "worktree": ExtensionWorktree, @@ -41,7 +45,11 @@ pub type ExtensionKeyValueStore = Arc; pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) + LINKER.get_or_init(|| { + super::new_linker(executor, |linker| { + Extension::add_to_linker::<_, WasmState>(linker, |s| s) + }) + }) } impl From for latest::CodeLabel { diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs index 202bcd6ce959b27b3b7ecf8e15830cb1955ec104..bc5674b051772e464c0cbdb74e75f935959e05d8 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs @@ -12,8 +12,12 @@ pub const MIN_VERSION: Version = Version::new(0, 6, 0); pub const MAX_VERSION: Version = Version::new(0, 7, 0); wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, + imports: { + default: async | trappable, + }, + exports: { + default: async, + }, path: "../extension_api/wit/since_v0.6.0", with: { "worktree": ExtensionWorktree, @@ -43,7 +47,11 @@ pub type ExtensionKeyValueStore = Arc; pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) + LINKER.get_or_init(|| { + super::new_linker(executor, |linker| { + Extension::add_to_linker::<_, WasmState>(linker, |s| s) + }) + }) } impl From for latest::CodeLabel { diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs index 324a572f40c98037816870c99151a4789793da1b..660ddd9688f7dc69f3ec3c52452122fd807257ad 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs @@ -40,8 +40,12 @@ pub const MIN_VERSION: Version = Version::new(0, 8, 0); pub const MAX_VERSION: Version = Version::new(0, 8, 0); wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, + imports: { + default: async | trappable, + }, + exports: { + default: async, + }, path: "../extension_api/wit/since_v0.8.0", with: { "worktree": ExtensionWorktree, @@ -65,7 +69,11 @@ pub type ExtensionHttpResponseStream = Arc &'static Linker { static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) + LINKER.get_or_init(|| { + super::new_linker(executor, |linker| { + Extension::add_to_linker::<_, WasmState>(linker, |s| s) + }) + }) } impl From for std::ops::Range { diff --git a/crates/git_graph/src/git_graph.rs b/crates/git_graph/src/git_graph.rs index b971566075181350453b28bf9909371e51436021..a66e840b2f41405b5c76f3999ea14414daa19d39 100644 --- a/crates/git_graph/src/git_graph.rs +++ b/crates/git_graph/src/git_graph.rs @@ -41,9 +41,9 @@ use theme::AccentColors; use theme_settings::ThemeSettings; use time::{OffsetDateTime, UtcOffset, format_description::BorrowedFormatItem}; use ui::{ - ButtonLike, Chip, CommonAnimationExt as _, ContextMenu, DiffStat, Divider, HighlightedLabel, - ScrollableHandle, Table, TableColumnWidths, TableInteractionState, TableResizeBehavior, - Tooltip, WithScrollbar, prelude::*, + ButtonLike, Chip, ColumnWidthConfig, CommonAnimationExt as _, ContextMenu, DiffStat, Divider, + HighlightedLabel, RedistributableColumnsState, ScrollableHandle, Table, TableInteractionState, + TableResizeBehavior, Tooltip, WithScrollbar, prelude::*, }; use workspace::{ Workspace, @@ -901,7 +901,7 @@ pub struct GitGraph { context_menu: Option<(Entity, Point, Subscription)>, row_height: Pixels, table_interaction_state: Entity, - table_column_widths: Entity, + table_column_widths: Entity, horizontal_scroll_offset: Pixels, graph_viewport_width: Pixels, selected_entry_idx: Option, @@ -972,7 +972,23 @@ impl GitGraph { }); let table_interaction_state = cx.new(|cx| TableInteractionState::new(cx)); - let table_column_widths = cx.new(|cx| TableColumnWidths::new(4, cx)); + let table_column_widths = cx.new(|_cx| { + RedistributableColumnsState::new( + 4, + vec![ + DefiniteLength::Fraction(0.72), + DefiniteLength::Fraction(0.12), + DefiniteLength::Fraction(0.10), + DefiniteLength::Fraction(0.06), + ], + vec![ + TableResizeBehavior::Resizable, + TableResizeBehavior::Resizable, + TableResizeBehavior::Resizable, + TableResizeBehavior::Resizable, + ], + ) + }); let mut row_height = Self::row_height(cx); cx.observe_global_in::(window, move |this, _window, cx| { @@ -1324,6 +1340,12 @@ impl GitGraph { editor.set_text_style_refinement(Default::default()); }); + if query.as_str().is_empty() { + self.search_state.state = QueryState::Empty; + cx.notify(); + return; + } + let (request_tx, request_rx) = smol::channel::unbounded::(); repo.update(cx, |repo, cx| { @@ -2453,11 +2475,6 @@ impl Render for GitGraph { self.search_state.state = QueryState::Empty; self.search(query, cx); } - let description_width_fraction = 0.72; - let date_width_fraction = 0.12; - let author_width_fraction = 0.10; - let commit_width_fraction = 0.06; - let (commit_count, is_loading) = match self.graph_data.max_commit_count { AllCommitCount::Loaded(count) => (count, true), AllCommitCount::NotLoaded => { @@ -2517,7 +2534,10 @@ impl Render for GitGraph { .flex_col() .child( div() - .p_2() + .flex() + .items_center() + .px_1() + .py_0p5() .border_b_1() .whitespace_nowrap() .border_color(cx.theme().colors().border) @@ -2559,25 +2579,9 @@ impl Render for GitGraph { Label::new("Author").color(Color::Muted).into_any_element(), Label::new("Commit").color(Color::Muted).into_any_element(), ]) - .column_widths( - [ - DefiniteLength::Fraction(description_width_fraction), - DefiniteLength::Fraction(date_width_fraction), - DefiniteLength::Fraction(author_width_fraction), - DefiniteLength::Fraction(commit_width_fraction), - ] - .to_vec(), - ) - .resizable_columns( - vec![ - TableResizeBehavior::Resizable, - TableResizeBehavior::Resizable, - TableResizeBehavior::Resizable, - TableResizeBehavior::Resizable, - ], - &self.table_column_widths, - cx, - ) + .width_config(ColumnWidthConfig::redistributable( + self.table_column_widths.clone(), + )) .map_row(move |(index, row), window, cx| { let is_selected = selected_entry_idx == Some(index); let is_hovered = hovered_entry_idx == Some(index); diff --git a/crates/git_ui/src/branch_picker.rs b/crates/git_ui/src/branch_picker.rs index 438df6839949d46d3ba8e0509995beb1300b7c80..83c8119a077ac1c024dbb3b3df948f762b072ec1 100644 --- a/crates/git_ui/src/branch_picker.rs +++ b/crates/git_ui/src/branch_picker.rs @@ -1087,13 +1087,8 @@ impl PickerDelegate for BranchListDelegate { ), ) .when(!is_new_items && !is_head_branch, |this| { - this.map(|this| { - if self.selected_index() == ix { - this.end_slot(deleted_branch_icon(ix)) - } else { - this.end_hover_slot(deleted_branch_icon(ix)) - } - }) + this.end_slot(deleted_branch_icon(ix)) + .show_end_slot_on_hover() }) .when_some( if is_new_items { @@ -1102,13 +1097,8 @@ impl PickerDelegate for BranchListDelegate { None }, |this, create_from_default_button| { - this.map(|this| { - if self.selected_index() == ix { - this.end_slot(create_from_default_button) - } else { - this.end_hover_slot(create_from_default_button) - } - }) + this.end_slot(create_from_default_button) + .show_end_slot_on_hover() }, ), ) diff --git a/crates/git_ui/src/stash_picker.rs b/crates/git_ui/src/stash_picker.rs index 9987190f45b73f3f1132ce1295de6f412022abe2..2d3515e833e4d353c323f533f1f0f39bb1d76561 100644 --- a/crates/git_ui/src/stash_picker.rs +++ b/crates/git_ui/src/stash_picker.rs @@ -501,16 +501,39 @@ impl PickerDelegate for StashListDelegate { .size(LabelSize::Small), ); - let focus_handle = self.focus_handle.clone(); + let view_button = { + let focus_handle = self.focus_handle.clone(); + IconButton::new(("view-stash", ix), IconName::Eye) + .icon_size(IconSize::Small) + .tooltip(move |_, cx| { + Tooltip::for_action_in("View Stash", &ShowStashItem, &focus_handle, cx) + }) + .on_click(cx.listener(move |this, _, window, cx| { + this.delegate.show_stash_at(ix, window, cx); + })) + }; + + let pop_button = { + let focus_handle = self.focus_handle.clone(); + IconButton::new(("pop-stash", ix), IconName::MaximizeAlt) + .icon_size(IconSize::Small) + .tooltip(move |_, cx| { + Tooltip::for_action_in("Pop Stash", &menu::SecondaryConfirm, &focus_handle, cx) + }) + .on_click(|_, window, cx| { + window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx); + }) + }; - let drop_button = |entry_ix: usize| { - IconButton::new(("drop-stash", entry_ix), IconName::Trash) + let drop_button = { + let focus_handle = self.focus_handle.clone(); + IconButton::new(("drop-stash", ix), IconName::Trash) .icon_size(IconSize::Small) .tooltip(move |_, cx| { Tooltip::for_action_in("Drop Stash", &DropStashItem, &focus_handle, cx) }) .on_click(cx.listener(move |this, _, window, cx| { - this.delegate.drop_stash_at(entry_ix, window, cx); + this.delegate.drop_stash_at(ix, window, cx); })) }; @@ -530,17 +553,14 @@ impl PickerDelegate for StashListDelegate { ) .child(div().w_full().child(stash_label).child(branch_info)), ) - .tooltip(Tooltip::text(format!( - "stash@{{{}}}", - entry_match.entry.index - ))) - .map(|this| { - if selected { - this.end_slot(drop_button(ix)) - } else { - this.end_hover_slot(drop_button(ix)) - } - }), + .end_slot( + h_flex() + .gap_0p5() + .child(view_button) + .child(pop_button) + .child(drop_button), + ) + .show_end_slot_on_hover(), ) } @@ -549,6 +569,10 @@ impl PickerDelegate for StashListDelegate { } fn render_footer(&self, _: &mut Window, cx: &mut Context>) -> Option { + if self.matches.is_empty() { + return None; + } + let focus_handle = self.focus_handle.clone(); Some( diff --git a/crates/git_ui/src/worktree_picker.rs b/crates/git_ui/src/worktree_picker.rs index 3e14b56f9bf4a95452855bc6cbef6f764e2b3530..c3e2259e411c7a3a56a36b92735f8d5e014e53d7 100644 --- a/crates/git_ui/src/worktree_picker.rs +++ b/crates/git_ui/src/worktree_picker.rs @@ -884,12 +884,30 @@ impl PickerDelegate for WorktreeListDelegate { } })), ) - .when(can_delete, |this| { - if selected { - this.end_slot(delete_button(ix)) - } else { - this.end_hover_slot(delete_button(ix)) - } + .when(!entry.is_new, |this| { + let focus_handle = self.focus_handle.clone(); + let open_in_new_window_button = + IconButton::new(("open-new-window", ix), IconName::ArrowUpRight) + .icon_size(IconSize::Small) + .tooltip(move |_, cx| { + Tooltip::for_action_in( + "Open in New Window", + &menu::SecondaryConfirm, + &focus_handle, + cx, + ) + }) + .on_click(|_, window, cx| { + window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx); + }); + + this.end_slot( + h_flex() + .gap_0p5() + .child(open_in_new_window_button) + .when(can_delete, |this| this.child(delete_button(ix))), + ) + .show_end_slot_on_hover() }), ) } diff --git a/crates/gpui/examples/input.rs b/crates/gpui/examples/input.rs index d15d791cd008883506389cc7bb16dbad765969c0..370e27de7d54c317af6683c240f343e750c68698 100644 --- a/crates/gpui/examples/input.rs +++ b/crates/gpui/examples/input.rs @@ -85,14 +85,24 @@ impl TextInput { fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context) { if self.selected_range.is_empty() { - self.select_to(self.previous_boundary(self.cursor_offset()), cx) + let prev = self.previous_boundary(self.cursor_offset()); + if self.cursor_offset() == prev { + window.play_system_bell(); + return; + } + self.select_to(prev, cx) } self.replace_text_in_range(None, "", window, cx) } fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context) { if self.selected_range.is_empty() { - self.select_to(self.next_boundary(self.cursor_offset()), cx) + let next = self.next_boundary(self.cursor_offset()); + if self.cursor_offset() == next { + window.play_system_bell(); + return; + } + self.select_to(next, cx) } self.replace_text_in_range(None, "", window, cx) } diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 806a34040a4ec685c3d5c6ec01f47b5026e349a6..efca26a6b4802037a96490bf81f7d1c5c1d8b298 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -689,6 +689,8 @@ pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle { fn update_ime_position(&self, _bounds: Bounds); + fn play_system_bell(&self) {} + #[cfg(any(test, feature = "test-support"))] fn as_test(&mut self) -> Option<&mut TestWindow> { None diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 48c381e5275e950bd6754541fedbab03ae3d64c2..7790480e32149fa33dfd082df7a8cdbb09568134 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -5024,6 +5024,12 @@ impl Window { .set_tabbing_identifier(tabbing_identifier) } + /// Request the OS to play an alert sound. On some platforms this is associated + /// with the window, for others it's just a simple global function call. + pub fn play_system_bell(&self) { + self.platform_window.play_system_bell() + } + /// Toggles the inspector mode on this window. #[cfg(any(feature = "inspector", debug_assertions))] pub fn toggle_inspector(&mut self, cx: &mut App) { diff --git a/crates/gpui_linux/src/linux/wayland/client.rs b/crates/gpui_linux/src/linux/wayland/client.rs index b65a203dd3448ba191b7e2f5ae0f5b6c396545a8..10f4aab0db19978302143519dd6e2a7e4d25ec4d 100644 --- a/crates/gpui_linux/src/linux/wayland/client.rs +++ b/crates/gpui_linux/src/linux/wayland/client.rs @@ -58,6 +58,7 @@ use wayland_protocols::xdg::decoration::zv1::client::{ zxdg_decoration_manager_v1, zxdg_toplevel_decoration_v1, }; use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base}; +use wayland_protocols::xdg::system_bell::v1::client::xdg_system_bell_v1; use wayland_protocols::{ wp::cursor_shape::v1::client::{wp_cursor_shape_device_v1, wp_cursor_shape_manager_v1}, xdg::dialog::v1::client::xdg_wm_dialog_v1::{self, XdgWmDialogV1}, @@ -129,6 +130,7 @@ pub struct Globals { pub text_input_manager: Option, pub gesture_manager: Option, pub dialog: Option, + pub system_bell: Option, pub executor: ForegroundExecutor, } @@ -170,6 +172,7 @@ impl Globals { text_input_manager: globals.bind(&qh, 1..=1, ()).ok(), gesture_manager: globals.bind(&qh, 1..=3, ()).ok(), dialog: globals.bind(&qh, dialog_v..=dialog_v, ()).ok(), + system_bell: globals.bind(&qh, 1..=1, ()).ok(), executor, qh, } @@ -1069,6 +1072,7 @@ impl Dispatch for WaylandClientStat } delegate_noop!(WaylandClientStatePtr: ignore xdg_activation_v1::XdgActivationV1); +delegate_noop!(WaylandClientStatePtr: ignore xdg_system_bell_v1::XdgSystemBellV1); delegate_noop!(WaylandClientStatePtr: ignore wl_compositor::WlCompositor); delegate_noop!(WaylandClientStatePtr: ignore wp_cursor_shape_device_v1::WpCursorShapeDeviceV1); delegate_noop!(WaylandClientStatePtr: ignore wp_cursor_shape_manager_v1::WpCursorShapeManagerV1); diff --git a/crates/gpui_linux/src/linux/wayland/window.rs b/crates/gpui_linux/src/linux/wayland/window.rs index c4ff55fc80cc4d14069dd510b8e6855c17096773..1e3af66c59858c435ca3da093a1c48056b77667e 100644 --- a/crates/gpui_linux/src/linux/wayland/window.rs +++ b/crates/gpui_linux/src/linux/wayland/window.rs @@ -1479,6 +1479,18 @@ impl PlatformWindow for WaylandWindow { fn gpu_specs(&self) -> Option { self.borrow().renderer.gpu_specs().into() } + + fn play_system_bell(&self) { + let state = self.borrow(); + let surface = if state.surface_state.toplevel().is_some() { + Some(&state.surface) + } else { + None + }; + if let Some(bell) = state.globals.system_bell.as_ref() { + bell.ring(surface); + } + } } fn update_window(mut state: RefMut) { diff --git a/crates/gpui_linux/src/linux/x11/window.rs b/crates/gpui_linux/src/linux/x11/window.rs index 5e1287976cbb3ba9bc2c1571fa9e215f47fdd615..1974cc0bb28f62da4d7dcb3e9fca92b6324470bb 100644 --- a/crates/gpui_linux/src/linux/x11/window.rs +++ b/crates/gpui_linux/src/linux/x11/window.rs @@ -1846,4 +1846,9 @@ impl PlatformWindow for X11Window { fn gpu_specs(&self) -> Option { self.0.state.borrow().renderer.gpu_specs().into() } + + fn play_system_bell(&self) { + // Volume 0% means don't increase or decrease from system volume + let _ = self.0.xcb.bell(0); + } } diff --git a/crates/gpui_macos/Cargo.toml b/crates/gpui_macos/Cargo.toml index 06e5d0e7321af523a249f19ec0d5ac50e2da5d3f..3626bbd05e8a7c7fa2ae577f11e5277da995d2f7 100644 --- a/crates/gpui_macos/Cargo.toml +++ b/crates/gpui_macos/Cargo.toml @@ -48,6 +48,7 @@ mach2.workspace = true media.workspace = true metal.workspace = true objc.workspace = true +objc2-app-kit.workspace = true parking_lot.workspace = true pathfinder_geometry = "0.5" raw-window-handle = "0.6" diff --git a/crates/gpui_macos/src/window.rs b/crates/gpui_macos/src/window.rs index 398cf46eab09dc8412ffdda8eb550b8ad4e09b40..ace36d695401ce76949129197dcd05135508f7d3 100644 --- a/crates/gpui_macos/src/window.rs +++ b/crates/gpui_macos/src/window.rs @@ -49,6 +49,7 @@ use objc::{ runtime::{BOOL, Class, NO, Object, Protocol, Sel, YES}, sel, sel_impl, }; +use objc2_app_kit::NSBeep; use parking_lot::Mutex; use raw_window_handle as rwh; use smallvec::SmallVec; @@ -1676,6 +1677,10 @@ impl PlatformWindow for MacWindow { } } + fn play_system_bell(&self) { + unsafe { NSBeep() } + } + #[cfg(any(test, feature = "test-support"))] fn render_to_image(&self, scene: &gpui::Scene) -> Result { let mut this = self.0.lock(); diff --git a/crates/gpui_windows/src/window.rs b/crates/gpui_windows/src/window.rs index 3a55100dfb75e961f57b977297bfcd2dc2ae2701..92255f93fd95969931c6b1ae8cb465ff628f82cb 100644 --- a/crates/gpui_windows/src/window.rs +++ b/crates/gpui_windows/src/window.rs @@ -20,7 +20,9 @@ use windows::{ Foundation::*, Graphics::Dwm::*, Graphics::Gdi::*, - System::{Com::*, LibraryLoader::*, Ole::*, SystemServices::*}, + System::{ + Com::*, Diagnostics::Debug::MessageBeep, LibraryLoader::*, Ole::*, SystemServices::*, + }, UI::{Controls::*, HiDpi::*, Input::KeyboardAndMouse::*, Shell::*, WindowsAndMessaging::*}, }, core::*, @@ -950,6 +952,11 @@ impl PlatformWindow for WindowsWindow { self.0.update_ime_position(self.0.hwnd, caret_position); } + + fn play_system_bell(&self) { + // MB_OK: The sound specified as the Windows Default Beep sound. + let _ = unsafe { MessageBeep(MB_OK) }; + } } #[implement(IDropTarget)] diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index 400b2a22bc6071b62c6ce22a2b1bf1053c8cf871..6929ae4e4ca8ca0ee00c9793c948892043dd6dd6 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -174,6 +174,7 @@ pub enum IconName { LockOutlined, MagnifyingGlass, Maximize, + MaximizeAlt, Menu, MenuAltTemp, Mic, @@ -240,6 +241,7 @@ pub enum IconName { ThinkingModeOff, Thread, ThreadFromSummary, + ThreadImport, ThreadsSidebarLeftClosed, ThreadsSidebarLeftOpen, ThreadsSidebarRightClosed, diff --git a/crates/keymap_editor/src/keymap_editor.rs b/crates/keymap_editor/src/keymap_editor.rs index 6a02289353f7fc0df8fd2b3fd99313d2ce650951..2e3172dac95fe91ed5b2a5a187ca57bbd9154fae 100644 --- a/crates/keymap_editor/src/keymap_editor.rs +++ b/crates/keymap_editor/src/keymap_editor.rs @@ -31,10 +31,10 @@ use settings::{ BaseKeymap, KeybindSource, KeymapFile, Settings as _, SettingsAssets, infer_json_indent_size, }; use ui::{ - ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, IconButtonShape, IconPosition, - Indicator, Modal, ModalFooter, ModalHeader, ParentElement as _, PopoverMenu, Render, Section, - SharedString, Styled as _, Table, TableColumnWidths, TableInteractionState, - TableResizeBehavior, Tooltip, Window, prelude::*, + ActiveTheme as _, App, Banner, BorrowAppContext, ColumnWidthConfig, ContextMenu, + IconButtonShape, IconPosition, Indicator, Modal, ModalFooter, ModalHeader, ParentElement as _, + PopoverMenu, RedistributableColumnsState, Render, Section, SharedString, Styled as _, Table, + TableInteractionState, TableResizeBehavior, Tooltip, Window, prelude::*, }; use ui_input::InputField; use util::ResultExt; @@ -450,7 +450,7 @@ struct KeymapEditor { context_menu: Option<(Entity, Point, Subscription)>, previous_edit: Option, humanized_action_names: HumanizedActionNameCache, - current_widths: Entity, + current_widths: Entity, show_hover_menus: bool, actions_with_schemas: HashSet<&'static str>, /// In order for the JSON LSP to run in the actions arguments editor, we @@ -623,7 +623,27 @@ impl KeymapEditor { actions_with_schemas: HashSet::default(), action_args_temp_dir: None, action_args_temp_dir_worktree: None, - current_widths: cx.new(|cx| TableColumnWidths::new(COLS, cx)), + current_widths: cx.new(|_cx| { + RedistributableColumnsState::new( + COLS, + vec![ + DefiniteLength::Absolute(AbsoluteLength::Pixels(px(36.))), + DefiniteLength::Fraction(0.25), + DefiniteLength::Fraction(0.20), + DefiniteLength::Fraction(0.14), + DefiniteLength::Fraction(0.45), + DefiniteLength::Fraction(0.08), + ], + vec![ + TableResizeBehavior::None, + TableResizeBehavior::Resizable, + TableResizeBehavior::Resizable, + TableResizeBehavior::Resizable, + TableResizeBehavior::Resizable, + TableResizeBehavior::Resizable, + ], + ) + }), }; this.on_keymap_changed(window, cx); @@ -2095,26 +2115,9 @@ impl Render for KeymapEditor { let this = cx.entity(); move |window, cx| this.read(cx).render_no_matches_hint(window, cx) }) - .column_widths(vec![ - DefiniteLength::Absolute(AbsoluteLength::Pixels(px(36.))), - DefiniteLength::Fraction(0.25), - DefiniteLength::Fraction(0.20), - DefiniteLength::Fraction(0.14), - DefiniteLength::Fraction(0.45), - DefiniteLength::Fraction(0.08), - ]) - .resizable_columns( - vec![ - TableResizeBehavior::None, - TableResizeBehavior::Resizable, - TableResizeBehavior::Resizable, - TableResizeBehavior::Resizable, - TableResizeBehavior::Resizable, - TableResizeBehavior::Resizable, // this column doesn't matter - ], - &self.current_widths, - cx, - ) + .width_config(ColumnWidthConfig::redistributable( + self.current_widths.clone(), + )) .header(vec!["", "Action", "Arguments", "Keystrokes", "Context", "Source"]) .uniform_list( "keymap-editor-table", diff --git a/crates/languages/src/eslint.rs b/crates/languages/src/eslint.rs index 943034652de852b2c39b4887218c3c8e28f329e1..bf51636f60bb4e0eec6eebcd3efaab2996352c18 100644 --- a/crates/languages/src/eslint.rs +++ b/crates/languages/src/eslint.rs @@ -148,6 +148,7 @@ impl LspInstaller for EsLintLspAdapter { ) -> Option { let server_path = Self::build_destination_path(&container_dir).join(EsLintLspAdapter::SERVER_PATH); + fs::metadata(&server_path).await.ok()?; Some(LanguageServerBinary { path: self.node.binary_path().await.ok()?, env: None, diff --git a/crates/markdown/src/html/html_rendering.rs b/crates/markdown/src/html/html_rendering.rs index 56ab2db26b682e197c194157a87e646d9e55019d..103e2a6accb7dce9bc429419aafd27cbdf5080ce 100644 --- a/crates/markdown/src/html/html_rendering.rs +++ b/crates/markdown/src/html/html_rendering.rs @@ -497,7 +497,10 @@ mod tests { use gpui::{TestAppContext, size}; use ui::prelude::*; - use crate::{CodeBlockRenderer, Markdown, MarkdownElement, MarkdownOptions, MarkdownStyle}; + use crate::{ + CodeBlockRenderer, CopyButtonVisibility, Markdown, MarkdownElement, MarkdownOptions, + MarkdownStyle, + }; fn ensure_theme_initialized(cx: &mut TestAppContext) { cx.update(|cx| { @@ -530,8 +533,7 @@ mod tests { |_window, _cx| { MarkdownElement::new(markdown, MarkdownStyle::default()).code_block_renderer( CodeBlockRenderer::Default { - copy_button: false, - copy_button_on_hover: false, + copy_button_visibility: CopyButtonVisibility::Hidden, border: false, }, ) @@ -591,8 +593,7 @@ mod tests { |_window, _cx| { MarkdownElement::new(markdown, MarkdownStyle::default()).code_block_renderer( CodeBlockRenderer::Default { - copy_button: false, - copy_button_on_hover: false, + copy_button_visibility: CopyButtonVisibility::Hidden, border: false, }, ) diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index 7b95688df54610f92b6960d9afc3037bf484b8ed..c31ca79e7581926e7696fa596aaccc9371512841 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -270,10 +270,16 @@ pub struct MarkdownOptions { pub render_mermaid_diagrams: bool, } +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum CopyButtonVisibility { + Hidden, + AlwaysVisible, + VisibleOnHover, +} + pub enum CodeBlockRenderer { Default { - copy_button: bool, - copy_button_on_hover: bool, + copy_button_visibility: CopyButtonVisibility, border: bool, }, Custom { @@ -826,8 +832,7 @@ impl MarkdownElement { markdown, style, code_block_renderer: CodeBlockRenderer::Default { - copy_button: true, - copy_button_on_hover: false, + copy_button_visibility: CopyButtonVisibility::VisibleOnHover, border: false, }, on_url_click: None, @@ -1609,23 +1614,18 @@ impl Element for MarkdownElement { builder.table.start(alignments.clone()); let column_count = alignments.len(); - builder.push_div( - div().flex().flex_col().items_start(), - range, - markdown_end, - ); builder.push_div( div() .id(("table", range.start)) - .min_w_0() .grid() .grid_cols(column_count as u16) .when(self.style.table_columns_min_size, |this| { this.grid_cols_min_content(column_count as u16) }) .when(!self.style.table_columns_min_size, |this| { - this.grid_cols_max_content(column_count as u16) + this.grid_cols(column_count as u16) }) + .w_full() .mb_2() .border(px(1.5)) .border_color(cx.theme().colors().border) @@ -1691,38 +1691,10 @@ impl Element for MarkdownElement { builder.pop_text_style(); if let CodeBlockRenderer::Default { - copy_button: true, .. - } = &self.code_block_renderer - { - builder.modify_current_div(|el| { - let content_range = parser::extract_code_block_content_range( - &parsed_markdown.source()[range.clone()], - ); - let content_range = content_range.start + range.start - ..content_range.end + range.start; - - let code = parsed_markdown.source()[content_range].to_string(); - let codeblock = render_copy_code_block_button( - range.end, - code, - self.markdown.clone(), - ); - el.child( - h_flex() - .w_4() - .absolute() - .top_1p5() - .right_1p5() - .justify_end() - .child(codeblock), - ) - }); - } - - if let CodeBlockRenderer::Default { - copy_button_on_hover: true, + copy_button_visibility, .. } = &self.code_block_renderer + && *copy_button_visibility != CopyButtonVisibility::Hidden { builder.modify_current_div(|el| { let content_range = parser::extract_code_block_content_range( @@ -1741,10 +1713,17 @@ impl Element for MarkdownElement { h_flex() .w_4() .absolute() - .top_0() - .right_0() .justify_end() - .visible_on_hover("code_block") + .when_else( + *copy_button_visibility + == CopyButtonVisibility::VisibleOnHover, + |this| { + this.top_0() + .right_0() + .visible_on_hover("code_block") + }, + |this| this.top_1p5().right_1p5(), + ) .child(codeblock), ) }); @@ -1770,7 +1749,6 @@ impl Element for MarkdownElement { } } MarkdownTagEnd::Table => { - builder.pop_div(); builder.pop_div(); builder.table.end(); } @@ -2778,8 +2756,7 @@ mod tests { |_window, _cx| { MarkdownElement::new(markdown, MarkdownStyle::default()).code_block_renderer( CodeBlockRenderer::Default { - copy_button: false, - copy_button_on_hover: false, + copy_button_visibility: CopyButtonVisibility::Hidden, border: false, }, ) diff --git a/crates/markdown/src/mermaid.rs b/crates/markdown/src/mermaid.rs index 15f3de4d8e8c64010fe96846b05d75f012c5fc0d..b8e40ebe7ec16cbbb8d9b11ab3edfc75da46f3a9 100644 --- a/crates/markdown/src/mermaid.rs +++ b/crates/markdown/src/mermaid.rs @@ -266,7 +266,10 @@ mod tests { CachedMermaidDiagram, MermaidDiagramCache, MermaidState, ParsedMarkdownMermaidDiagramContents, extract_mermaid_diagrams, parse_mermaid_info, }; - use crate::{CodeBlockRenderer, Markdown, MarkdownElement, MarkdownOptions, MarkdownStyle}; + use crate::{ + CodeBlockRenderer, CopyButtonVisibility, Markdown, MarkdownElement, MarkdownOptions, + MarkdownStyle, + }; use collections::HashMap; use gpui::{Context, IntoElement, Render, RenderImage, TestAppContext, Window, size}; use std::sync::Arc; @@ -309,8 +312,7 @@ mod tests { |_window, _cx| { MarkdownElement::new(markdown, MarkdownStyle::default()).code_block_renderer( CodeBlockRenderer::Default { - copy_button: false, - copy_button_on_hover: false, + copy_button_visibility: CopyButtonVisibility::Hidden, border: false, }, ) @@ -581,8 +583,7 @@ mod tests { |_window, _cx| { MarkdownElement::new(markdown.clone(), MarkdownStyle::default()) .code_block_renderer(CodeBlockRenderer::Default { - copy_button: false, - copy_button_on_hover: false, + copy_button_visibility: CopyButtonVisibility::Hidden, border: false, }) }, diff --git a/crates/markdown_preview/src/markdown_preview_view.rs b/crates/markdown_preview/src/markdown_preview_view.rs index 0b9c63c3b16f5686afcfdafdba119ede8c37fe3f..6dbf44c20f3ce453a7ef711e1854b806cf29737a 100644 --- a/crates/markdown_preview/src/markdown_preview_view.rs +++ b/crates/markdown_preview/src/markdown_preview_view.rs @@ -13,7 +13,8 @@ use gpui::{ }; use language::LanguageRegistry; use markdown::{ - CodeBlockRenderer, Markdown, MarkdownElement, MarkdownFont, MarkdownOptions, MarkdownStyle, + CodeBlockRenderer, CopyButtonVisibility, Markdown, MarkdownElement, MarkdownFont, + MarkdownOptions, MarkdownStyle, }; use settings::Settings; use theme_settings::ThemeSettings; @@ -580,20 +581,33 @@ impl MarkdownPreviewView { .as_ref() .map(|state| state.editor.clone()); + let mut workspace_directory = None; + if let Some(workspace_entity) = self.workspace.upgrade() { + let project = workspace_entity.read(cx).project(); + if let Some(tree) = project.read(cx).worktrees(cx).next() { + workspace_directory = Some(tree.read(cx).abs_path().to_path_buf()); + } + } + let mut markdown_element = MarkdownElement::new( self.markdown.clone(), MarkdownStyle::themed(MarkdownFont::Editor, window, cx), ) .code_block_renderer(CodeBlockRenderer::Default { - copy_button: false, - copy_button_on_hover: true, + copy_button_visibility: CopyButtonVisibility::VisibleOnHover, border: false, }) .scroll_handle(self.scroll_handle.clone()) .show_root_block_markers() .image_resolver({ let base_directory = self.base_directory.clone(); - move |dest_url| resolve_preview_image(dest_url, base_directory.as_deref()) + move |dest_url| { + resolve_preview_image( + dest_url, + base_directory.as_deref(), + workspace_directory.as_deref(), + ) + } }) .on_url_click(move |url, window, cx| { open_preview_url(url, base_directory.clone(), &workspace, window, cx); @@ -687,7 +701,11 @@ fn resolve_preview_path(url: &str, base_directory: Option<&Path>) -> Option) -> Option { +fn resolve_preview_image( + dest_url: &str, + base_directory: Option<&Path>, + workspace_directory: Option<&Path>, +) -> Option { if dest_url.starts_with("data:") { return None; } @@ -702,6 +720,19 @@ fn resolve_preview_image(dest_url: &str, base_directory: Option<&Path>) -> Optio .map(|decoded| decoded.into_owned()) .unwrap_or_else(|_| dest_url.to_string()); + let decoded_path = Path::new(&decoded); + + if let Ok(relative_path) = decoded_path.strip_prefix("/") { + if let Some(root) = workspace_directory { + let absolute_path = root.join(relative_path); + if absolute_path.exists() { + return Some(ImageSource::Resource(Resource::Path(Arc::from( + absolute_path.as_path(), + )))); + } + } + } + let path = if Path::new(&decoded).is_absolute() { PathBuf::from(decoded) } else { @@ -778,6 +809,9 @@ impl Render for MarkdownPreviewView { #[cfg(test)] mod tests { + use crate::markdown_preview_view::ImageSource; + use crate::markdown_preview_view::Resource; + use crate::markdown_preview_view::resolve_preview_image; use anyhow::Result; use std::fs; use tempfile::TempDir; @@ -819,6 +853,54 @@ mod tests { Ok(()) } + #[test] + fn resolves_workspace_absolute_preview_images() -> Result<()> { + let temp_dir = TempDir::new()?; + let workspace_directory = temp_dir.path(); + + let base_directory = workspace_directory.join("docs"); + fs::create_dir_all(&base_directory)?; + + let image_file = workspace_directory.join("test_image.png"); + fs::write(&image_file, "mock data")?; + + let resolved_success = resolve_preview_image( + "/test_image.png", + Some(&base_directory), + Some(workspace_directory), + ); + + match resolved_success { + Some(ImageSource::Resource(Resource::Path(p))) => { + assert_eq!(p.as_ref(), image_file.as_path()); + } + _ => panic!("Expected successful resolution to be a Resource::Path"), + } + + let resolved_missing = resolve_preview_image( + "/missing_image.png", + Some(&base_directory), + Some(workspace_directory), + ); + + let expected_missing_path = if std::path::Path::new("/missing_image.png").is_absolute() { + std::path::PathBuf::from("/missing_image.png") + } else { + // join is to retain windows path prefix C:/ + #[expect(clippy::join_absolute_paths)] + base_directory.join("/missing_image.png") + }; + + match resolved_missing { + Some(ImageSource::Resource(Resource::Path(p))) => { + assert_eq!(p.as_ref(), expected_missing_path.as_path()); + } + _ => panic!("Expected missing file to fallback to a Resource::Path"), + } + + Ok(()) + } + #[test] fn does_not_treat_web_links_as_preview_paths() { assert_eq!(resolve_preview_path("https://zed.dev", None), None); diff --git a/crates/migrator/src/migrations.rs b/crates/migrator/src/migrations.rs index c49df39d59abaa924edb6c986c63701952dce01e..d554ee1dd887d6048f55a584ed2534db944b3c08 100644 --- a/crates/migrator/src/migrations.rs +++ b/crates/migrator/src/migrations.rs @@ -316,9 +316,3 @@ pub(crate) mod m_2026_03_23 { pub(crate) use keymap::KEYMAP_PATTERNS; } - -pub(crate) mod m_2026_03_31 { - mod settings; - - pub(crate) use settings::remove_text_thread_settings; -} diff --git a/crates/migrator/src/migrations/m_2026_03_31/settings.rs b/crates/migrator/src/migrations/m_2026_03_31/settings.rs deleted file mode 100644 index 1a3fdb109f3773bada7a5fd5c00b1947e556e4c9..0000000000000000000000000000000000000000 --- a/crates/migrator/src/migrations/m_2026_03_31/settings.rs +++ /dev/null @@ -1,29 +0,0 @@ -use anyhow::Result; -use serde_json::Value; - -use crate::migrations::migrate_settings; - -pub fn remove_text_thread_settings(value: &mut Value) -> Result<()> { - migrate_settings(value, &mut migrate_one) -} - -fn migrate_one(obj: &mut serde_json::Map) -> Result<()> { - // Remove `agent.default_view` - if let Some(agent) = obj.get_mut("agent") { - if let Some(agent_obj) = agent.as_object_mut() { - agent_obj.remove("default_view"); - } - } - - // Remove `edit_predictions.enabled_in_text_threads` - if let Some(edit_predictions) = obj.get_mut("edit_predictions") { - if let Some(edit_predictions_obj) = edit_predictions.as_object_mut() { - edit_predictions_obj.remove("enabled_in_text_threads"); - } - } - - // Remove top-level `slash_commands` - obj.remove("slash_commands"); - - Ok(()) -} diff --git a/crates/migrator/src/migrator.rs b/crates/migrator/src/migrator.rs index 46cccfc4055a78a27d12da54ee187a0fdc202917..ceb6ec2e0e35f0dd3bbd23174637bba00baab6b3 100644 --- a/crates/migrator/src/migrator.rs +++ b/crates/migrator/src/migrator.rs @@ -247,7 +247,6 @@ pub fn migrate_settings(text: &str) -> Result> { migrations::m_2026_03_16::SETTINGS_PATTERNS, &SETTINGS_QUERY_2026_03_16, ), - MigrationType::Json(migrations::m_2026_03_31::remove_text_thread_settings), ]; run_migrations(text, migrations) } @@ -941,7 +940,8 @@ mod tests { "foo": "bar" }, "edit_predictions": { - } + "enabled_in_text_threads": false, + } }"#, ), ); @@ -4480,109 +4480,4 @@ mod tests { ), ); } - - #[test] - fn test_remove_text_thread_settings() { - assert_migrate_with_migrations( - &[MigrationType::Json( - migrations::m_2026_03_31::remove_text_thread_settings, - )], - r#"{ - "agent": { - "default_model": { - "provider": "anthropic", - "model": "claude-sonnet" - }, - "default_view": "text_thread" - }, - "edit_predictions": { - "mode": "eager", - "enabled_in_text_threads": true - }, - "slash_commands": { - "cargo_workspace": { - "enabled": true - } - } -}"#, - Some( - r#"{ - "agent": { - "default_model": { - "provider": "anthropic", - "model": "claude-sonnet" - } - }, - "edit_predictions": { - "mode": "eager" - } -}"#, - ), - ); - } - - #[test] - fn test_remove_text_thread_settings_only_default_view() { - assert_migrate_with_migrations( - &[MigrationType::Json( - migrations::m_2026_03_31::remove_text_thread_settings, - )], - r#"{ - "agent": { - "default_model": "claude-sonnet", - "default_view": "thread" - } -}"#, - Some( - r#"{ - "agent": { - "default_model": "claude-sonnet" - } -}"#, - ), - ); - } - - #[test] - fn test_remove_text_thread_settings_only_slash_commands() { - assert_migrate_with_migrations( - &[MigrationType::Json( - migrations::m_2026_03_31::remove_text_thread_settings, - )], - r#"{ - "slash_commands": { - "cargo_workspace": { - "enabled": true - } - }, - "vim_mode": true -}"#, - Some( - r#"{ - "vim_mode": true -}"#, - ), - ); - } - - #[test] - fn test_remove_text_thread_settings_none_present() { - assert_migrate_with_migrations( - &[MigrationType::Json( - migrations::m_2026_03_31::remove_text_thread_settings, - )], - r#"{ - "agent": { - "default_model": { - "provider": "anthropic", - "model": "claude-sonnet" - } - }, - "edit_predictions": { - "mode": "eager" - } -}"#, - None, - ); - } } diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index 4dc06036ef8416fd859cc815ab090ba5896c0040..22987f6c56669e1972a9bfc940449991d9f55642 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -1264,13 +1264,8 @@ impl PickerDelegate for RecentProjectsDelegate { this.tooltip(Tooltip::text(path.to_string_lossy().to_string())) }), ) - .map(|el| { - if self.selected_index == ix { - el.end_slot(secondary_actions) - } else { - el.end_hover_slot(secondary_actions) - } - }) + .end_slot(secondary_actions) + .show_end_slot_on_hover() .into_any_element(), ) } @@ -1363,13 +1358,8 @@ impl PickerDelegate for RecentProjectsDelegate { }) .tooltip(Tooltip::text(tooltip_path)), ) - .map(|el| { - if self.selected_index == ix { - el.end_slot(secondary_actions) - } else { - el.end_hover_slot(secondary_actions) - } - }) + .end_slot(secondary_actions) + .show_end_slot_on_hover() .into_any_element(), ) } @@ -1503,13 +1493,8 @@ impl PickerDelegate for RecentProjectsDelegate { }) .tooltip(Tooltip::text(tooltip_path)), ) - .map(|el| { - if self.selected_index == ix { - el.end_slot(secondary_actions) - } else { - el.end_hover_slot(secondary_actions) - } - }) + .end_slot(secondary_actions) + .show_end_slot_on_hover() .into_any_element(), ) } diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index f7054687579155d4895ae191de1b7fa7cd14fbf6..26592a8035d50caa4e267a5478d5aceb9fba6e3e 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -1621,23 +1621,24 @@ impl RemoteServerProjects { })) .tooltip(Tooltip::text(project.paths.join("\n"))) .when(is_from_zed, |server_list_item| { - server_list_item.end_hover_slot::(Some( - div() - .mr_2() - .child({ - let project = project.clone(); - // Right-margin to offset it from the Scrollbar - IconButton::new("remove-remote-project", IconName::Trash) - .icon_size(IconSize::Small) - .shape(IconButtonShape::Square) - .size(ButtonSize::Large) - .tooltip(Tooltip::text("Delete Remote Project")) - .on_click(cx.listener(move |this, _, _, cx| { - this.delete_remote_project(server_ix, &project, cx) - })) - }) - .into_any_element(), - )) + server_list_item + .end_slot( + div() + .mr_2() + .child({ + let project = project.clone(); + IconButton::new("remove-remote-project", IconName::Trash) + .icon_size(IconSize::Small) + .shape(IconButtonShape::Square) + .size(ButtonSize::Large) + .tooltip(Tooltip::text("Delete Remote Project")) + .on_click(cx.listener(move |this, _, _, cx| { + this.delete_remote_project(server_ix, &project, cx) + })) + }) + .into_any_element(), + ) + .show_end_slot_on_hover() }), ) } @@ -2413,9 +2414,8 @@ impl RemoteServerProjects { .spacing(ui::ListItemSpacing::Sparse) .start_slot(Icon::new(IconName::Copy).color(Color::Muted)) .child(Label::new("Copy Server Address")) - .end_hover_slot( - Label::new(connection_string.clone()).color(Color::Muted), - ) + .end_slot(Label::new(connection_string.clone()).color(Color::Muted)) + .show_end_slot_on_hover() .on_click({ let connection_string = connection_string.clone(); move |_, _, cx| { diff --git a/crates/rules_library/src/rules_library.rs b/crates/rules_library/src/rules_library.rs index 23e7b83772f755089c49f824719af389ec589bd9..7e5a56f22d48c4d51f60d7d200dc8384582beb23 100644 --- a/crates/rules_library/src/rules_library.rs +++ b/crates/rules_library/src/rules_library.rs @@ -389,7 +389,7 @@ impl PickerDelegate for RulePickerDelegate { })) })) .when(!prompt_id.is_built_in(), |this| { - this.end_hover_slot( + this.end_slot_on_hover( h_flex() .child( IconButton::new("delete-rule", IconName::Trash) diff --git a/crates/settings_content/src/agent.rs b/crates/settings_content/src/agent.rs index f9d3376a26b8d84d89e563b21a969bfca68ee2f7..dae5c99b9ef9b5b3892b1201ff9a1686330dc365 100644 --- a/crates/settings_content/src/agent.rs +++ b/crates/settings_content/src/agent.rs @@ -81,11 +81,14 @@ pub enum SidebarSide { )] #[serde(rename_all = "snake_case")] pub enum ThinkingBlockDisplay { + /// Thinking blocks fully expand during streaming, then auto-collapse + /// when the model finishes thinking. Users can re-expand after collapse. + #[default] + Auto, /// Thinking blocks auto-expand with a height constraint during streaming, /// then remain in their constrained state when complete. Users can click /// to fully expand or collapse. - #[default] - Automatic, + Preview, /// Thinking blocks are always fully expanded by default (no height constraint). AlwaysExpanded, /// Thinking blocks are always collapsed by default. diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index 08a597dc992913e144ba70e30c1a81b2ab8de1aa..b6d10424f4a6cf0710a916410e0e6068d80d6064 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -7340,7 +7340,7 @@ fn ai_page(cx: &App) -> SettingsPage { }), SettingsPageItem::SettingItem(SettingItem { title: "Thinking Display", - description: "How thinking blocks should be displayed by default. 'Automatic' auto-expands with a height constraint during streaming. 'Always Expanded' shows full content. 'Always Collapsed' keeps them collapsed.", + description: "How thinking blocks should be displayed by default. 'Auto' fully expands during streaming, then auto-collapses when done. 'Preview' auto-expands with a height constraint during streaming. 'Always Expanded' shows full content. 'Always Collapsed' keeps them collapsed.", field: Box::new(SettingField { json_path: Some("agent.thinking_display"), pick: |settings_content| { diff --git a/crates/sidebar/src/sidebar.rs b/crates/sidebar/src/sidebar.rs index 450a2674e0062d917003758c41c445048ee603f7..e09ee3e8809417924b1b1b43f25cee75834568a1 100644 --- a/crates/sidebar/src/sidebar.rs +++ b/crates/sidebar/src/sidebar.rs @@ -3309,10 +3309,24 @@ impl Sidebar { } fn render_sidebar_bottom_bar(&mut self, cx: &mut Context) -> impl IntoElement { - let on_right = self.side(cx) == SidebarSide::Right; let is_archive = matches!(self.view, SidebarView::Archive(..)); + let show_import_button = is_archive && !self.should_render_acp_import_onboarding(cx); + let on_right = self.side(cx) == SidebarSide::Right; + let action_buttons = h_flex() .gap_1() + .when(on_right, |this| this.flex_row_reverse()) + .when(show_import_button, |this| { + this.child( + IconButton::new("thread-import", IconName::ThreadImport) + .icon_size(IconSize::Small) + .tooltip(Tooltip::text("Import ACP Threads")) + .on_click(cx.listener(|this, _, window, cx| { + this.show_archive(window, cx); + this.show_thread_import_modal(window, cx); + })), + ) + }) .child( IconButton::new("archive", IconName::Archive) .icon_size(IconSize::Small) @@ -3325,21 +3339,16 @@ impl Sidebar { })), ) .child(self.render_recent_projects_button(cx)); - let border_color = cx.theme().colors().border; - let toggle_button = self.render_sidebar_toggle_button(cx); - let bar = h_flex() + h_flex() .p_1() .gap_1() + .when(on_right, |this| this.flex_row_reverse()) .justify_between() .border_t_1() - .border_color(border_color); - - if on_right { - bar.child(action_buttons).child(toggle_button) - } else { - bar.child(toggle_button).child(action_buttons) - } + .border_color(cx.theme().colors().border) + .child(self.render_sidebar_toggle_button(cx)) + .child(action_buttons) } fn active_workspace(&self, cx: &App) -> Option> { @@ -3409,7 +3418,7 @@ impl Sidebar { v_flex() .min_w_0() .w_full() - .p_1p5() + .p_2() .border_t_1() .border_color(cx.theme().colors().border) .bg(linear_gradient( @@ -3437,8 +3446,8 @@ impl Sidebar { .style(ButtonStyle::OutlinedCustom(cx.theme().colors().border)) .label_size(LabelSize::Small) .start_icon( - Icon::new(IconName::ArrowDown) - .size(IconSize::XSmall) + Icon::new(IconName::ThreadImport) + .size(IconSize::Small) .color(Color::Muted), ) .on_click(cx.listener(|this, _, window, cx| { @@ -3467,9 +3476,6 @@ impl Sidebar { let Some(agent_panel) = active_workspace.read(cx).panel::(cx) else { return; }; - let Some(agent_registry_store) = AgentRegistryStore::try_global(cx) else { - return; - }; let agent_server_store = active_workspace .read(cx) @@ -3482,11 +3488,9 @@ impl Sidebar { let archive_view = cx.new(|cx| { ThreadsArchiveView::new( + active_workspace.downgrade(), agent_connection_store.clone(), agent_server_store.clone(), - agent_registry_store.downgrade(), - active_workspace.downgrade(), - self.multi_workspace.clone(), window, cx, ) diff --git a/crates/tab_switcher/src/tab_switcher.rs b/crates/tab_switcher/src/tab_switcher.rs index 0fb13c85d21797e4d57728c88fc8bb014a898f78..d1e19ea4faee8d8259d06e2c24875faac7a0117c 100644 --- a/crates/tab_switcher/src/tab_switcher.rs +++ b/crates/tab_switcher/src/tab_switcher.rs @@ -875,7 +875,7 @@ impl PickerDelegate for TabSwitcherDelegate { el.end_slot::(close_button) } else { el.end_slot::(indicator) - .end_hover_slot::(close_button) + .end_slot_on_hover::(close_button) } }), ) diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index 34f0cd809692d649bcfbabb7952f3075618ead04..285a07c9562849b26b4cbba3de3979614384d875 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -570,7 +570,7 @@ impl PickerDelegate for TasksModalDelegate { Tooltip::simple("Delete Previously Scheduled Task", cx) }), ); - item.end_hover_slot(delete_button) + item.end_slot_on_hover(delete_button) } else { item } diff --git a/crates/title_bar/src/onboarding_banner.rs b/crates/title_bar/src/onboarding_banner.rs index f96ce3a92740da4a0aac3dc154384f20f3b05eb0..96400a91a0a26fdc6a4c1acb6387f27c3077e393 100644 --- a/crates/title_bar/src/onboarding_banner.rs +++ b/crates/title_bar/src/onboarding_banner.rs @@ -1,3 +1,7 @@ +// This module provides infrastructure for showing onboarding banners in the title bar. +// It's currently not in use but is kept for future feature announcements. +#![allow(dead_code)] + use gpui::{Action, Entity, Global, Render, SharedString}; use ui::{ButtonLike, Tooltip, prelude::*}; use util::ResultExt; @@ -94,21 +98,21 @@ fn persist_dismissed(source: &str, cx: &mut App) { } pub fn restore_banner(cx: &mut App) { - cx.defer(|cx| { - cx.global::() - .entity - .clone() - .update(cx, |this, cx| { + if let Some(banner_global) = cx.try_global::() { + let entity = banner_global.entity.clone(); + cx.defer(move |cx| { + entity.update(cx, |this, cx| { this.dismissed = false; cx.notify(); }); - }); + }); - let source = &cx.global::().entity.read(cx).source; - let dismissed_at = dismissed_at_key(source); - let kvp = db::kvp::KeyValueStore::global(cx); - cx.spawn(async move |_| kvp.delete_kvp(dismissed_at).await) - .detach_and_log_err(cx); + let source = &cx.global::().entity.read(cx).source; + let dismissed_at = dismissed_at_key(source); + let kvp = db::kvp::KeyValueStore::global(cx); + cx.spawn(async move |_| kvp.delete_kvp(dismissed_at).await) + .detach_and_log_err(cx); + } } impl Render for OnboardingBanner { diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 42c348bacb680e2a09586d0dc0279fc8c95d1604..440249907adb6d29602ad8e950d0fd26a2d1c31d 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -155,7 +155,7 @@ pub struct TitleBar { multi_workspace: Option>, application_menu: Option>, _subscriptions: Vec, - banner: Entity, + banner: Option>, update_version: Entity, screen_share_popover_handle: PopoverMenuHandle, _diagnostics_subscription: Option, @@ -246,7 +246,9 @@ impl Render for TitleBar { children.push(self.render_collaborator_list(window, cx).into_any_element()); if title_bar_settings.show_onboarding_banner { - children.push(self.banner.clone().into_any_element()) + if let Some(banner) = &self.banner { + children.push(banner.clone().into_any_element()) + } } let status = self.client.status(); @@ -385,19 +387,6 @@ impl TitleBar { })); } - let banner = cx.new(|cx| { - OnboardingBanner::new( - "ACP Claude Code Onboarding", - IconName::AiClaude, - "Claude Agent", - Some("Introducing:".into()), - zed_actions::agent::OpenClaudeAgentOnboardingModal.boxed_clone(), - cx, - ) - // When updating this to a non-AI feature release, remove this line. - .visible_when(|cx| !project::DisableAiSettings::get_global(cx).disable_ai) - }); - let update_version = cx.new(|cx| UpdateVersion::new(cx)); let platform_titlebar = cx.new(|cx| { let mut titlebar = PlatformTitleBar::new(id, cx); @@ -416,7 +405,7 @@ impl TitleBar { user_store, client, _subscriptions: subscriptions, - banner, + banner: None, update_version, screen_share_popover_handle: PopoverMenuHandle::default(), _diagnostics_subscription: None, diff --git a/crates/ui/src/components/data_table.rs b/crates/ui/src/components/data_table.rs index 3da30838ca8313b68608e432ce1e76870157c1fd..2012defc47d9cccea87849fa41470ad1183b552f 100644 --- a/crates/ui/src/components/data_table.rs +++ b/crates/ui/src/components/data_table.rs @@ -1,14 +1,15 @@ use std::{ops::Range, rc::Rc}; use gpui::{ - AbsoluteLength, AppContext, Context, DefiniteLength, DragMoveEvent, Entity, EntityId, - FocusHandle, Length, ListHorizontalSizingBehavior, ListSizingBehavior, ListState, Point, - Stateful, UniformListScrollHandle, WeakEntity, list, transparent_black, uniform_list, + AbsoluteLength, AppContext as _, DefiniteLength, DragMoveEvent, Entity, EntityId, FocusHandle, + Length, ListHorizontalSizingBehavior, ListSizingBehavior, ListState, Point, Stateful, + UniformListScrollHandle, WeakEntity, list, transparent_black, uniform_list, }; +use itertools::intersperse_with; use crate::{ ActiveTheme as _, AnyElement, App, Button, ButtonCommon as _, ButtonStyle, Color, Component, - ComponentScope, Div, ElementId, FixedWidth as _, FluentBuilder as _, Indicator, + ComponentScope, Context, Div, ElementId, FixedWidth as _, FluentBuilder as _, Indicator, InteractiveElement, IntoElement, ParentElement, Pixels, RegisterComponent, RenderOnce, ScrollAxes, ScrollableHandle, Scrollbars, SharedString, StatefulInteractiveElement, Styled, StyledExt as _, StyledTypography, Window, WithScrollbar, div, example_group_with_title, h_flex, @@ -16,20 +17,20 @@ use crate::{ table_row::{IntoTableRow as _, TableRow}, v_flex, }; -use itertools::intersperse_with; pub mod table_row; #[cfg(test)] mod tests; const RESIZE_COLUMN_WIDTH: f32 = 8.0; +const RESIZE_DIVIDER_WIDTH: f32 = 1.0; /// Represents an unchecked table row, which is a vector of elements. /// Will be converted into `TableRow` internally pub type UncheckedTableRow = Vec; #[derive(Debug)] -struct DraggedColumn(usize); +pub(crate) struct DraggedColumn(pub(crate) usize); struct UniformListData { render_list_of_rows_fn: @@ -110,106 +111,103 @@ impl TableInteractionState { view.update(cx, |view, cx| f(view, e, window, cx)).ok(); } } +} - /// Renders invisible resize handles overlaid on top of table content. - /// - /// - Spacer: invisible element that matches the width of table column content - /// - Divider: contains the actual resize handle that users can drag to resize columns - /// - /// Structure: [spacer] [divider] [spacer] [divider] [spacer] - /// - /// Business logic: - /// 1. Creates spacers matching each column width - /// 2. Intersperses (inserts) resize handles between spacers (interactive only for resizable columns) - /// 3. Each handle supports hover highlighting, double-click to reset, and drag to resize - /// 4. Returns an absolute-positioned overlay that sits on top of table content - fn render_resize_handles( - &self, - column_widths: &TableRow, - resizable_columns: &TableRow, - initial_sizes: &TableRow, - columns: Option>, - window: &mut Window, - cx: &mut App, - ) -> AnyElement { - let spacers = column_widths - .as_slice() - .iter() - .map(|width| base_cell_style(Some(*width)).into_any_element()); - - let mut column_ix = 0; - let resizable_columns_shared = Rc::new(resizable_columns.clone()); - let initial_sizes_shared = Rc::new(initial_sizes.clone()); - let mut resizable_columns_iter = resizable_columns.as_slice().iter(); - - // Insert dividers between spacers (column content) - let dividers = intersperse_with(spacers, || { - let resizable_columns = Rc::clone(&resizable_columns_shared); - let initial_sizes = Rc::clone(&initial_sizes_shared); - window.with_id(column_ix, |window| { - let mut resize_divider = div() - // This is required because this is evaluated at a different time than the use_state call above - .id(column_ix) - .relative() - .top_0() - .w_px() - .h_full() - .bg(cx.theme().colors().border.opacity(0.8)); - - let mut resize_handle = div() - .id("column-resize-handle") - .absolute() - .left_neg_0p5() - .w(px(RESIZE_COLUMN_WIDTH)) - .h_full(); - - if resizable_columns_iter - .next() - .is_some_and(TableResizeBehavior::is_resizable) - { - let hovered = window.use_state(cx, |_window, _cx| false); - - resize_divider = resize_divider.when(*hovered.read(cx), |div| { - div.bg(cx.theme().colors().border_focused) - }); - - resize_handle = resize_handle - .on_hover(move |&was_hovered, _, cx| hovered.write(cx, was_hovered)) - .cursor_col_resize() - .when_some(columns.clone(), |this, columns| { - this.on_click(move |event, window, cx| { - if event.click_count() >= 2 { - columns.update(cx, |columns, _| { - columns.on_double_click( - column_ix, - &initial_sizes, - &resizable_columns, - window, - ); - }) - } +/// Renders invisible resize handles overlaid on top of table content. +/// +/// - Spacer: invisible element that matches the width of table column content +/// - Divider: contains the actual resize handle that users can drag to resize columns +/// +/// Structure: [spacer] [divider] [spacer] [divider] [spacer] +/// +/// Business logic: +/// 1. Creates spacers matching each column width +/// 2. Intersperses (inserts) resize handles between spacers (interactive only for resizable columns) +/// 3. Each handle supports hover highlighting, double-click to reset, and drag to resize +/// 4. Returns an absolute-positioned overlay that sits on top of table content +fn render_resize_handles( + column_widths: &TableRow, + resizable_columns: &TableRow, + initial_sizes: &TableRow, + columns: Option>, + window: &mut Window, + cx: &mut App, +) -> AnyElement { + let spacers = column_widths + .as_slice() + .iter() + .map(|width| base_cell_style(Some(*width)).into_any_element()); + + let mut column_ix = 0; + let resizable_columns_shared = Rc::new(resizable_columns.clone()); + let initial_sizes_shared = Rc::new(initial_sizes.clone()); + let mut resizable_columns_iter = resizable_columns.as_slice().iter(); + + let dividers = intersperse_with(spacers, || { + let resizable_columns = Rc::clone(&resizable_columns_shared); + let initial_sizes = Rc::clone(&initial_sizes_shared); + window.with_id(column_ix, |window| { + let mut resize_divider = div() + .id(column_ix) + .relative() + .top_0() + .w(px(RESIZE_DIVIDER_WIDTH)) + .h_full() + .bg(cx.theme().colors().border.opacity(0.8)); + + let mut resize_handle = div() + .id("column-resize-handle") + .absolute() + .left_neg_0p5() + .w(px(RESIZE_COLUMN_WIDTH)) + .h_full(); + + if resizable_columns_iter + .next() + .is_some_and(TableResizeBehavior::is_resizable) + { + let hovered = window.use_state(cx, |_window, _cx| false); + + resize_divider = resize_divider.when(*hovered.read(cx), |div| { + div.bg(cx.theme().colors().border_focused) + }); + + resize_handle = resize_handle + .on_hover(move |&was_hovered, _, cx| hovered.write(cx, was_hovered)) + .cursor_col_resize() + .when_some(columns.clone(), |this, columns| { + this.on_click(move |event, window, cx| { + if event.click_count() >= 2 { + columns.update(cx, |columns, _| { + columns.on_double_click( + column_ix, + &initial_sizes, + &resizable_columns, + window, + ); + }) + } - cx.stop_propagation(); - }) + cx.stop_propagation(); }) - .on_drag(DraggedColumn(column_ix), |_, _offset, _window, cx| { - cx.new(|_cx| gpui::Empty) - }) - } + }) + .on_drag(DraggedColumn(column_ix), |_, _offset, _window, cx| { + cx.new(|_cx| gpui::Empty) + }) + } - column_ix += 1; - resize_divider.child(resize_handle).into_any_element() - }) - }); + column_ix += 1; + resize_divider.child(resize_handle).into_any_element() + }) + }); - h_flex() - .id("resize-handles") - .absolute() - .inset_0() - .w_full() - .children(dividers) - .into_any_element() - } + h_flex() + .id("resize-handles") + .absolute() + .inset_0() + .w_full() + .children(dividers) + .into_any_element() } #[derive(Debug, Copy, Clone, PartialEq)] @@ -233,25 +231,181 @@ impl TableResizeBehavior { } } -pub struct TableColumnWidths { - widths: TableRow, - visible_widths: TableRow, - cached_bounds_width: Pixels, - initialized: bool, +pub enum ColumnWidthConfig { + /// Static column widths (no resize handles). + Static { + widths: StaticColumnWidths, + /// Controls widths of the whole table. + table_width: Option, + }, + /// Redistributable columns — dragging redistributes the fixed available space + /// among columns without changing the overall table width. + Redistributable { + columns_state: Entity, + table_width: Option, + }, +} + +pub enum StaticColumnWidths { + /// All columns share space equally (flex-1 / Length::Auto). + Auto, + /// Each column has a specific width. + Explicit(TableRow), } -impl TableColumnWidths { - pub fn new(cols: usize, _: &mut App) -> Self { +impl ColumnWidthConfig { + /// Auto-width columns, auto-size table. + pub fn auto() -> Self { + ColumnWidthConfig::Static { + widths: StaticColumnWidths::Auto, + table_width: None, + } + } + + /// Redistributable columns with no fixed table width. + pub fn redistributable(columns_state: Entity) -> Self { + ColumnWidthConfig::Redistributable { + columns_state, + table_width: None, + } + } + + /// Auto-width columns, fixed table width. + pub fn auto_with_table_width(width: impl Into) -> Self { + ColumnWidthConfig::Static { + widths: StaticColumnWidths::Auto, + table_width: Some(width.into()), + } + } + + /// Column widths for rendering. + pub fn widths_to_render(&self, cx: &App) -> Option> { + match self { + ColumnWidthConfig::Static { + widths: StaticColumnWidths::Auto, + .. + } => None, + ColumnWidthConfig::Static { + widths: StaticColumnWidths::Explicit(widths), + .. + } => Some(widths.map_cloned(Length::Definite)), + ColumnWidthConfig::Redistributable { + columns_state: entity, + .. + } => { + let state = entity.read(cx); + Some(state.preview_widths.map_cloned(Length::Definite)) + } + } + } + + /// Table-level width. + pub fn table_width(&self) -> Option { + match self { + ColumnWidthConfig::Static { table_width, .. } + | ColumnWidthConfig::Redistributable { table_width, .. } => { + table_width.map(Length::Definite) + } + } + } + + /// ListHorizontalSizingBehavior for uniform_list. + pub fn list_horizontal_sizing(&self) -> ListHorizontalSizingBehavior { + match self.table_width() { + Some(_) => ListHorizontalSizingBehavior::Unconstrained, + None => ListHorizontalSizingBehavior::FitList, + } + } + + /// Render resize handles overlay if applicable. + pub fn render_resize_handles(&self, window: &mut Window, cx: &mut App) -> Option { + match self { + ColumnWidthConfig::Redistributable { + columns_state: entity, + .. + } => { + let (column_widths, resize_behavior, initial_widths) = { + let state = entity.read(cx); + ( + state.preview_widths.map_cloned(Length::Definite), + state.resize_behavior.clone(), + state.initial_widths.clone(), + ) + }; + Some(render_resize_handles( + &column_widths, + &resize_behavior, + &initial_widths, + Some(entity.clone()), + window, + cx, + )) + } + _ => None, + } + } + + /// Returns info needed for header double-click-to-reset, if applicable. + pub fn header_resize_info(&self, cx: &App) -> Option { + match self { + ColumnWidthConfig::Redistributable { columns_state, .. } => { + let state = columns_state.read(cx); + Some(HeaderResizeInfo { + columns_state: columns_state.downgrade(), + resize_behavior: state.resize_behavior.clone(), + initial_widths: state.initial_widths.clone(), + }) + } + _ => None, + } + } +} + +#[derive(Clone)] +pub struct HeaderResizeInfo { + pub columns_state: WeakEntity, + pub resize_behavior: TableRow, + pub initial_widths: TableRow, +} + +pub struct RedistributableColumnsState { + pub(crate) initial_widths: TableRow, + pub(crate) committed_widths: TableRow, + pub(crate) preview_widths: TableRow, + pub(crate) resize_behavior: TableRow, + pub(crate) cached_table_width: Pixels, +} + +impl RedistributableColumnsState { + pub fn new( + cols: usize, + initial_widths: UncheckedTableRow>, + resize_behavior: UncheckedTableRow, + ) -> Self { + let widths: TableRow = initial_widths + .into_iter() + .map(Into::into) + .collect::>() + .into_table_row(cols); Self { - widths: vec![DefiniteLength::default(); cols].into_table_row(cols), - visible_widths: vec![DefiniteLength::default(); cols].into_table_row(cols), - cached_bounds_width: Default::default(), - initialized: false, + initial_widths: widths.clone(), + committed_widths: widths.clone(), + preview_widths: widths, + resize_behavior: resize_behavior.into_table_row(cols), + cached_table_width: Default::default(), } } pub fn cols(&self) -> usize { - self.widths.cols() + self.committed_widths.cols() + } + + pub fn initial_widths(&self) -> &TableRow { + &self.initial_widths + } + + pub fn resize_behavior(&self) -> &TableRow { + &self.resize_behavior } fn get_fraction(length: &DefiniteLength, bounds_width: Pixels, rem_size: Pixels) -> f32 { @@ -264,19 +418,19 @@ impl TableColumnWidths { } } - fn on_double_click( + pub(crate) fn on_double_click( &mut self, double_click_position: usize, initial_sizes: &TableRow, resize_behavior: &TableRow, window: &mut Window, ) { - let bounds_width = self.cached_bounds_width; + let bounds_width = self.cached_table_width; let rem_size = window.rem_size(); let initial_sizes = initial_sizes.map_ref(|length| Self::get_fraction(length, bounds_width, rem_size)); let widths = self - .widths + .committed_widths .map_ref(|length| Self::get_fraction(length, bounds_width, rem_size)); let updated_widths = Self::reset_to_initial_size( @@ -285,53 +439,16 @@ impl TableColumnWidths { initial_sizes, resize_behavior, ); - self.widths = updated_widths.map(DefiniteLength::Fraction); - self.visible_widths = self.widths.clone(); // previously was copy + self.committed_widths = updated_widths.map(DefiniteLength::Fraction); + self.preview_widths = self.committed_widths.clone(); } - fn reset_to_initial_size( + pub(crate) fn reset_to_initial_size( col_idx: usize, mut widths: TableRow, initial_sizes: TableRow, resize_behavior: &TableRow, ) -> TableRow { - // RESET: - // Part 1: - // Figure out if we should shrink/grow the selected column - // Get diff which represents the change in column we want to make initial size delta curr_size = diff - // - // Part 2: We need to decide which side column we should move and where - // - // If we want to grow our column we should check the left/right columns diff to see what side - // has a greater delta than their initial size. Likewise, if we shrink our column we should check - // the left/right column diffs to see what side has the smallest delta. - // - // Part 3: resize - // - // col_idx represents the column handle to the right of an active column - // - // If growing and right has the greater delta { - // shift col_idx to the right - // } else if growing and left has the greater delta { - // shift col_idx - 1 to the left - // } else if shrinking and the right has the greater delta { - // shift - // } { - // - // } - // } - // - // if we need to shrink, then if the right - // - - // DRAGGING - // we get diff which represents the change in the _drag handle_ position - // -diff => dragging left -> - // grow the column to the right of the handle as much as we can shrink columns to the left of the handle - // +diff => dragging right -> growing handles column - // grow the column to the left of the handle as much as we can shrink columns to the right of the handle - // - let diff = initial_sizes[col_idx] - widths[col_idx]; let left_diff = @@ -376,10 +493,9 @@ impl TableColumnWidths { widths } - fn on_drag_move( + pub(crate) fn on_drag_move( &mut self, drag_event: &DragMoveEvent, - resize_behavior: &TableRow, window: &mut Window, cx: &mut Context, ) { @@ -391,43 +507,42 @@ impl TableColumnWidths { let bounds_width = bounds.right() - bounds.left(); let col_idx = drag_event.drag(cx).0; - let column_handle_width = Self::get_fraction( - &DefiniteLength::Absolute(AbsoluteLength::Pixels(px(RESIZE_COLUMN_WIDTH))), + let divider_width = Self::get_fraction( + &DefiniteLength::Absolute(AbsoluteLength::Pixels(px(RESIZE_DIVIDER_WIDTH))), bounds_width, rem_size, ); let mut widths = self - .widths + .committed_widths .map_ref(|length| Self::get_fraction(length, bounds_width, rem_size)); for length in widths[0..=col_idx].iter() { - col_position += length + column_handle_width; + col_position += length + divider_width; } let mut total_length_ratio = col_position; for length in widths[col_idx + 1..].iter() { total_length_ratio += length; } - let cols = resize_behavior.cols(); - total_length_ratio += (cols - 1 - col_idx) as f32 * column_handle_width; + let cols = self.resize_behavior.cols(); + total_length_ratio += (cols - 1 - col_idx) as f32 * divider_width; let drag_fraction = (drag_position.x - bounds.left()) / bounds_width; let drag_fraction = drag_fraction * total_length_ratio; - let diff = drag_fraction - col_position - column_handle_width / 2.0; + let diff = drag_fraction - col_position - divider_width / 2.0; - Self::drag_column_handle(diff, col_idx, &mut widths, resize_behavior); + Self::drag_column_handle(diff, col_idx, &mut widths, &self.resize_behavior); - self.visible_widths = widths.map(DefiniteLength::Fraction); + self.preview_widths = widths.map(DefiniteLength::Fraction); } - fn drag_column_handle( + pub(crate) fn drag_column_handle( diff: f32, col_idx: usize, widths: &mut TableRow, resize_behavior: &TableRow, ) { - // if diff > 0.0 then go right if diff > 0.0 { Self::propagate_resize_diff(diff, col_idx, widths, resize_behavior, 1); } else { @@ -435,7 +550,7 @@ impl TableColumnWidths { } } - fn propagate_resize_diff( + pub(crate) fn propagate_resize_diff( diff: f32, col_idx: usize, widths: &mut TableRow, @@ -493,44 +608,16 @@ impl TableColumnWidths { } } -pub struct TableWidths { - initial: TableRow, - current: Option>, - resizable: TableRow, -} - -impl TableWidths { - pub fn new(widths: TableRow>) -> Self { - let widths = widths.map(Into::into); - - let expected_length = widths.cols(); - TableWidths { - initial: widths, - current: None, - resizable: vec![TableResizeBehavior::None; expected_length] - .into_table_row(expected_length), - } - } - - fn lengths(&self, cx: &App) -> TableRow { - self.current - .as_ref() - .map(|entity| entity.read(cx).visible_widths.map_cloned(Length::Definite)) - .unwrap_or_else(|| self.initial.map_cloned(Length::Definite)) - } -} - /// A table component #[derive(RegisterComponent, IntoElement)] pub struct Table { striped: bool, show_row_borders: bool, show_row_hover: bool, - width: Option, headers: Option>, rows: TableContents, interaction_state: Option>, - col_widths: Option, + column_width_config: ColumnWidthConfig, map_row: Option), &mut Window, &mut App) -> AnyElement>>, use_ui_font: bool, empty_table_callback: Option AnyElement>>, @@ -547,15 +634,14 @@ impl Table { striped: false, show_row_borders: true, show_row_hover: true, - width: None, headers: None, rows: TableContents::Vec(Vec::new()), interaction_state: None, map_row: None, use_ui_font: true, empty_table_callback: None, - col_widths: None, disable_base_cell_style: false, + column_width_config: ColumnWidthConfig::auto(), } } @@ -626,10 +712,18 @@ impl Table { self } - /// Sets the width of the table. - /// Will enable horizontal scrolling if [`Self::interactable`] is also called. - pub fn width(mut self, width: impl Into) -> Self { - self.width = Some(width.into()); + /// Sets a fixed table width with auto column widths. + /// + /// This is a shorthand for `.width_config(ColumnWidthConfig::auto_with_table_width(width))`. + /// For resizable columns or explicit column widths, use [`Table::width_config`] directly. + pub fn width(mut self, width: impl Into) -> Self { + self.column_width_config = ColumnWidthConfig::auto_with_table_width(width); + self + } + + /// Sets the column width configuration for the table. + pub fn width_config(mut self, config: ColumnWidthConfig) -> Self { + self.column_width_config = config; self } @@ -637,10 +731,8 @@ impl Table { /// /// Vertical scrolling will be enabled by default if the table is taller than its container. /// - /// Horizontal scrolling will only be enabled if [`Self::width`] is also called, otherwise - /// the list will always shrink the table columns to fit their contents I.e. If [`Self::uniform_list`] - /// is used without a width and with [`Self::interactable`], the [`ListHorizontalSizingBehavior`] will - /// be set to [`ListHorizontalSizingBehavior::FitList`]. + /// Horizontal scrolling will only be enabled if a table width is set via [`ColumnWidthConfig`], + /// otherwise the list will always shrink the table columns to fit their contents. pub fn interactable(mut self, interaction_state: &Entity) -> Self { self.interaction_state = Some(interaction_state.downgrade()); self @@ -666,36 +758,6 @@ impl Table { self } - pub fn column_widths(mut self, widths: UncheckedTableRow>) -> Self { - if self.col_widths.is_none() { - self.col_widths = Some(TableWidths::new(widths.into_table_row(self.cols))); - } - self - } - - pub fn resizable_columns( - mut self, - resizable: UncheckedTableRow, - column_widths: &Entity, - cx: &mut App, - ) -> Self { - if let Some(table_widths) = self.col_widths.as_mut() { - table_widths.resizable = resizable.into_table_row(self.cols); - let column_widths = table_widths - .current - .get_or_insert_with(|| column_widths.clone()); - - column_widths.update(cx, |widths, _| { - if !widths.initialized { - widths.initialized = true; - widths.widths = table_widths.initial.clone(); - widths.visible_widths = widths.widths.clone(); - } - }) - } - self - } - pub fn no_ui_font(mut self) -> Self { self.use_ui_font = false; self @@ -812,11 +874,7 @@ pub fn render_table_row( pub fn render_table_header( headers: TableRow, table_context: TableRenderContext, - columns_widths: Option<( - WeakEntity, - TableRow, - TableRow, - )>, + resize_info: Option, entity_id: Option, cx: &mut App, ) -> impl IntoElement { @@ -837,9 +895,7 @@ pub fn render_table_header( .flex() .flex_row() .items_center() - .justify_between() .w_full() - .p_2() .border_b_1() .border_color(cx.theme().colors().border) .children( @@ -850,34 +906,33 @@ pub fn render_table_header( .zip(column_widths.into_vec()) .map(|((header_idx, h), width)| { base_cell_style_text(width, table_context.use_ui_font, cx) + .px_1() + .py_0p5() .child(h) .id(ElementId::NamedInteger( shared_element_id.clone(), header_idx as u64, )) - .when_some( - columns_widths.as_ref().cloned(), - |this, (column_widths, resizables, initial_sizes)| { - if resizables[header_idx].is_resizable() { - this.on_click(move |event, window, cx| { - if event.click_count() > 1 { - column_widths - .update(cx, |column, _| { - column.on_double_click( - header_idx, - &initial_sizes, - &resizables, - window, - ); - }) - .ok(); - } - }) - } else { - this - } - }, - ) + .when_some(resize_info.as_ref().cloned(), |this, info| { + if info.resize_behavior[header_idx].is_resizable() { + this.on_click(move |event, window, cx| { + if event.click_count() > 1 { + info.columns_state + .update(cx, |column, _| { + column.on_double_click( + header_idx, + &info.initial_widths, + &info.resize_behavior, + window, + ); + }) + .ok(); + } + }) + } else { + this + } + }) }), ) } @@ -901,7 +956,7 @@ impl TableRenderContext { show_row_borders: table.show_row_borders, show_row_hover: table.show_row_hover, total_row_count: table.rows.len(), - column_widths: table.col_widths.as_ref().map(|widths| widths.lengths(cx)), + column_widths: table.column_width_config.widths_to_render(cx), map_row: table.map_row.clone(), use_ui_font: table.use_ui_font, disable_base_cell_style: table.disable_base_cell_style, @@ -913,48 +968,52 @@ impl RenderOnce for Table { fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement { let table_context = TableRenderContext::new(&self, cx); let interaction_state = self.interaction_state.and_then(|state| state.upgrade()); - let current_widths = self - .col_widths - .as_ref() - .and_then(|widths| Some((widths.current.as_ref()?, widths.resizable.clone()))) - .map(|(curr, resize_behavior)| (curr.downgrade(), resize_behavior)); - let current_widths_with_initial_sizes = self - .col_widths + let header_resize_info = interaction_state .as_ref() - .and_then(|widths| { - Some(( - widths.current.as_ref()?, - widths.resizable.clone(), - widths.initial.clone(), - )) - }) - .map(|(curr, resize_behavior, initial)| (curr.downgrade(), resize_behavior, initial)); + .and_then(|_| self.column_width_config.header_resize_info(cx)); - let width = self.width; + let table_width = self.column_width_config.table_width(); + let horizontal_sizing = self.column_width_config.list_horizontal_sizing(); let no_rows_rendered = self.rows.is_empty(); + // Extract redistributable entity for drag/drop/prepaint handlers + let redistributable_entity = + interaction_state + .as_ref() + .and_then(|_| match &self.column_width_config { + ColumnWidthConfig::Redistributable { + columns_state: entity, + .. + } => Some(entity.downgrade()), + _ => None, + }); + + let resize_handles = interaction_state + .as_ref() + .and_then(|_| self.column_width_config.render_resize_handles(window, cx)); + let table = div() - .when_some(width, |this, width| this.w(width)) + .when_some(table_width, |this, width| this.w(width)) .h_full() .v_flex() .when_some(self.headers.take(), |this, headers| { this.child(render_table_header( headers, table_context.clone(), - current_widths_with_initial_sizes, + header_resize_info, interaction_state.as_ref().map(Entity::entity_id), cx, )) }) - .when_some(current_widths, { - |this, (widths, resize_behavior)| { + .when_some(redistributable_entity, { + |this, widths| { this.on_drag_move::({ let widths = widths.clone(); move |e, window, cx| { widths .update(cx, |widths, cx| { - widths.on_drag_move(e, &resize_behavior, window, cx); + widths.on_drag_move(e, window, cx); }) .ok(); } @@ -965,7 +1024,7 @@ impl RenderOnce for Table { widths .update(cx, |widths, _| { // This works because all children x axis bounds are the same - widths.cached_bounds_width = + widths.cached_table_width = bounds[0].right() - bounds[0].left(); }) .ok(); @@ -974,10 +1033,9 @@ impl RenderOnce for Table { .on_drop::(move |_, _, cx| { widths .update(cx, |widths, _| { - widths.widths = widths.visible_widths.clone(); + widths.committed_widths = widths.preview_widths.clone(); }) .ok(); - // Finish the resize operation }) } }) @@ -1029,11 +1087,7 @@ impl RenderOnce for Table { .size_full() .flex_grow() .with_sizing_behavior(ListSizingBehavior::Auto) - .with_horizontal_sizing_behavior(if width.is_some() { - ListHorizontalSizingBehavior::Unconstrained - } else { - ListHorizontalSizingBehavior::FitList - }) + .with_horizontal_sizing_behavior(horizontal_sizing) .when_some( interaction_state.as_ref(), |this, state| { @@ -1063,25 +1117,7 @@ impl RenderOnce for Table { .with_sizing_behavior(ListSizingBehavior::Auto), ), }) - .when_some( - self.col_widths.as_ref().zip(interaction_state.as_ref()), - |parent, (table_widths, state)| { - parent.child(state.update(cx, |state, cx| { - let resizable_columns = &table_widths.resizable; - let column_widths = table_widths.lengths(cx); - let columns = table_widths.current.clone(); - let initial_sizes = &table_widths.initial; - state.render_resize_handles( - &column_widths, - resizable_columns, - initial_sizes, - columns, - window, - cx, - ) - })) - }, - ); + .when_some(resize_handles, |parent, handles| parent.child(handles)); if let Some(state) = interaction_state.as_ref() { let scrollbars = state diff --git a/crates/ui/src/components/data_table/tests.rs b/crates/ui/src/components/data_table/tests.rs index f0982a8aa5abe5f5a9351ebaaaf4072ca17839e6..0936cd3088cc50bc08bf0a0a09d9a6fa7a2cdaf0 100644 --- a/crates/ui/src/components/data_table/tests.rs +++ b/crates/ui/src/components/data_table/tests.rs @@ -82,7 +82,7 @@ mod reset_column_size { let cols = initial_sizes.len(); let resize_behavior_vec = parse_resize_behavior(resize_behavior, total_1, cols); let resize_behavior = TableRow::from_vec(resize_behavior_vec, cols); - let result = TableColumnWidths::reset_to_initial_size( + let result = RedistributableColumnsState::reset_to_initial_size( column_index, TableRow::from_vec(widths, cols), TableRow::from_vec(initial_sizes, cols), @@ -259,7 +259,7 @@ mod drag_handle { let distance = distance as f32 / total_1; let mut widths_table_row = TableRow::from_vec(widths, cols); - TableColumnWidths::drag_column_handle( + RedistributableColumnsState::drag_column_handle( distance, column_index, &mut widths_table_row, diff --git a/crates/ui/src/components/list/list_item.rs b/crates/ui/src/components/list/list_item.rs index 693cf3d52e34369d04db445d1ddac765691fb429..9a764efd58cfd3365d92e534a715a0f23ce46e90 100644 --- a/crates/ui/src/components/list/list_item.rs +++ b/crates/ui/src/components/list/list_item.rs @@ -14,6 +14,14 @@ pub enum ListItemSpacing { Sparse, } +#[derive(Default)] +enum EndSlotVisibility { + #[default] + Always, + OnHover, + SwapOnHover(AnyElement), +} + #[derive(IntoElement, RegisterComponent)] pub struct ListItem { id: ElementId, @@ -28,9 +36,7 @@ pub struct ListItem { /// A slot for content that appears after the children, usually on the other side of the header. /// This might be a button, a disclosure arrow, a face pile, etc. end_slot: Option, - /// A slot for content that appears on hover after the children - /// It will obscure the `end_slot` when visible. - end_hover_slot: Option, + end_slot_visibility: EndSlotVisibility, toggle: Option, inset: bool, on_click: Option>, @@ -61,7 +67,7 @@ impl ListItem { indent_step_size: px(12.), start_slot: None, end_slot: None, - end_hover_slot: None, + end_slot_visibility: EndSlotVisibility::default(), toggle: None, inset: false, on_click: None, @@ -165,8 +171,14 @@ impl ListItem { self } - pub fn end_hover_slot(mut self, end_hover_slot: impl Into>) -> Self { - self.end_hover_slot = end_hover_slot.into().map(IntoElement::into_any_element); + pub fn end_slot_on_hover(mut self, end_slot_on_hover: E) -> Self { + self.end_slot_visibility = + EndSlotVisibility::SwapOnHover(end_slot_on_hover.into_any_element()); + self + } + + pub fn show_end_slot_on_hover(mut self) -> Self { + self.end_slot_visibility = EndSlotVisibility::OnHover; self } @@ -234,9 +246,9 @@ impl RenderOnce for ListItem { this.ml(self.indent_level as f32 * self.indent_step_size) .px(DynamicSpacing::Base04.rems(cx)) }) - .when(!self.inset && !self.disabled, |this| { + .when(!self.inset, |this| { this.when_some(self.focused, |this, focused| { - if focused { + if focused && !self.disabled { this.border_1() .when(self.docked_right, |this| this.border_r_2()) .border_color(cx.theme().colors().border_focused) @@ -244,7 +256,7 @@ impl RenderOnce for ListItem { this.border_1() } }) - .when(self.selectable, |this| { + .when(self.selectable && !self.disabled, |this| { this.hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) .active(|style| style.bg(cx.theme().colors().ghost_element_active)) .when(self.outlined, |this| this.rounded_sm()) @@ -268,16 +280,16 @@ impl RenderOnce for ListItem { ListItemSpacing::ExtraDense => this.py_neg_px(), ListItemSpacing::Sparse => this.py_1(), }) - .when(self.inset && !self.disabled, |this| { + .when(self.inset, |this| { this.when_some(self.focused, |this, focused| { - if focused { + if focused && !self.disabled { this.border_1() .border_color(cx.theme().colors().border_focused) } else { this.border_1() } }) - .when(self.selectable, |this| { + .when(self.selectable && !self.disabled, |this| { this.hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) .active(|style| style.bg(cx.theme().colors().ghost_element_active)) .when(self.selected, |this| { @@ -338,28 +350,31 @@ impl RenderOnce for ListItem { .children(self.start_slot) .children(self.children), ) + .when(self.end_slot.is_some(), |this| this.justify_between()) .when_some(self.end_slot, |this, end_slot| { - this.justify_between().child( - h_flex() + this.child(match self.end_slot_visibility { + EndSlotVisibility::Always => { + h_flex().flex_shrink().overflow_hidden().child(end_slot) + } + EndSlotVisibility::OnHover => h_flex() .flex_shrink() .overflow_hidden() - .when(self.end_hover_slot.is_some(), |this| { - this.visible() - .group_hover("list_item", |this| this.invisible()) - }) - .child(end_slot), - ) - }) - .when_some(self.end_hover_slot, |this, end_hover_slot| { - this.child( - h_flex() - .h_full() - .absolute() - .right(DynamicSpacing::Base06.rems(cx)) - .top_0() .visible_on_hover("list_item") - .child(end_hover_slot), - ) + .child(end_slot), + EndSlotVisibility::SwapOnHover(hover_slot) => h_flex() + .relative() + .flex_shrink() + .child(h_flex().visible_on_hover("list_item").child(hover_slot)) + .child( + h_flex() + .absolute() + .inset_0() + .justify_end() + .overflow_hidden() + .group_hover("list_item", |this| this.invisible()) + .child(end_slot), + ), + }) }), ) } diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index c1e766c03a897facb3c7acf76b3ef7811e6910a8..d2c8f4b78dcde8c4f2135b63ee3d07f04e01ebd5 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -648,6 +648,7 @@ impl Vim { self.search = SearchState { direction: searchable::Direction::Next, count: 1, + cmd_f_search: false, prior_selections, prior_operator: self.operator_stack.last().cloned(), prior_mode: self.mode, diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index 248f43c08192182cb266dbfc43a5a769f87429cd..6a8394f44710b7e241b7ba38f4913899a5afbce6 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -284,6 +284,7 @@ impl Vim { self.search = SearchState { direction, count, + cmd_f_search: false, prior_selections, prior_operator: self.operator_stack.last().cloned(), prior_mode, @@ -298,6 +299,7 @@ impl Vim { let current_mode = self.mode; self.search = Default::default(); self.search.prior_mode = current_mode; + self.search.cmd_f_search = true; cx.propagate(); } @@ -957,6 +959,45 @@ mod test { cx.assert_editor_state("«oneˇ» one one one"); } + #[gpui::test] + async fn test_non_vim_search_in_vim_mode(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + cx.cx.set_state("ˇone one one one"); + cx.run_until_parked(); + cx.simulate_keystrokes("cmd-f"); + cx.run_until_parked(); + + cx.assert_state("«oneˇ» one one one", Mode::Visual); + cx.simulate_keystrokes("enter"); + cx.run_until_parked(); + cx.assert_state("one «oneˇ» one one", Mode::Visual); + cx.simulate_keystrokes("shift-enter"); + cx.run_until_parked(); + cx.assert_state("«oneˇ» one one one", Mode::Visual); + + cx.simulate_keystrokes("escape"); + cx.run_until_parked(); + cx.assert_state("«oneˇ» one one one", Mode::Visual); + } + + #[gpui::test] + async fn test_non_vim_search_in_vim_insert_mode(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + cx.set_state("ˇone one one one", Mode::Insert); + cx.run_until_parked(); + cx.simulate_keystrokes("cmd-f"); + cx.run_until_parked(); + + cx.assert_state("«oneˇ» one one one", Mode::Insert); + cx.simulate_keystrokes("enter"); + cx.run_until_parked(); + cx.assert_state("one «oneˇ» one one", Mode::Insert); + + cx.simulate_keystrokes("escape"); + cx.run_until_parked(); + cx.assert_state("one «oneˇ» one one", Mode::Insert); + } + #[gpui::test] async fn test_visual_star_hash(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 2ae4abe33a0fbb4bc6f8a838e60dc0857949e0dc..2fa5382c542999b8d3cb53ea85bed4c99257a3ea 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -1022,6 +1022,7 @@ impl Clone for ReplayableAction { pub struct SearchState { pub direction: Direction, pub count: usize, + pub cmd_f_search: bool, pub prior_selections: Vec>, pub prior_operator: Option, diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 05046899b6164f7c5884e3ad64ad69caaeb2015f..6e1849340f17b776a34546dd9a118dc55e8dab84 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -432,8 +432,12 @@ pub fn init(cx: &mut App) { .and_then(|item| item.act_as::(cx)) .and_then(|editor| editor.read(cx).addon::().cloned()); let Some(vim) = vim else { return }; - vim.entity.update(cx, |_, cx| { - cx.defer_in(window, |vim, window, cx| vim.search_submit(window, cx)) + vim.entity.update(cx, |vim, cx| { + if !vim.search.cmd_f_search { + cx.defer_in(window, |vim, window, cx| vim.search_submit(window, cx)) + } else { + cx.propagate() + } }) }); workspace.register_action(|_, _: &GoToTab, window, cx| { @@ -2086,7 +2090,7 @@ impl Vim { VimEditorSettingsState { cursor_shape: self.cursor_shape(cx), clip_at_line_ends: self.clip_at_line_ends(), - collapse_matches: !HelixModeSetting::get_global(cx).0, + collapse_matches: !HelixModeSetting::get_global(cx).0 && !self.search.cmd_f_search, input_enabled: self.editor_input_enabled(), expects_character_input: self.expects_character_input(), autoindent: self.should_autoindent(), diff --git a/crates/workspace/src/multi_workspace.rs b/crates/workspace/src/multi_workspace.rs index 862f7c7b267721833fa395e501b604d30745a1b7..10a5ce70ead2d5aea7cc21a9af53ee9f216859c3 100644 --- a/crates/workspace/src/multi_workspace.rs +++ b/crates/workspace/src/multi_workspace.rs @@ -1113,178 +1113,3 @@ impl Render for MultiWorkspace { ) } } - -#[cfg(test)] -mod tests { - use super::*; - use fs::FakeFs; - use gpui::TestAppContext; - use settings::SettingsStore; - - fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - theme_settings::init(theme::LoadThemes::JustBase, cx); - DisableAiSettings::register(cx); - cx.update_flags(false, vec!["agent-v2".into()]); - }); - } - - #[gpui::test] - async fn test_sidebar_disabled_when_disable_ai_is_enabled(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [], cx).await; - - let (multi_workspace, cx) = - cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); - - multi_workspace.read_with(cx, |mw, cx| { - assert!(mw.multi_workspace_enabled(cx)); - }); - - multi_workspace.update_in(cx, |mw, _window, cx| { - mw.open_sidebar(cx); - assert!(mw.sidebar_open()); - }); - - cx.update(|_window, cx| { - DisableAiSettings::override_global(DisableAiSettings { disable_ai: true }, cx); - }); - cx.run_until_parked(); - - multi_workspace.read_with(cx, |mw, cx| { - assert!( - !mw.sidebar_open(), - "Sidebar should be closed when disable_ai is true" - ); - assert!( - !mw.multi_workspace_enabled(cx), - "Multi-workspace should be disabled when disable_ai is true" - ); - }); - - multi_workspace.update_in(cx, |mw, window, cx| { - mw.toggle_sidebar(window, cx); - }); - multi_workspace.read_with(cx, |mw, _cx| { - assert!( - !mw.sidebar_open(), - "Sidebar should remain closed when toggled with disable_ai true" - ); - }); - - cx.update(|_window, cx| { - DisableAiSettings::override_global(DisableAiSettings { disable_ai: false }, cx); - }); - cx.run_until_parked(); - - multi_workspace.read_with(cx, |mw, cx| { - assert!( - mw.multi_workspace_enabled(cx), - "Multi-workspace should be enabled after re-enabling AI" - ); - assert!( - !mw.sidebar_open(), - "Sidebar should still be closed after re-enabling AI (not auto-opened)" - ); - }); - - multi_workspace.update_in(cx, |mw, window, cx| { - mw.toggle_sidebar(window, cx); - }); - multi_workspace.read_with(cx, |mw, _cx| { - assert!( - mw.sidebar_open(), - "Sidebar should open when toggled after re-enabling AI" - ); - }); - } - - #[gpui::test] - async fn test_replace(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - let project_a = Project::test(fs.clone(), [], cx).await; - let project_b = Project::test(fs.clone(), [], cx).await; - let project_c = Project::test(fs.clone(), [], cx).await; - let project_d = Project::test(fs.clone(), [], cx).await; - - let (multi_workspace, cx) = cx - .add_window_view(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx)); - - let workspace_a_id = - multi_workspace.read_with(cx, |mw, _cx| mw.workspaces()[0].entity_id()); - - // Replace the only workspace (single-workspace case). - let workspace_b = multi_workspace.update_in(cx, |mw, window, cx| { - let workspace = cx.new(|cx| Workspace::test_new(project_b.clone(), window, cx)); - mw.replace(workspace.clone(), &*window, cx); - workspace - }); - - multi_workspace.read_with(cx, |mw, _cx| { - assert_eq!(mw.workspaces().len(), 1); - assert_eq!( - mw.workspaces()[0].entity_id(), - workspace_b.entity_id(), - "slot should now be project_b" - ); - assert_ne!( - mw.workspaces()[0].entity_id(), - workspace_a_id, - "project_a should be gone" - ); - }); - - // Add project_c as a second workspace, then replace it with project_d. - let workspace_c = multi_workspace.update_in(cx, |mw, window, cx| { - mw.test_add_workspace(project_c.clone(), window, cx) - }); - - multi_workspace.read_with(cx, |mw, _cx| { - assert_eq!(mw.workspaces().len(), 2); - assert_eq!(mw.active_workspace_index(), 1); - }); - - let workspace_d = multi_workspace.update_in(cx, |mw, window, cx| { - let workspace = cx.new(|cx| Workspace::test_new(project_d.clone(), window, cx)); - mw.replace(workspace.clone(), &*window, cx); - workspace - }); - - multi_workspace.read_with(cx, |mw, _cx| { - assert_eq!(mw.workspaces().len(), 2, "should still have 2 workspaces"); - assert_eq!(mw.active_workspace_index(), 1); - assert_eq!( - mw.workspaces()[1].entity_id(), - workspace_d.entity_id(), - "active slot should now be project_d" - ); - assert_ne!( - mw.workspaces()[1].entity_id(), - workspace_c.entity_id(), - "project_c should be gone" - ); - }); - - // Replace with workspace_b which is already in the list — should just switch. - multi_workspace.update_in(cx, |mw, window, cx| { - mw.replace(workspace_b.clone(), &*window, cx); - }); - - multi_workspace.read_with(cx, |mw, _cx| { - assert_eq!( - mw.workspaces().len(), - 2, - "no workspace should be added or removed" - ); - assert_eq!( - mw.active_workspace_index(), - 0, - "should have switched to workspace_b" - ); - }); - } -} diff --git a/crates/workspace/src/multi_workspace_tests.rs b/crates/workspace/src/multi_workspace_tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..50161121719ec7b2835fd11e389f24860e57d8f5 --- /dev/null +++ b/crates/workspace/src/multi_workspace_tests.rs @@ -0,0 +1,172 @@ +use super::*; +use feature_flags::FeatureFlagAppExt; +use fs::FakeFs; +use gpui::TestAppContext; +use project::DisableAiSettings; +use settings::SettingsStore; + +fn init_test(cx: &mut TestAppContext) { + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + theme_settings::init(theme::LoadThemes::JustBase, cx); + DisableAiSettings::register(cx); + cx.update_flags(false, vec!["agent-v2".into()]); + }); +} + +#[gpui::test] +async fn test_sidebar_disabled_when_disable_ai_is_enabled(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + let project = Project::test(fs, [], cx).await; + + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + + multi_workspace.read_with(cx, |mw, cx| { + assert!(mw.multi_workspace_enabled(cx)); + }); + + multi_workspace.update_in(cx, |mw, _window, cx| { + mw.open_sidebar(cx); + assert!(mw.sidebar_open()); + }); + + cx.update(|_window, cx| { + DisableAiSettings::override_global(DisableAiSettings { disable_ai: true }, cx); + }); + cx.run_until_parked(); + + multi_workspace.read_with(cx, |mw, cx| { + assert!( + !mw.sidebar_open(), + "Sidebar should be closed when disable_ai is true" + ); + assert!( + !mw.multi_workspace_enabled(cx), + "Multi-workspace should be disabled when disable_ai is true" + ); + }); + + multi_workspace.update_in(cx, |mw, window, cx| { + mw.toggle_sidebar(window, cx); + }); + multi_workspace.read_with(cx, |mw, _cx| { + assert!( + !mw.sidebar_open(), + "Sidebar should remain closed when toggled with disable_ai true" + ); + }); + + cx.update(|_window, cx| { + DisableAiSettings::override_global(DisableAiSettings { disable_ai: false }, cx); + }); + cx.run_until_parked(); + + multi_workspace.read_with(cx, |mw, cx| { + assert!( + mw.multi_workspace_enabled(cx), + "Multi-workspace should be enabled after re-enabling AI" + ); + assert!( + !mw.sidebar_open(), + "Sidebar should still be closed after re-enabling AI (not auto-opened)" + ); + }); + + multi_workspace.update_in(cx, |mw, window, cx| { + mw.toggle_sidebar(window, cx); + }); + multi_workspace.read_with(cx, |mw, _cx| { + assert!( + mw.sidebar_open(), + "Sidebar should open when toggled after re-enabling AI" + ); + }); +} + +#[gpui::test] +async fn test_replace(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + let project_a = Project::test(fs.clone(), [], cx).await; + let project_b = Project::test(fs.clone(), [], cx).await; + let project_c = Project::test(fs.clone(), [], cx).await; + let project_d = Project::test(fs.clone(), [], cx).await; + + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx)); + + let workspace_a_id = multi_workspace.read_with(cx, |mw, _cx| mw.workspaces()[0].entity_id()); + + // Replace the only workspace (single-workspace case). + let workspace_b = multi_workspace.update_in(cx, |mw, window, cx| { + let workspace = cx.new(|cx| Workspace::test_new(project_b.clone(), window, cx)); + mw.replace(workspace.clone(), &*window, cx); + workspace + }); + + multi_workspace.read_with(cx, |mw, _cx| { + assert_eq!(mw.workspaces().len(), 1); + assert_eq!( + mw.workspaces()[0].entity_id(), + workspace_b.entity_id(), + "slot should now be project_b" + ); + assert_ne!( + mw.workspaces()[0].entity_id(), + workspace_a_id, + "project_a should be gone" + ); + }); + + // Add project_c as a second workspace, then replace it with project_d. + let workspace_c = multi_workspace.update_in(cx, |mw, window, cx| { + mw.test_add_workspace(project_c.clone(), window, cx) + }); + + multi_workspace.read_with(cx, |mw, _cx| { + assert_eq!(mw.workspaces().len(), 2); + assert_eq!(mw.active_workspace_index(), 1); + }); + + let workspace_d = multi_workspace.update_in(cx, |mw, window, cx| { + let workspace = cx.new(|cx| Workspace::test_new(project_d.clone(), window, cx)); + mw.replace(workspace.clone(), &*window, cx); + workspace + }); + + multi_workspace.read_with(cx, |mw, _cx| { + assert_eq!(mw.workspaces().len(), 2, "should still have 2 workspaces"); + assert_eq!(mw.active_workspace_index(), 1); + assert_eq!( + mw.workspaces()[1].entity_id(), + workspace_d.entity_id(), + "active slot should now be project_d" + ); + assert_ne!( + mw.workspaces()[1].entity_id(), + workspace_c.entity_id(), + "project_c should be gone" + ); + }); + + // Replace with workspace_b which is already in the list — should just switch. + multi_workspace.update_in(cx, |mw, window, cx| { + mw.replace(workspace_b.clone(), &*window, cx); + }); + + multi_workspace.read_with(cx, |mw, _cx| { + assert_eq!( + mw.workspaces().len(), + 2, + "no workspace should be added or removed" + ); + assert_eq!( + mw.active_workspace_index(), + 0, + "should have switched to workspace_b" + ); + }); +} diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index dbf2accf3dd9910426ca3557daf9cee0e5b0a82b..b4f683fa6952b9d6f26b8933e010f4c7d2de898c 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -5,7 +5,7 @@ use gpui::{ DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, PromptLevel, Render, ScrollHandle, Task, TextStyleRefinement, UnderlineStyle, WeakEntity, svg, }; -use markdown::{Markdown, MarkdownElement, MarkdownStyle}; +use markdown::{CopyButtonVisibility, Markdown, MarkdownElement, MarkdownStyle}; use parking_lot::Mutex; use project::project_settings::ProjectSettings; use settings::Settings; @@ -401,8 +401,7 @@ impl Render for LanguageServerPrompt { MarkdownElement::new(self.markdown.clone(), markdown_style(window, cx)) .text_size(TextSize::Small.rems(cx)) .code_block_renderer(markdown::CodeBlockRenderer::Default { - copy_button: false, - copy_button_on_hover: false, + copy_button_visibility: CopyButtonVisibility::Hidden, border: false, }) .on_url_click(|link, _, cx| cx.open_url(&link)), diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 33d1befe38e3b48a39377547bd433398b37d6a77..ae05c2c59012b2caf217ac54a80b377aee87f09d 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -5,6 +5,8 @@ pub mod invalid_item_view; pub mod item; mod modal_layer; mod multi_workspace; +#[cfg(test)] +mod multi_workspace_tests; pub mod notifications; pub mod pane; pub mod pane_group; diff --git a/crates/zed/src/zed/telemetry_log.rs b/crates/zed/src/zed/telemetry_log.rs index cc07783f57b27cc57a281089effb208fc3947050..7df7e83d25804edb1a7a73abf055d9adaf080a90 100644 --- a/crates/zed/src/zed/telemetry_log.rs +++ b/crates/zed/src/zed/telemetry_log.rs @@ -12,7 +12,7 @@ use gpui::{ StyleRefinement, Task, TextStyleRefinement, Window, list, prelude::*, }; use language::LanguageRegistry; -use markdown::{CodeBlockRenderer, Markdown, MarkdownElement, MarkdownStyle}; +use markdown::{CodeBlockRenderer, CopyButtonVisibility, Markdown, MarkdownElement, MarkdownStyle}; use project::Project; use settings::Settings; use telemetry_events::{Event, EventWrapper}; @@ -424,8 +424,11 @@ impl TelemetryLogView { }, ) .code_block_renderer(CodeBlockRenderer::Default { - copy_button: false, - copy_button_on_hover: expanded, + copy_button_visibility: if expanded { + CopyButtonVisibility::VisibleOnHover + } else { + CopyButtonVisibility::Hidden + }, border: false, }), ), diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 75b21c528a1e6952700264a154ab4c15045149b0..66ccf9c41c1e1cfcb821e03b4e9b7d4803f53c0b 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -450,8 +450,6 @@ pub mod agent { OpenOnboardingModal, /// Opens the ACP onboarding modal. OpenAcpOnboardingModal, - /// Opens the Claude Agent onboarding modal. - OpenClaudeAgentOnboardingModal, /// Resets the agent onboarding state. ResetOnboarding, /// Starts a chat conversation with the agent.