From f6e396837c9a07d3ba7e83287f287da38b2a8fdb Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 7 Feb 2025 20:19:57 +0100 Subject: [PATCH 01/42] Re-introduce syntax-based context and use new model (#24469) Release Notes: - N/A --------- Co-authored-by: Marshall --- Cargo.lock | 289 +++++++++++++++---------------- Cargo.toml | 2 +- crates/zeta/src/input_excerpt.rs | 238 +++++++++++++++++++++++++ crates/zeta/src/zeta.rs | 281 +++++------------------------- 4 files changed, 426 insertions(+), 384 deletions(-) create mode 100644 crates/zeta/src/input_excerpt.rs diff --git a/Cargo.lock b/Cargo.lock index 3888da0dd0161121ac3ccd1164279575fe8a78de..7817dcee3bb605205cf744f36699cc2eee412673 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -631,7 +631,7 @@ dependencies = [ "smol", "terminal_view", "text", - "toml 0.8.20", + "toml 0.8.19", "ui", "util", "workspace", @@ -1012,9 +1012,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.86" +version = "0.1.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" +checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" dependencies = [ "proc-macro2", "quote", @@ -1067,7 +1067,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a860072022177f903e59730004fb5dc13db9275b79bb2aef7ba8ce831956c233" dependencies = [ - "bytes 1.10.0", + "bytes 1.9.0", "futures-sink", "futures-util", "memchr", @@ -1181,9 +1181,9 @@ dependencies = [ [[package]] name = "aws-config" -version = "1.5.16" +version = "1.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50236e4d60fe8458de90a71c0922c761e41755adf091b1b03de1cef537179915" +checksum = "dc47e70fc35d054c8fcd296d47a61711f043ac80534a10b4f741904f81e73a90" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1197,7 +1197,7 @@ dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", "aws-types", - "bytes 1.10.0", + "bytes 1.9.0", "fastrand 2.3.0", "hex", "http 0.2.12", @@ -1234,9 +1234,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.25.0" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71b2ddd3ada61a305e1d8bb6c005d1eaa7d14d903681edfc400406d523a9b491" +checksum = "54ac4f13dad353b209b34cbec082338202cbc01c8f00336b55c750c13ac91f8f" dependencies = [ "bindgen 0.69.5", "cc", @@ -1248,9 +1248,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.5.5" +version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76dd04d39cc12844c0994f2c9c5a6f5184c22e9188ec1ff723de41910a21dcad" +checksum = "bee7643696e7fdd74c10f9eb42848a87fe469d35eae9c3323f80aa98f350baac" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -1261,7 +1261,7 @@ dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", "aws-types", - "bytes 1.10.0", + "bytes 1.9.0", "fastrand 2.3.0", "http 0.2.12", "http-body 0.4.6", @@ -1274,9 +1274,9 @@ dependencies = [ [[package]] name = "aws-sdk-kinesis" -version = "1.60.0" +version = "1.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b8052335b6ba19b08ba2b363c7505f8ed34074ac23fa14a652ff6a0a02a4c06" +checksum = "7963cf7a0f49ba4f8351044751f4d42c003c4a5f31d9e084f0d0e68b6fb8b8cf" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1287,7 +1287,7 @@ dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", "aws-types", - "bytes 1.10.0", + "bytes 1.9.0", "http 0.2.12", "once_cell", "regex-lite", @@ -1296,9 +1296,9 @@ dependencies = [ [[package]] name = "aws-sdk-s3" -version = "1.73.0" +version = "1.72.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3978e0a211bdc5cddecfd91fb468665a662a27fbdaef39ddf36a2a18fef12cb4" +checksum = "1c7ce6d85596c4bcb3aba8ad5bb134b08e204c8a475c9999c1af9290f80aa8ad" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1313,7 +1313,7 @@ dependencies = [ "aws-smithy-types", "aws-smithy-xml", "aws-types", - "bytes 1.10.0", + "bytes 1.9.0", "fastrand 2.3.0", "hex", "hmac", @@ -1330,9 +1330,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.58.0" +version = "1.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16ff718c9ee45cc1ebd4774a0e086bb80a6ab752b4902edf1c9f56b86ee1f770" +checksum = "c54bab121fe1881a74c338c5f723d1592bf3b53167f80268a1274f404e1acc38" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1343,7 +1343,7 @@ dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", "aws-types", - "bytes 1.10.0", + "bytes 1.9.0", "http 0.2.12", "once_cell", "regex-lite", @@ -1352,9 +1352,9 @@ dependencies = [ [[package]] name = "aws-sdk-ssooidc" -version = "1.59.0" +version = "1.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5183e088715cc135d8d396fdd3bc02f018f0da4c511f53cb8d795b6a31c55809" +checksum = "8c8234fd024f7ac61c4e44ea008029bde934250f371efe7d4a39708397b1080c" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1365,7 +1365,7 @@ dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", "aws-types", - "bytes 1.10.0", + "bytes 1.9.0", "http 0.2.12", "once_cell", "regex-lite", @@ -1374,9 +1374,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.59.0" +version = "1.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9f944ef032717596639cea4a2118a3a457268ef51bbb5fde9637e54c465da00" +checksum = "ba60e1d519d6f23a9df712c04fdeadd7872ac911c84b2f62a8bda92e129b7962" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1397,16 +1397,16 @@ dependencies = [ [[package]] name = "aws-sigv4" -version = "1.2.8" +version = "1.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bc5bbd1e4a2648fd8c5982af03935972c24a2f9846b396de661d351ee3ce837" +checksum = "690118821e46967b3c4501d67d7d52dd75106a9c54cf36cefa1985cedbe94e05" dependencies = [ "aws-credential-types", "aws-smithy-eventstream", "aws-smithy-http", "aws-smithy-runtime-api", "aws-smithy-types", - "bytes 1.10.0", + "bytes 1.9.0", "crypto-bigint 0.5.5", "form_urlencoded", "hex", @@ -1443,7 +1443,7 @@ checksum = "f2f45a1c384d7a393026bc5f5c177105aa9fa68e4749653b985707ac27d77295" dependencies = [ "aws-smithy-http", "aws-smithy-types", - "bytes 1.10.0", + "bytes 1.9.0", "crc32c", "crc32fast", "crc64fast-nvme", @@ -1464,7 +1464,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b18559a41e0c909b77625adf2b8c50de480a8041e5e4a3f5f7d177db70abc5a" dependencies = [ "aws-smithy-types", - "bytes 1.10.0", + "bytes 1.9.0", "crc32fast", ] @@ -1477,7 +1477,7 @@ dependencies = [ "aws-smithy-eventstream", "aws-smithy-runtime-api", "aws-smithy-types", - "bytes 1.10.0", + "bytes 1.9.0", "bytes-utils", "futures-core", "http 0.2.12", @@ -1510,15 +1510,15 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.7.8" +version = "1.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d526a12d9ed61fadefda24abe2e682892ba288c2018bcb38b1b4c111d13f6d92" +checksum = "865f7050bbc7107a6c98a397a9fcd9413690c27fa718446967cf03b2d3ac517e" dependencies = [ "aws-smithy-async", "aws-smithy-http", "aws-smithy-runtime-api", "aws-smithy-types", - "bytes 1.10.0", + "bytes 1.9.0", "fastrand 2.3.0", "h2 0.3.26", "http 0.2.12", @@ -1543,7 +1543,7 @@ checksum = "92165296a47a812b267b4f41032ff8069ab7ff783696d217f0994a0d7ab585cd" dependencies = [ "aws-smithy-async", "aws-smithy-types", - "bytes 1.10.0", + "bytes 1.9.0", "http 0.2.12", "http 1.2.0", "pin-project-lite", @@ -1554,12 +1554,12 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.2.13" +version = "1.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7b8a53819e42f10d0821f56da995e1470b199686a1809168db6ca485665f042" +checksum = "a28f6feb647fb5e0d5b50f0472c19a7db9462b74e2fec01bb0b44eedcc834e97" dependencies = [ "base64-simd", - "bytes 1.10.0", + "bytes 1.9.0", "bytes-utils", "futures-core", "http 0.2.12", @@ -1589,9 +1589,9 @@ dependencies = [ [[package]] name = "aws-types" -version = "1.3.5" +version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbd0a668309ec1f66c0f6bda4840dd6d4796ae26d699ebc266d7cc95c6d040f" +checksum = "b0df5a18c4f951c645300d365fec53a61418bcf4650f604f85fe2a665bfaa0c2" dependencies = [ "aws-credential-types", "aws-smithy-async", @@ -1611,7 +1611,7 @@ dependencies = [ "axum-core", "base64 0.21.7", "bitflags 1.3.2", - "bytes 1.10.0", + "bytes 1.9.0", "futures-util", "headers", "http 0.2.12", @@ -1644,7 +1644,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" dependencies = [ "async-trait", - "bytes 1.10.0", + "bytes 1.9.0", "futures-util", "http 0.2.12", "http-body 0.4.6", @@ -1661,7 +1661,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9a320103719de37b7b4da4c8eb629d4573f6bcfd3dfe80d3208806895ccf81d" dependencies = [ "axum", - "bytes 1.10.0", + "bytes 1.9.0", "futures-util", "http 0.2.12", "mime", @@ -2108,9 +2108,9 @@ dependencies = [ [[package]] name = "bytes" -version = "1.10.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "bytes-utils" @@ -2118,7 +2118,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" dependencies = [ - "bytes 1.10.0", + "bytes 1.9.0", "either", ] @@ -2311,7 +2311,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fbd1fe9db3ebf71b89060adaf7b0504c2d6a425cf061313099547e382c2e472" dependencies = [ "serde", - "toml 0.8.20", + "toml 0.8.19", ] [[package]] @@ -2345,7 +2345,7 @@ dependencies = [ "serde_json", "syn 2.0.90", "tempfile", - "toml 0.8.20", + "toml 0.8.19", ] [[package]] @@ -2363,7 +2363,7 @@ dependencies = [ "serde_json", "syn 2.0.90", "tempfile", - "toml 0.8.20", + "toml 0.8.19", ] [[package]] @@ -2515,9 +2515,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.28" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e77c3243bd94243c03672cb5154667347c457ca271254724f9f393aee1c05ff" +checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" dependencies = [ "clap_builder", "clap_derive", @@ -2525,9 +2525,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.27" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" +checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" dependencies = [ "anstream", "anstyle", @@ -2547,9 +2547,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.28" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -2818,7 +2818,7 @@ dependencies = [ "thiserror 1.0.69", "time", "tokio", - "toml 0.8.20", + "toml 0.8.19", "tower", "tower-http 0.4.4", "tracing", @@ -2879,7 +2879,7 @@ name = "collections" version = "0.1.0" dependencies = [ "indexmap", - "rustc-hash 2.1.1", + "rustc-hash 2.1.0", ] [[package]] @@ -2900,7 +2900,7 @@ version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ - "bytes 1.10.0", + "bytes 1.9.0", "memchr", ] @@ -3776,9 +3776,9 @@ dependencies = [ [[package]] name = "derive_more" -version = "0.99.19" +version = "0.99.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3da29a38df43d6f156149c9b43ded5e018ddff2a855cf2cfd62e8cd7d079c69f" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" dependencies = [ "convert_case 0.4.0", "proc-macro2", @@ -4132,7 +4132,7 @@ dependencies = [ "cc", "memchr", "rustc_version", - "toml 0.8.20", + "toml 0.8.19", "vswhom", "winreg 0.52.0", ] @@ -4423,7 +4423,7 @@ dependencies = [ "semantic_version", "serde", "serde_json", - "toml 0.8.20", + "toml 0.8.19", "util", "wasm-encoder 0.215.0", "wasmparser 0.215.0", @@ -4447,7 +4447,7 @@ dependencies = [ "serde_json", "theme", "tokio", - "toml 0.8.20", + "toml 0.8.19", "tree-sitter", "wasmtime", ] @@ -4492,7 +4492,7 @@ dependencies = [ "tempfile", "theme", "theme_extension", - "toml 0.8.20", + "toml 0.8.19", "url", "util", "wasmparser 0.215.0", @@ -5595,7 +5595,7 @@ version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ - "bytes 1.10.0", + "bytes 1.9.0", "fnv", "futures-core", "futures-sink", @@ -5615,7 +5615,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" dependencies = [ "atomic-waker", - "bytes 1.10.0", + "bytes 1.9.0", "fnv", "futures-core", "futures-sink", @@ -5731,7 +5731,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" dependencies = [ "base64 0.21.7", - "bytes 1.10.0", + "bytes 1.9.0", "headers-core", "http 0.2.12", "httpdate", @@ -5910,7 +5910,7 @@ version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ - "bytes 1.10.0", + "bytes 1.9.0", "fnv", "itoa", ] @@ -5921,7 +5921,7 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" dependencies = [ - "bytes 1.10.0", + "bytes 1.9.0", "fnv", "itoa", ] @@ -5932,7 +5932,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ - "bytes 1.10.0", + "bytes 1.9.0", "http 0.2.12", "pin-project-lite", ] @@ -5943,7 +5943,7 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ - "bytes 1.10.0", + "bytes 1.9.0", "http 1.2.0", ] @@ -5953,7 +5953,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ - "bytes 1.10.0", + "bytes 1.9.0", "futures-util", "http 1.2.0", "http-body 1.0.1", @@ -5992,7 +5992,7 @@ name = "http_client" version = "0.1.0" dependencies = [ "anyhow", - "bytes 1.10.0", + "bytes 1.9.0", "derive_more", "futures 0.3.31", "http 1.2.0", @@ -6032,7 +6032,7 @@ version = "0.14.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" dependencies = [ - "bytes 1.10.0", + "bytes 1.9.0", "futures-channel", "futures-core", "futures-util", @@ -6056,7 +6056,7 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" dependencies = [ - "bytes 1.10.0", + "bytes 1.9.0", "futures-channel", "futures-util", "h2 0.4.7", @@ -6110,7 +6110,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ - "bytes 1.10.0", + "bytes 1.9.0", "hyper 0.14.32", "native-tls", "tokio", @@ -6123,7 +6123,7 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" dependencies = [ - "bytes 1.10.0", + "bytes 1.9.0", "futures-channel", "futures-util", "http 1.2.0", @@ -6778,7 +6778,7 @@ checksum = "c9ae6296f9476658b3550293c113996daf75fa542cd8d078abb4c60207bded14" dependencies = [ "anyhow", "async-trait", - "bytes 1.10.0", + "bytes 1.9.0", "chrono", "futures 0.3.31", "serde", @@ -7089,7 +7089,7 @@ dependencies = [ "task", "text", "theme", - "toml 0.8.20", + "toml 0.8.19", "tree-sitter", "tree-sitter-bash", "tree-sitter-c", @@ -7184,7 +7184,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -9137,7 +9137,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18f596653ba4ac51bdecbb4ef6773bc7f56042dc13927910de1684ad3d32aa12" dependencies = [ - "bytes 1.10.0", + "bytes 1.9.0", "chrono", "pbjson", "pbjson-build", @@ -9481,7 +9481,7 @@ dependencies = [ "serde", "serde_json", "sha2", - "toml 0.8.20", + "toml 0.8.19", ] [[package]] @@ -10092,7 +10092,7 @@ dependencies = [ "tempfile", "terminal", "text", - "toml 0.8.20", + "toml 0.8.19", "unindent", "url", "util", @@ -10210,7 +10210,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" dependencies = [ - "bytes 1.10.0", + "bytes 1.9.0", "prost-derive 0.9.0", ] @@ -10220,7 +10220,7 @@ version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" dependencies = [ - "bytes 1.10.0", + "bytes 1.9.0", "prost-derive 0.12.6", ] @@ -10230,7 +10230,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62941722fb675d463659e49c4f3fe1fe792ff24fe5bbaa9c08cd3b98a1c354f5" dependencies = [ - "bytes 1.10.0", + "bytes 1.9.0", "heck 0.3.3", "itertools 0.10.5", "lazy_static", @@ -10250,7 +10250,7 @@ version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" dependencies = [ - "bytes 1.10.0", + "bytes 1.9.0", "heck 0.5.0", "itertools 0.12.1", "log", @@ -10297,7 +10297,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "534b7a0e836e3c482d2693070f982e39e7611da9695d4d1f5a4b186b51faef0a" dependencies = [ - "bytes 1.10.0", + "bytes 1.9.0", "prost 0.9.0", ] @@ -10411,9 +10411,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.37.2" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003" +checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" dependencies = [ "memchr", ] @@ -10424,11 +10424,11 @@ version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" dependencies = [ - "bytes 1.10.0", + "bytes 1.9.0", "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.1", + "rustc-hash 2.1.0", "rustls 0.23.22", "socket2", "thiserror 2.0.6", @@ -10442,11 +10442,11 @@ version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" dependencies = [ - "bytes 1.10.0", + "bytes 1.9.0", "getrandom 0.2.15", "rand 0.8.5", "ring", - "rustc-hash 2.1.1", + "rustc-hash 2.1.0", "rustls 0.23.22", "rustls-pki-types", "slab", @@ -10874,7 +10874,7 @@ dependencies = [ "smol", "sysinfo", "telemetry_events", - "toml 0.8.20", + "toml 0.8.19", "unindent", "util", "worktree", @@ -10947,7 +10947,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ "base64 0.21.7", - "bytes 1.10.0", + "bytes 1.9.0", "encoding_rs", "futures-core", "futures-util", @@ -10990,7 +10990,7 @@ version = "0.12.8" source = "git+https://github.com/zed-industries/reqwest.git?rev=fd110f6998da16bbca97b6dddda9be7827c50e29#fd110f6998da16bbca97b6dddda9be7827c50e29" dependencies = [ "base64 0.22.1", - "bytes 1.10.0", + "bytes 1.9.0", "encoding_rs", "futures-core", "futures-util", @@ -11036,7 +11036,7 @@ name = "reqwest_client" version = "0.1.0" dependencies = [ "anyhow", - "bytes 1.10.0", + "bytes 1.9.0", "futures 0.3.31", "gpui", "http_client", @@ -11118,7 +11118,7 @@ checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" dependencies = [ "bitvec", "bytecheck", - "bytes 1.10.0", + "bytes 1.9.0", "hashbrown 0.12.3", "ptr_meta", "rend", @@ -11249,7 +11249,7 @@ dependencies = [ "async-dispatcher", "async-std", "base64 0.22.1", - "bytes 1.10.0", + "bytes 1.9.0", "chrono", "data-encoding", "dirs 5.0.1", @@ -11308,7 +11308,7 @@ checksum = "b082d80e3e3cc52b2ed634388d436fe1f4de6af5786cc2de9ba9737527bdf555" dependencies = [ "arrayvec", "borsh", - "bytes 1.10.0", + "bytes 1.9.0", "num-traits", "rand 0.8.5", "rkyv", @@ -11330,9 +11330,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" [[package]] name = "rustc_version" @@ -12446,7 +12446,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a007b6936676aa9ab40207cde35daab0a04b823be8ae004368c0793b96a61e0" dependencies = [ "bigdecimal", - "bytes 1.10.0", + "bytes 1.9.0", "chrono", "crc", "crossbeam-queue", @@ -12530,7 +12530,7 @@ dependencies = [ "bigdecimal", "bitflags 2.8.0", "byteorder", - "bytes 1.10.0", + "bytes 1.9.0", "chrono", "crc", "digest", @@ -13065,7 +13065,7 @@ dependencies = [ "cfg-expr", "heck 0.5.0", "pkg-config", - "toml 0.8.20", + "toml 0.8.19", "version-compare", ] @@ -13650,7 +13650,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" dependencies = [ "backtrace", - "bytes 1.10.0", + "bytes 1.9.0", "libc", "mio 1.0.3", "parking_lot", @@ -13770,7 +13770,7 @@ version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" dependencies = [ - "bytes 1.10.0", + "bytes 1.9.0", "futures-core", "futures-io", "futures-sink", @@ -13789,9 +13789,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.20" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ "serde", "serde_spanned", @@ -13810,15 +13810,15 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.23" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", - "winnow 0.7.1", + "winnow", ] [[package]] @@ -13865,7 +13865,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f873044bf02dd1e8239e9c1293ea39dad76dc594ec16185d0a1bf31d8dc8d858" dependencies = [ "bitflags 1.3.2", - "bytes 1.10.0", + "bytes 1.9.0", "futures-core", "futures-util", "http 0.2.12", @@ -13883,7 +13883,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" dependencies = [ "bitflags 2.8.0", - "bytes 1.10.0", + "bytes 1.9.0", "futures-core", "futures-util", "http 0.2.12", @@ -14234,7 +14234,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" dependencies = [ "byteorder", - "bytes 1.10.0", + "bytes 1.9.0", "data-encoding", "http 0.2.12", "httparse", @@ -14254,7 +14254,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" dependencies = [ "byteorder", - "bytes 1.10.0", + "bytes 1.9.0", "data-encoding", "http 1.2.0", "httparse", @@ -14273,7 +14273,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" dependencies = [ "byteorder", - "bytes 1.10.0", + "bytes 1.9.0", "data-encoding", "http 1.2.0", "httparse", @@ -14775,7 +14775,7 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4378d202ff965b011c64817db11d5829506d3404edeadb61f190d111da3f231c" dependencies = [ - "bytes 1.10.0", + "bytes 1.9.0", "futures-channel", "futures-util", "headers", @@ -15192,7 +15192,7 @@ dependencies = [ "anyhow", "async-trait", "bitflags 2.8.0", - "bytes 1.10.0", + "bytes 1.9.0", "cap-fs-ext", "cap-net-ext", "cap-rand", @@ -15254,9 +15254,9 @@ dependencies = [ [[package]] name = "wayland-backend" -version = "0.3.8" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7208998eaa3870dad37ec8836979581506e0c5c64c20c9e79e9d2a10d6f47bf" +checksum = "056535ced7a150d45159d3a8dc30f91a2e2d588ca0b23f70e56033622b8016f6" dependencies = [ "cc", "downcast-rs", @@ -15268,9 +15268,9 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.8" +version = "0.31.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2120de3d33638aaef5b9f4472bff75f07c56379cf76ea320bd3a3d65ecaf73f" +checksum = "b66249d3fc69f76fd74c82cc319300faa554e9d865dab1f7cd66cc20db10b280" dependencies = [ "bitflags 2.8.0", "rustix", @@ -15280,9 +15280,9 @@ dependencies = [ [[package]] name = "wayland-cursor" -version = "0.31.8" +version = "0.31.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a93029cbb6650748881a00e4922b076092a6a08c11e7fbdb923f064b23968c5d" +checksum = "32b08bc3aafdb0035e7fe0fdf17ba0c09c268732707dca4ae098f60cb28c9e4c" dependencies = [ "rustix", "wayland-client", @@ -15316,20 +15316,20 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.6" +version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" +checksum = "597f2001b2e5fc1121e3d5b9791d3e78f05ba6bfa4641053846248e3a13661c3" dependencies = [ "proc-macro2", - "quick-xml 0.37.2", + "quick-xml 0.36.2", "quote", ] [[package]] name = "wayland-sys" -version = "0.31.6" +version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615" +checksum = "efa8ac0d8e8ed3e3b5c9fc92c7881406a268e11555abe36493efabe649a29e09" dependencies = [ "dlib", "log", @@ -15927,15 +15927,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winnow" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86e376c75f4f43f44db463cf729e0d3acbf954d13e22c51e26e4c264b4ab545f" -dependencies = [ - "memchr", -] - [[package]] name = "winreg" version = "0.50.0" @@ -15962,7 +15953,7 @@ version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7276691b353ad4547af8c3268488d1311f4be791ffdc0c65b8cfa8f41eed693b" dependencies = [ - "toml 0.8.20", + "toml 0.8.19", "version_check", ] @@ -16481,7 +16472,7 @@ dependencies = [ "tracing", "uds_windows", "windows-sys 0.59.0", - "winnow 0.6.20", + "winnow", "xdg-home", "zbus_macros 5.1.1", "zbus_names 4.1.0", @@ -16535,7 +16526,7 @@ checksum = "856b7a38811f71846fd47856ceee8bccaec8399ff53fb370247e66081ace647b" dependencies = [ "serde", "static_assertions", - "winnow 0.6.20", + "winnow", "zvariant 5.1.0", ] @@ -16766,9 +16757,9 @@ dependencies = [ [[package]] name = "zed_llm_client" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea4d8ead1e1158e5ebdd6735df25973781da70de5c8008e3a13595865ca4f31" +checksum = "656118e6b072924d28815cb892278f12c2548117794e733bd2c075ef4a0427e8" dependencies = [ "serde", "serde_json", @@ -16930,7 +16921,7 @@ dependencies = [ "async-std", "async-trait", "asynchronous-codec", - "bytes 1.10.0", + "bytes 1.9.0", "crossbeam-queue", "dashmap 5.5.3", "futures 0.3.31", @@ -17114,7 +17105,7 @@ dependencies = [ "serde", "static_assertions", "url", - "winnow 0.6.20", + "winnow", "zvariant_derive 5.1.0", "zvariant_utils 3.0.2", ] @@ -17167,5 +17158,5 @@ dependencies = [ "serde", "static_assertions", "syn 2.0.90", - "winnow 0.6.20", + "winnow", ] diff --git a/Cargo.toml b/Cargo.toml index 217cdd9d1f27072e1188de71b7063ddbb9aaa176..b203bd0af3f14f6ad2b096251be2e4a34376a6af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -561,7 +561,7 @@ wasmtime = { version = "24", default-features = false, features = [ wasmtime-wasi = "24" which = "6.0.0" wit-component = "0.201" -zed_llm_client = "0.2" +zed_llm_client = "0.3" zstd = "0.11" metal = "0.31" diff --git a/crates/zeta/src/input_excerpt.rs b/crates/zeta/src/input_excerpt.rs new file mode 100644 index 0000000000000000000000000000000000000000..7171facaffdad66acd4438c23e684ea51eb306bb --- /dev/null +++ b/crates/zeta/src/input_excerpt.rs @@ -0,0 +1,238 @@ +use crate::{ + tokens_for_bytes, CURSOR_MARKER, EDITABLE_REGION_END_MARKER, EDITABLE_REGION_START_MARKER, + START_OF_FILE_MARKER, +}; +use language::{BufferSnapshot, Point}; +use std::{fmt::Write, ops::Range}; + +#[derive(Debug)] +pub struct InputExcerpt { + pub editable_range: Range, + pub prompt: String, + pub speculated_output: String, +} + +pub fn excerpt_for_cursor_position( + position: Point, + path: &str, + snapshot: &BufferSnapshot, + editable_region_token_limit: usize, + context_token_limit: usize, +) -> InputExcerpt { + let mut scope_range = position..position; + let mut remaining_edit_tokens = editable_region_token_limit; + + while let Some(parent) = snapshot.syntax_ancestor(scope_range.clone()) { + let parent_tokens = tokens_for_bytes(parent.byte_range().len()); + let parent_point_range = Point::new( + parent.start_position().row as u32, + parent.start_position().column as u32, + ) + ..Point::new( + parent.end_position().row as u32, + parent.end_position().column as u32, + ); + if parent_point_range == scope_range { + break; + } else if parent_tokens <= editable_region_token_limit { + scope_range = parent_point_range; + remaining_edit_tokens = editable_region_token_limit - parent_tokens; + } else { + break; + } + } + + let editable_range = expand_range(snapshot, scope_range, remaining_edit_tokens); + let context_range = expand_range(snapshot, editable_range.clone(), context_token_limit); + + let mut prompt = String::new(); + let mut speculated_output = String::new(); + + writeln!(&mut prompt, "```{path}").unwrap(); + if context_range.start == Point::zero() { + writeln!(&mut prompt, "{START_OF_FILE_MARKER}").unwrap(); + } + + for chunk in snapshot.chunks(context_range.start..editable_range.start, false) { + prompt.push_str(chunk.text); + } + + push_editable_range(position, snapshot, editable_range.clone(), &mut prompt); + push_editable_range( + position, + snapshot, + editable_range.clone(), + &mut speculated_output, + ); + + for chunk in snapshot.chunks(editable_range.end..context_range.end, false) { + prompt.push_str(chunk.text); + } + write!(prompt, "\n```").unwrap(); + + InputExcerpt { + editable_range, + prompt, + speculated_output, + } +} + +fn push_editable_range( + cursor_position: Point, + snapshot: &BufferSnapshot, + editable_range: Range, + prompt: &mut String, +) { + writeln!(prompt, "{EDITABLE_REGION_START_MARKER}").unwrap(); + for chunk in snapshot.chunks(editable_range.start..cursor_position, false) { + prompt.push_str(chunk.text); + } + prompt.push_str(CURSOR_MARKER); + for chunk in snapshot.chunks(cursor_position..editable_range.end, false) { + prompt.push_str(chunk.text); + } + write!(prompt, "\n{EDITABLE_REGION_END_MARKER}").unwrap(); +} + +fn expand_range( + snapshot: &BufferSnapshot, + range: Range, + mut remaining_tokens: usize, +) -> Range { + let mut expanded_range = range.clone(); + expanded_range.start.column = 0; + expanded_range.end.column = snapshot.line_len(expanded_range.end.row); + loop { + let mut expanded = false; + + if remaining_tokens > 0 && expanded_range.start.row > 0 { + expanded_range.start.row -= 1; + let line_tokens = + tokens_for_bytes(snapshot.line_len(expanded_range.start.row) as usize); + remaining_tokens = remaining_tokens.saturating_sub(line_tokens); + expanded = true; + } + + if remaining_tokens > 0 && expanded_range.end.row < snapshot.max_point().row { + expanded_range.end.row += 1; + expanded_range.end.column = snapshot.line_len(expanded_range.end.row); + let line_tokens = tokens_for_bytes(expanded_range.end.column as usize); + remaining_tokens = remaining_tokens.saturating_sub(line_tokens); + expanded = true; + } + + if !expanded { + break; + } + } + expanded_range +} + +#[cfg(test)] +mod tests { + use super::*; + use gpui::{App, AppContext}; + use indoc::indoc; + use language::{Buffer, Language, LanguageConfig, LanguageMatcher}; + use std::sync::Arc; + + #[gpui::test] + fn test_excerpt_for_cursor_position(cx: &mut App) { + let text = indoc! {r#" + fn foo() { + let x = 42; + println!("Hello, world!"); + } + + fn bar() { + let x = 42; + let mut sum = 0; + for i in 0..x { + sum += i; + } + println!("Sum: {}", sum); + return sum; + } + + fn generate_random_numbers() -> Vec { + let mut rng = rand::thread_rng(); + let mut numbers = Vec::new(); + for _ in 0..5 { + numbers.push(rng.gen_range(1..101)); + } + numbers + } + "#}; + let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx)); + let snapshot = buffer.read(cx).snapshot(); + + // Ensure we try to fit the largest possible syntax scope, resorting to line-based expansion + // when a larger scope doesn't fit the editable region. + let excerpt = excerpt_for_cursor_position(Point::new(12, 5), "main.rs", &snapshot, 50, 32); + assert_eq!( + excerpt.prompt, + indoc! {r#" + ```main.rs + let x = 42; + println!("Hello, world!"); + <|editable_region_start|> + } + + fn bar() { + let x = 42; + let mut sum = 0; + for i in 0..x { + sum += i; + } + println!("Sum: {}", sum); + r<|user_cursor_is_here|>eturn sum; + } + + fn generate_random_numbers() -> Vec { + <|editable_region_end|> + let mut rng = rand::thread_rng(); + let mut numbers = Vec::new(); + ```"#} + ); + + // The `bar` function won't fit within the editable region, so we resort to line-based expansion. + let excerpt = excerpt_for_cursor_position(Point::new(12, 5), "main.rs", &snapshot, 40, 32); + assert_eq!( + excerpt.prompt, + indoc! {r#" + ```main.rs + fn bar() { + let x = 42; + let mut sum = 0; + <|editable_region_start|> + for i in 0..x { + sum += i; + } + println!("Sum: {}", sum); + r<|user_cursor_is_here|>eturn sum; + } + + fn generate_random_numbers() -> Vec { + let mut rng = rand::thread_rng(); + <|editable_region_end|> + let mut numbers = Vec::new(); + for _ in 0..5 { + numbers.push(rng.gen_range(1..101)); + ```"#} + ); + } + + fn rust_lang() -> Language { + Language::new( + LanguageConfig { + name: "Rust".into(), + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + ..Default::default() + }, + Some(tree_sitter_rust::LANGUAGE.into()), + ) + } +} diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 7741e52f3102be22d6452c21835e215e9d037a60..2dd6b7cbf29826b7cc654a98dd38986b4cca1081 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -1,5 +1,6 @@ mod completion_diff_element; mod init; +mod input_excerpt; mod license_detection; mod onboarding_banner; mod onboarding_modal; @@ -25,9 +26,8 @@ use gpui::{ actions, App, AppContext as _, AsyncApp, Context, Entity, EntityId, Global, Subscription, Task, }; use http_client::{HttpClient, Method}; -use language::{ - Anchor, Buffer, BufferSnapshot, EditPreview, OffsetRangeExt, Point, ToOffset, ToPoint, -}; +use input_excerpt::excerpt_for_cursor_position; +use language::{Anchor, Buffer, BufferSnapshot, EditPreview, OffsetRangeExt, ToOffset, ToPoint}; use language_models::LlmApiToken; use postage::watch; use project::Project; @@ -57,38 +57,13 @@ const EDITABLE_REGION_END_MARKER: &'static str = "<|editable_region_end|>"; const BUFFER_CHANGE_GROUPING_INTERVAL: Duration = Duration::from_secs(1); const ZED_PREDICT_DATA_COLLECTION_CHOICE: &str = "zed_predict_data_collection_choice"; -// TODO(mgsloan): more systematic way to choose or tune these fairly arbitrary constants? - -/// Typical number of string bytes per token for the purposes of limiting model input. This is -/// intentionally low to err on the side of underestimating limits. -const BYTES_PER_TOKEN_GUESS: usize = 3; - -/// Output token limit, used to inform the size of the input. A copy of this constant is also in -/// `crates/collab/src/llm.rs`. -const MAX_OUTPUT_TOKENS: usize = 2048; - -/// Total bytes limit for editable region of buffer excerpt. -/// -/// The number of output tokens is relevant to the size of the input excerpt because the model is -/// tasked with outputting a modified excerpt. `2/3` is chosen so that there are some output tokens -/// remaining for the model to specify insertions. -const BUFFER_EXCERPT_BYTE_LIMIT: usize = (MAX_OUTPUT_TOKENS * 2 / 3) * BYTES_PER_TOKEN_GUESS; +const MAX_CONTEXT_TOKENS: usize = 100; +const MAX_REWRITE_TOKENS: usize = 300; +const MAX_EVENT_TOKENS: usize = 400; -/// Total line limit for editable region of buffer excerpt. -const BUFFER_EXCERPT_LINE_LIMIT: u32 = 64; - -/// Note that this is not the limit for the overall prompt, just for the inputs to the template -/// instantiated in `crates/collab/src/llm.rs`. -const TOTAL_BYTE_LIMIT: usize = BUFFER_EXCERPT_BYTE_LIMIT * 2; - -/// Maximum number of events to include in the prompt. +/// Maximum number of events to track. const MAX_EVENT_COUNT: usize = 16; -/// Maximum number of string bytes in a single event. Arbitrarily choosing this to be 4x the size of -/// equally splitting up the the remaining bytes after the largest possible buffer excerpt. -const PER_EVENT_BYTE_LIMIT: usize = - (TOTAL_BYTE_LIMIT - BUFFER_EXCERPT_BYTE_LIMIT) / MAX_EVENT_COUNT * 4; - actions!(edit_prediction, [ClearHistory]); #[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)] @@ -418,7 +393,8 @@ impl Zeta { struct BackgroundValues { input_events: String, input_excerpt: String, - excerpt_range: Range, + speculated_output: String, + editable_range: Range, input_outline: String, } @@ -429,32 +405,21 @@ impl Zeta { let path = path.clone(); async move { let path = path.to_string_lossy(); - let (excerpt_range, excerpt_len_guess) = excerpt_range_for_position( + let input_excerpt = excerpt_for_cursor_position( cursor_point, - BUFFER_EXCERPT_BYTE_LIMIT, - BUFFER_EXCERPT_LINE_LIMIT, - &path, - &snapshot, - )?; - let input_excerpt = prompt_for_excerpt( - cursor_offset, - &excerpt_range, - excerpt_len_guess, &path, &snapshot, + MAX_REWRITE_TOKENS, + MAX_CONTEXT_TOKENS, ); - - let bytes_remaining = TOTAL_BYTE_LIMIT.saturating_sub(input_excerpt.len()); - let input_events = prompt_for_events(events.iter(), bytes_remaining); - - // Note that input_outline is not currently used in prompt generation and so - // is not counted towards TOTAL_BYTE_LIMIT. + let input_events = prompt_for_events(&events, MAX_EVENT_TOKENS); let input_outline = prompt_for_outline(&snapshot); anyhow::Ok(BackgroundValues { input_events, - input_excerpt, - excerpt_range, + input_excerpt: input_excerpt.prompt, + speculated_output: input_excerpt.speculated_output, + editable_range: input_excerpt.editable_range.to_offset(&snapshot), input_outline, }) } @@ -462,7 +427,7 @@ impl Zeta { .await?; log::debug!( - "Events:\n{}\nExcerpt:\n{}", + "Events:\n{}\nExcerpt:\n{:?}", values.input_events, values.input_excerpt ); @@ -470,6 +435,7 @@ impl Zeta { let body = PredictEditsBody { input_events: values.input_events.clone(), input_excerpt: values.input_excerpt.clone(), + speculated_output: Some(values.speculated_output), outline: Some(values.input_outline.clone()), can_collect_data, diagnostic_groups: diagnostic_groups.and_then(|diagnostic_groups| { @@ -492,7 +458,7 @@ impl Zeta { output_excerpt, buffer, &snapshot, - values.excerpt_range, + values.editable_range, cursor_offset, path, values.input_outline, @@ -508,6 +474,8 @@ impl Zeta { // Generates several example completions of various states to fill the Zeta completion modal #[cfg(any(test, feature = "test-support"))] pub fn fill_with_fake_completions(&mut self, cx: &mut Context) -> Task<()> { + use language::Point; + let test_buffer_text = indoc::indoc! {r#"a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line And maybe a short line @@ -697,7 +665,7 @@ and then another loop { let request_builder = http_client::Request::builder().method(Method::POST).uri( http_client - .build_zed_llm_url("/predict_edits", &[])? + .build_zed_llm_url("/predict_edits/v2", &[])? .as_ref(), ); let request = request_builder @@ -737,7 +705,7 @@ and then another output_excerpt: String, buffer: Entity, snapshot: &BufferSnapshot, - excerpt_range: Range, + editable_range: Range, cursor_offset: usize, path: Arc, input_outline: String, @@ -754,9 +722,9 @@ and then another .background_executor() .spawn({ let output_excerpt = output_excerpt.clone(); - let excerpt_range = excerpt_range.clone(); + let editable_range = editable_range.clone(); let snapshot = snapshot.clone(); - async move { Self::parse_edits(output_excerpt, excerpt_range, &snapshot) } + async move { Self::parse_edits(output_excerpt, editable_range, &snapshot) } }) .await? .into(); @@ -779,7 +747,7 @@ and then another Ok(Some(InlineCompletion { id: InlineCompletionId::new(), path, - excerpt_range, + excerpt_range: editable_range, cursor_offset, edits, edit_preview, @@ -796,7 +764,7 @@ and then another fn parse_edits( output_excerpt: Arc, - excerpt_range: Range, + editable_range: Range, snapshot: &BufferSnapshot, ) -> Result, String)>> { let content = output_excerpt.replace(CURSOR_MARKER, ""); @@ -840,13 +808,13 @@ and then another let new_text = &content[..codefence_end]; let old_text = snapshot - .text_for_range(excerpt_range.clone()) + .text_for_range(editable_range.clone()) .collect::(); Ok(Self::compute_edits( old_text, new_text, - excerpt_range.start, + editable_range.start, &snapshot, )) } @@ -1080,9 +1048,7 @@ fn prompt_for_outline(snapshot: &BufferSnapshot) -> String { .unwrap(); if let Some(outline) = snapshot.outline(None) { - let guess_size = outline.items.len() * 15; - input_outline.reserve(guess_size); - for item in outline.items.iter() { + for item in &outline.items { let spacing = " ".repeat(item.depth); writeln!(input_outline, "{}{}", spacing, item.text).unwrap(); } @@ -1093,181 +1059,20 @@ fn prompt_for_outline(snapshot: &BufferSnapshot) -> String { input_outline } -fn prompt_for_excerpt( - offset: usize, - excerpt_range: &Range, - mut len_guess: usize, - path: &str, - snapshot: &BufferSnapshot, -) -> String { - let point_range = excerpt_range.to_point(snapshot); - - // Include one line of extra context before and after editable range, if those lines are non-empty. - let extra_context_before_range = - if point_range.start.row > 0 && !snapshot.is_line_blank(point_range.start.row - 1) { - let range = - (Point::new(point_range.start.row - 1, 0)..point_range.start).to_offset(snapshot); - len_guess += range.end - range.start; - Some(range) - } else { - None - }; - let extra_context_after_range = if point_range.end.row < snapshot.max_point().row - && !snapshot.is_line_blank(point_range.end.row + 1) - { - let range = (point_range.end - ..Point::new( - point_range.end.row + 1, - snapshot.line_len(point_range.end.row + 1), - )) - .to_offset(snapshot); - len_guess += range.end - range.start; - Some(range) - } else { - None - }; - - let mut prompt_excerpt = String::with_capacity(len_guess); - writeln!(prompt_excerpt, "```{}", path).unwrap(); - - if excerpt_range.start == 0 { - writeln!(prompt_excerpt, "{START_OF_FILE_MARKER}").unwrap(); - } - - if let Some(extra_context_before_range) = extra_context_before_range { - for chunk in snapshot.text_for_range(extra_context_before_range) { - prompt_excerpt.push_str(chunk); - } - } - writeln!(prompt_excerpt, "{EDITABLE_REGION_START_MARKER}").unwrap(); - for chunk in snapshot.text_for_range(excerpt_range.start..offset) { - prompt_excerpt.push_str(chunk); - } - prompt_excerpt.push_str(CURSOR_MARKER); - for chunk in snapshot.text_for_range(offset..excerpt_range.end) { - prompt_excerpt.push_str(chunk); - } - write!(prompt_excerpt, "\n{EDITABLE_REGION_END_MARKER}").unwrap(); - - if let Some(extra_context_after_range) = extra_context_after_range { - for chunk in snapshot.text_for_range(extra_context_after_range) { - prompt_excerpt.push_str(chunk); - } - } - - write!(prompt_excerpt, "\n```").unwrap(); - debug_assert!( - prompt_excerpt.len() <= len_guess, - "Excerpt length {} exceeds estimated length {}", - prompt_excerpt.len(), - len_guess - ); - prompt_excerpt -} - -fn excerpt_range_for_position( - cursor_point: Point, - byte_limit: usize, - line_limit: u32, - path: &str, - snapshot: &BufferSnapshot, -) -> Result<(Range, usize)> { - let cursor_row = cursor_point.row; - let last_buffer_row = snapshot.max_point().row; - - // This is an overestimate because it includes parts of prompt_for_excerpt which are - // conditionally skipped. - let mut len_guess = 0; - len_guess += "```".len() + path.len() + 1; - len_guess += START_OF_FILE_MARKER.len() + 1; - len_guess += EDITABLE_REGION_START_MARKER.len() + 1; - len_guess += CURSOR_MARKER.len(); - len_guess += EDITABLE_REGION_END_MARKER.len() + 1; - len_guess += "```".len() + 1; - - len_guess += usize::try_from(snapshot.line_len(cursor_row) + 1).unwrap(); - - if len_guess > byte_limit { - return Err(anyhow!("Current line too long to send to model.")); - } - - let mut excerpt_start_row = cursor_row; - let mut excerpt_end_row = cursor_row; - let mut no_more_before = cursor_row == 0; - let mut no_more_after = cursor_row >= last_buffer_row; - let mut row_delta = 1; - loop { - if !no_more_before { - let row = cursor_point.row - row_delta; - let line_len: usize = usize::try_from(snapshot.line_len(row) + 1).unwrap(); - let mut new_len_guess = len_guess + line_len; - if row == 0 { - new_len_guess += START_OF_FILE_MARKER.len() + 1; - } - if new_len_guess <= byte_limit { - len_guess = new_len_guess; - excerpt_start_row = row; - if row == 0 { - no_more_before = true; - } - } else { - no_more_before = true; - } - } - if excerpt_end_row - excerpt_start_row >= line_limit { - break; - } - if !no_more_after { - let row = cursor_point.row + row_delta; - let line_len: usize = usize::try_from(snapshot.line_len(row) + 1).unwrap(); - let new_len_guess = len_guess + line_len; - if new_len_guess <= byte_limit { - len_guess = new_len_guess; - excerpt_end_row = row; - if row >= last_buffer_row { - no_more_after = true; - } - } else { - no_more_after = true; - } - } - if excerpt_end_row - excerpt_start_row >= line_limit { - break; - } - if no_more_before && no_more_after { +fn prompt_for_events(events: &VecDeque, mut remaining_tokens: usize) -> String { + let mut result = String::new(); + for event in events.iter().rev() { + let event_string = event.to_prompt(); + let event_tokens = tokens_for_bytes(event_string.len()); + if event_tokens > remaining_tokens { break; } - row_delta += 1; - } - - let excerpt_start = Point::new(excerpt_start_row, 0); - let excerpt_end = Point::new(excerpt_end_row, snapshot.line_len(excerpt_end_row)); - Ok(( - excerpt_start.to_offset(snapshot)..excerpt_end.to_offset(snapshot), - len_guess, - )) -} -fn prompt_for_events<'a>( - events: impl Iterator, - mut bytes_remaining: usize, -) -> String { - let mut result = String::new(); - for event in events { if !result.is_empty() { - result.push('\n'); - result.push('\n'); - } - let event_string = event.to_prompt(); - let len = event_string.len(); - if len > PER_EVENT_BYTE_LIMIT { - continue; + result.insert_str(0, "\n\n"); } - if len > bytes_remaining { - break; - } - bytes_remaining -= len; - result.push_str(&event_string); + result.insert_str(0, &event_string); + remaining_tokens -= event_tokens; } result } @@ -1750,6 +1555,13 @@ impl inline_completion::EditPredictionProvider for ZetaInlineCompletionProvider } } +fn tokens_for_bytes(bytes: usize) -> usize { + /// Typical number of string bytes per token for the purposes of limiting model input. This is + /// intentionally low to err on the side of underestimating limits. + const BYTES_PER_TOKEN_GUESS: usize = 3; + bytes / BYTES_PER_TOKEN_GUESS +} + #[cfg(test)] mod tests { use client::test::FakeServer; @@ -1757,6 +1569,7 @@ mod tests { use gpui::TestAppContext; use http_client::FakeHttpClient; use indoc::indoc; + use language::Point; use language_models::RefreshLlmTokenListener; use rpc::proto; use settings::SettingsStore; From 3be8066415843bff3859e4c468c28343503216d0 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 7 Feb 2025 12:31:12 -0700 Subject: [PATCH 02/42] Newlines in commit editor (#24465) Release Notes: - N/A --- assets/keymaps/default-macos.json | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 7f852ee4f76797dad7e56fb7d65278b45646e6b5..3f6799722431c57e4eb0d081fbd3db66f6be79fe 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -737,6 +737,7 @@ "context": "GitPanel > Editor", "use_key_equivalents": true, "bindings": { + "enter": "editor::Newline", "cmd-enter": "git::Commit", "tab": "git_panel::FocusChanges", "shift-tab": "git_panel::FocusChanges", From ead5a836a1dcebdc7d59a189d2d74766dbdb1408 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Sat, 8 Feb 2025 03:54:34 +0800 Subject: [PATCH 03/42] gpui: Add data table example (#24373) Release Notes: - N/A As https://github.com/zed-industries/zed/discussions/24260 I mentioned issue. Make a complex data table example to test the text rendering performance. This example also can be an example to show how to build a large data table. ```bash cargo run -p gpui --example data_table ``` image ---- I will try to do some test. For example: With a threshold for the hold number of caches in `FrameCache`, and only when the threshold is greater than a certain number, some caches are released, or when a certain time has passed. I am not sure if this is feasible. This example is added to help us to test. --- crates/gpui/examples/data_table.rs | 479 +++++++++++++++++++++++++++++ 1 file changed, 479 insertions(+) create mode 100644 crates/gpui/examples/data_table.rs diff --git a/crates/gpui/examples/data_table.rs b/crates/gpui/examples/data_table.rs new file mode 100644 index 0000000000000000000000000000000000000000..8a70b5546624d9ffc0ce0c3a8c295545167fd0c5 --- /dev/null +++ b/crates/gpui/examples/data_table.rs @@ -0,0 +1,479 @@ +use std::{ + ops::Range, + rc::Rc, + time::{Duration, Instant}, +}; + +use gpui::{ + canvas, div, point, prelude::*, px, rgb, size, uniform_list, App, Application, Bounds, Context, + MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, Render, SharedString, + UniformListScrollHandle, Window, WindowBounds, WindowOptions, +}; + +const TOTAL_ITEMS: usize = 10000; +const SCROLLBAR_THUMB_WIDTH: Pixels = px(8.); +const SCROLLBAR_THUMB_HEIGHT: Pixels = px(100.); + +pub struct Quote { + name: SharedString, + symbol: SharedString, + last_done: f64, + prev_close: f64, + open: f64, + high: f64, + low: f64, + timestamp: Instant, + volume: i64, + turnover: f64, + ttm: f64, + market_cap: f64, + float_cap: f64, + shares: f64, + pb: f64, + pe: f64, + eps: f64, + dividend: f64, + dividend_yield: f64, + dividend_per_share: f64, + dividend_date: SharedString, + dividend_payment: f64, +} + +impl Quote { + pub fn random() -> Self { + use rand::Rng; + let mut rng = rand::thread_rng(); + // simulate a base price in a realistic range + let prev_close = rng.gen_range(100.0..200.0); + let change = rng.gen_range(-5.0..5.0); + let last_done = prev_close + change; + let open = prev_close + rng.gen_range(-3.0..3.0); + let high = (prev_close + rng.gen_range::(0.0..10.0)).max(open); + let low = (prev_close - rng.gen_range::(0.0..10.0)).min(open); + // Randomize the timestamp in the past 24 hours + let timestamp = Instant::now() - Duration::from_secs(rng.gen_range(0..86400)); + let volume = rng.gen_range(1_000_000..100_000_000); + let turnover = last_done * volume as f64; + let symbol = { + let mut ticker = String::new(); + if rng.gen_bool(0.5) { + ticker.push_str(&format!( + "{:03}.{}", + rng.gen_range(100..1000), + rng.gen_range(0..10) + )); + } else { + ticker.push_str(&format!( + "{}{}", + rng.gen_range('A'..='Z'), + rng.gen_range('A'..='Z') + )); + } + ticker.push_str(&format!(".{}", rng.gen_range('A'..='Z'))); + ticker + }; + let name = format!( + "{} {} - #{}", + symbol, + rng.gen_range(1..100), + rng.gen_range(10000..100000) + ); + let ttm = rng.gen_range(0.0..10.0); + let market_cap = rng.gen_range(1_000_000.0..10_000_000.0); + let float_cap = market_cap + rng.gen_range(1_000.0..10_000.0); + let shares = rng.gen_range(100.0..1000.0); + let pb = market_cap / shares; + let pe = market_cap / shares; + let eps = market_cap / shares; + let dividend = rng.gen_range(0.0..10.0); + let dividend_yield = rng.gen_range(0.0..10.0); + let dividend_per_share = rng.gen_range(0.0..10.0); + let dividend_date = SharedString::new(format!( + "{}-{}-{}", + rng.gen_range(2000..2023), + rng.gen_range(1..12), + rng.gen_range(1..28) + )); + let dividend_payment = rng.gen_range(0.0..10.0); + + Self { + name: name.into(), + symbol: symbol.into(), + last_done, + prev_close, + open, + high, + low, + timestamp, + volume, + turnover, + pb, + pe, + eps, + ttm, + market_cap, + float_cap, + shares, + dividend, + dividend_yield, + dividend_per_share, + dividend_date, + dividend_payment, + } + } + + fn change(&self) -> f64 { + (self.last_done - self.prev_close) / self.prev_close * 100.0 + } + + fn change_color(&self) -> gpui::Hsla { + if self.change() > 0.0 { + gpui::green() + } else { + gpui::red() + } + } + + fn turnover_ratio(&self) -> f64 { + self.volume as f64 / self.turnover * 100.0 + } +} + +#[derive(IntoElement)] +struct TableRow { + ix: usize, + quote: Rc, +} +impl TableRow { + fn new(ix: usize, quote: Rc) -> Self { + Self { ix, quote } + } + + fn render_cell(&self, key: &str, width: Pixels, color: gpui::Hsla) -> impl IntoElement { + div() + .whitespace_nowrap() + .truncate() + .w(width) + .px_1() + .child(match key { + "id" => div().child(format!("{}", self.ix)), + "symbol" => div().child(self.quote.symbol.clone()), + "name" => div().child(self.quote.name.clone()), + "last_done" => div() + .text_color(color) + .child(format!("{:.3}", self.quote.last_done)), + "prev_close" => div() + .text_color(color) + .child(format!("{:.3}", self.quote.prev_close)), + "change" => div() + .text_color(color) + .child(format!("{:.2}%", self.quote.change())), + "timestamp" => div() + .text_color(color) + .child(format!("{:?}", self.quote.timestamp.elapsed().as_secs())), + "open" => div() + .text_color(color) + .child(format!("{:.2}", self.quote.open)), + "low" => div() + .text_color(color) + .child(format!("{:.2}", self.quote.low)), + "high" => div() + .text_color(color) + .child(format!("{:.2}", self.quote.high)), + "ttm" => div() + .text_color(color) + .child(format!("{:.2}", self.quote.ttm)), + "eps" => div() + .text_color(color) + .child(format!("{:.2}", self.quote.eps)), + "market_cap" => { + div().child(format!("{:.2} M", self.quote.market_cap / 1_000_000.0)) + } + "float_cap" => div().child(format!("{:.2} M", self.quote.float_cap / 1_000_000.0)), + "turnover" => div().child(format!("{:.2} M", self.quote.turnover / 1_000_000.0)), + "volume" => div().child(format!("{:.2} M", self.quote.volume as f64 / 1_000_000.0)), + "turnover_ratio" => div().child(format!("{:.2}%", self.quote.turnover_ratio())), + "pe" => div().child(format!("{:.2}", self.quote.pe)), + "pb" => div().child(format!("{:.2}", self.quote.pb)), + "shares" => div().child(format!("{:.2}", self.quote.shares)), + "dividend" => div().child(format!("{:.2}", self.quote.dividend)), + "yield" => div().child(format!("{:.2}%", self.quote.dividend_yield)), + "dividend_per_share" => { + div().child(format!("{:.2}", self.quote.dividend_per_share)) + } + "dividend_date" => div().child(format!("{}", self.quote.dividend_date)), + "dividend_payment" => div().child(format!("{:.2}", self.quote.dividend_payment)), + _ => div().child("--"), + }) + } +} + +const FIELDS: [(&str, f32); 24] = [ + ("id", 64.), + ("symbol", 64.), + ("name", 180.), + ("last_done", 80.), + ("prev_close", 80.), + ("open", 80.), + ("low", 80.), + ("high", 80.), + ("ttm", 50.), + ("market_cap", 96.), + ("float_cap", 96.), + ("turnover", 120.), + ("volume", 100.), + ("turnover_ratio", 96.), + ("pe", 64.), + ("pb", 64.), + ("eps", 64.), + ("shares", 96.), + ("dividend", 64.), + ("yield", 64.), + ("dividend_per_share", 64.), + ("dividend_date", 96.), + ("dividend_payment", 64.), + ("timestamp", 120.), +]; + +impl RenderOnce for TableRow { + fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { + let color = self.quote.change_color(); + div() + .flex() + .flex_row() + .border_b_1() + .border_color(rgb(0xE0E0E0)) + .bg(if self.ix % 2 == 0 { + rgb(0xFFFFFF) + } else { + rgb(0xFAFAFA) + }) + .py_0p5() + .px_2() + .children(FIELDS.map(|(key, width)| self.render_cell(key, px(width), color))) + } +} + +struct DataTable { + /// Use `Rc` to share the same quote data across multiple items, avoid cloning. + quotes: Vec>, + visible_range: Range, + scroll_handle: UniformListScrollHandle, + /// The position in thumb bounds when dragging start mouse down. + drag_position: Option>, +} + +impl DataTable { + fn new() -> Self { + Self { + quotes: Vec::new(), + visible_range: 0..0, + scroll_handle: UniformListScrollHandle::new(), + drag_position: None, + } + } + + fn generate(&mut self) { + self.quotes = (0..TOTAL_ITEMS).map(|_| Rc::new(Quote::random())).collect(); + } + + fn table_bounds(&self) -> Bounds { + self.scroll_handle.0.borrow().base_handle.bounds() + } + + fn scroll_top(&self) -> Pixels { + self.scroll_handle.0.borrow().base_handle.offset().y + } + + fn scroll_height(&self) -> Pixels { + self.scroll_handle + .0 + .borrow() + .last_item_size + .unwrap_or_default() + .contents + .height + } + + fn render_scrollbar(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let scroll_height = self.scroll_height(); + let table_bounds = self.table_bounds(); + let table_height = table_bounds.size.height; + if table_height == px(0.) { + return div().id("scrollbar"); + } + + let percentage = -self.scroll_top() / scroll_height; + let offset_top = (table_height * percentage).clamp( + px(4.), + (table_height - SCROLLBAR_THUMB_HEIGHT - px(4.)).max(px(4.)), + ); + let entity = cx.entity(); + let scroll_handle = self.scroll_handle.0.borrow().base_handle.clone(); + + div() + .id("scrollbar") + .absolute() + .top(offset_top) + .right_1() + .h(SCROLLBAR_THUMB_HEIGHT) + .w(SCROLLBAR_THUMB_WIDTH) + .bg(rgb(0xC0C0C0)) + .hover(|this| this.bg(rgb(0xA0A0A0))) + .rounded_lg() + .child( + canvas( + |_, _, _| (), + move |thumb_bounds, _, window, _| { + window.on_mouse_event({ + let entity = entity.clone(); + move |ev: &MouseDownEvent, _, _, cx| { + if !thumb_bounds.contains(&ev.position) { + return; + } + + entity.update(cx, |this, _| { + this.drag_position = Some( + ev.position - thumb_bounds.origin - table_bounds.origin, + ); + }) + } + }); + window.on_mouse_event({ + let entity = entity.clone(); + move |_: &MouseUpEvent, _, _, cx| { + entity.update(cx, |this, _| { + this.drag_position = None; + }) + } + }); + + window.on_mouse_event(move |ev: &MouseMoveEvent, _, _, cx| { + if !ev.dragging() { + return; + } + + let Some(drag_pos) = entity.read(cx).drag_position else { + return; + }; + + let inside_offset = drag_pos.y; + let percentage = ((ev.position.y - table_bounds.origin.y + + inside_offset) + / (table_bounds.size.height)) + .clamp(0., 1.); + + let offset_y = ((scroll_height - table_bounds.size.height) + * percentage) + .clamp(px(0.), scroll_height - SCROLLBAR_THUMB_HEIGHT); + scroll_handle.set_offset(point(px(0.), -offset_y)); + cx.notify(entity.entity_id()); + }) + }, + ) + .size_full(), + ) + } +} + +impl Render for DataTable { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let entity = cx.entity(); + + div() + .font_family(".SystemUIFont") + .bg(gpui::white()) + .text_sm() + .size_full() + .p_4() + .gap_2() + .flex() + .flex_col() + .child(format!( + "Total {} items, visible range: {:?}", + self.quotes.len(), + self.visible_range + )) + .child( + div() + .flex() + .flex_col() + .flex_1() + .overflow_hidden() + .border_1() + .border_color(rgb(0xE0E0E0)) + .rounded_md() + .child( + div() + .flex() + .flex_row() + .w_full() + .overflow_hidden() + .border_b_1() + .border_color(rgb(0xE0E0E0)) + .text_color(rgb(0x555555)) + .bg(rgb(0xF0F0F0)) + .py_1() + .px_2() + .text_xs() + .children(FIELDS.map(|(key, width)| { + div() + .whitespace_nowrap() + .flex_shrink_0() + .truncate() + .px_1() + .w(px(width)) + .child(key.replace("_", " ").to_uppercase()) + })), + ) + .child( + div() + .relative() + .size_full() + .child( + uniform_list(entity, "items", self.quotes.len(), { + move |this, range, _, _| { + this.visible_range = range.clone(); + let mut items = Vec::with_capacity(range.end - range.start); + for i in range { + if let Some(quote) = this.quotes.get(i) { + items.push(TableRow::new(i, quote.clone())); + } + } + items + } + }) + .size_full() + .track_scroll(self.scroll_handle.clone()), + ) + .child(self.render_scrollbar(window, cx)), + ), + ) + } +} + +fn main() { + Application::new().run(|cx: &mut App| { + cx.open_window( + WindowOptions { + focus: true, + window_bounds: Some(WindowBounds::Windowed(Bounds::centered( + None, + size(px(1280.0), px(1000.0)), + cx, + ))), + ..Default::default() + }, + |_, cx| { + cx.new(|_| { + let mut table = DataTable::new(); + table.generate(); + table + }) + }, + ) + .unwrap(); + + cx.activate(true); + }); +} From 7148092e12b22d50f5936327313925086c7381f2 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 7 Feb 2025 13:08:09 -0700 Subject: [PATCH 04/42] Fix adding new git repos to a project (#24471) Release Notes: - N/A --- crates/git_ui/src/git_panel.rs | 6 +++++- crates/util/src/paths.rs | 2 +- crates/worktree/src/worktree.rs | 18 +++++++++++++++--- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 7071504cff286e4844171a252517a965a082d89e..50ed70b3b40fa13e8a74f7152c97795a448bb255 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -1243,7 +1243,11 @@ impl GitPanel { .child( v_flex() .gap_3() - .child("No changes to commit") + .child(if self.active_repository.is_some() { + "No changes to commit" + } else { + "No Git repositories" + }) .text_ui_sm(cx) .mx_auto() .text_color(Color::Placeholder.color(cx)), diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index b3d0c28bbba40dddbc5049858d0a152093637b7c..c2d66b573e321c77195cecc0e71d07ec8138d1da 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -105,7 +105,7 @@ impl> PathExt for T { /// leverages Rust's type system to ensure that all paths entering Zed are always "sanitized" by removing the `\\\\?\\` prefix. /// On non-Windows operating systems, this struct is effectively a no-op. #[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct SanitizedPath(Arc); +pub struct SanitizedPath(pub Arc); impl SanitizedPath { pub fn starts_with(&self, prefix: &SanitizedPath) -> bool { diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index c2e0a1551e717c9ff9ed7a7b515334d3a9acaea8..ed559eea177d852b54324f6c7a54e50973671a8b 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -2817,6 +2817,7 @@ impl Snapshot { pub fn entry_for_path(&self, path: impl AsRef) -> Option<&Entry> { let path = path.as_ref(); + debug_assert!(path.is_relative()); self.traverse_from_path(true, true, true, path) .entry() .and_then(|entry| { @@ -4384,6 +4385,13 @@ impl BackgroundScanner { dot_git_abs_paths.push(dot_git_abs_path); } } + if abs_path.0.file_name() == Some(*GITIGNORE) { + for (_, repo) in snapshot.git_repositories.iter().filter(|(_, repo)| repo.directory_contains(&abs_path.0)) { + if !dot_git_abs_paths.iter().any(|dot_git_abs_path| dot_git_abs_path == repo.dot_git_dir_abs_path.as_ref()) { + dot_git_abs_paths.push(repo.dot_git_dir_abs_path.to_path_buf()); + } + } + } let relative_path: Arc = if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) { @@ -5169,8 +5177,12 @@ impl BackgroundScanner { let local_repository = match existing_repository_entry { None => { + let Ok(relative) = dot_git_dir.strip_prefix(state.snapshot.abs_path()) + else { + return; + }; match state.insert_git_repository( - dot_git_dir.into(), + relative.into(), self.fs.as_ref(), self.watcher.as_ref(), ) { @@ -5299,8 +5311,8 @@ impl BackgroundScanner { let Some(mut repository) = snapshot.repository(job.local_repository.work_directory.path_key()) else { - log::error!("Got an UpdateGitStatusesJob for a repository that isn't in the snapshot"); - debug_assert!(false); + // happens when a folder is deleted + log::debug!("Got an UpdateGitStatusesJob for a repository that isn't in the snapshot"); return; }; From 9e5bc81f1c8fe6b611c2c0538e91e688521d68b7 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 7 Feb 2025 15:57:42 -0500 Subject: [PATCH 05/42] zeta: Promote line comment to doc comment (#24476) This PR promotes a line comment for the `tos_accepted` field to a doc comment. Release Notes: - N/A --- crates/zeta/src/zeta.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 2dd6b7cbf29826b7cc654a98dd38986b4cca1081..bdd5c6412bf94d393ba83ac87b5f2918406d571f 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -189,7 +189,8 @@ pub struct Zeta { data_collection_choice: Entity, llm_token: LlmApiToken, _llm_token_subscription: Subscription, - tos_accepted: bool, // Terms of service accepted + /// Whether the terms of service have been accepted. + tos_accepted: bool, _user_store_subscription: Subscription, license_detection_watchers: HashMap>, } From 07f1b612cf4451c9a5f3a85cc153fd68df2e2833 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Fri, 7 Feb 2025 17:58:20 -0300 Subject: [PATCH 06/42] edit predictions: Fix translucent "jump to edit" background color (#24473) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR uses a pretty cool GPUI method called `blend` to make this callout's background color not translucent. | Before | Header | |--------|--------| | Screenshot 2025-02-07 at 4 58 16 PM | Screenshot 2025-02-07 at 4 56 48 PM | Release Notes: - N/A --- crates/editor/src/element.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 04bcf722625bfb40016f4d7b16b0b246f97c877e..fe829e2ad6fb11a8af93f9c5be1f98658cd8eef0 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -5800,6 +5800,9 @@ fn inline_completion_accept_indicator( .child(accept_keystroke.key.clone()); let padding_right = if icon.is_some() { px(4.) } else { px(8.) }; + let accent_color = cx.theme().colors().text_accent; + let editor_bg_color = cx.theme().colors().editor_background; + let bg_color = editor_bg_color.blend(accent_color.opacity(0.2)); Some( h_flex() @@ -5807,7 +5810,7 @@ fn inline_completion_accept_indicator( .pl_1() .pr(padding_right) .gap_1() - .bg(cx.theme().colors().text_accent.opacity(0.15)) + .bg(bg_color) .border_1() .border_color(cx.theme().colors().text_accent.opacity(0.8)) .rounded_md() From c4bcff1e87a3b4851beac254042ce37d218036e3 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Fri, 7 Feb 2025 18:01:39 -0300 Subject: [PATCH 07/42] edit predictions: Add binding to the prediction toggle (#24468) This PR primary goal is to add a keybinding to the (ephemeral) prediction toggle. In doing that, we also standardized the keybinding to open the status bar menu with it. Release Notes: - N/A --------- Co-authored-by: Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com> --- assets/icons/lock_outlined.svg | 6 + assets/keymaps/default-linux.json | 9 +- assets/keymaps/default-macos.json | 13 +- .../src/inline_completion_button.rs | 63 +++- crates/ui/src/components/context_menu.rs | 3 +- crates/ui/src/components/icon.rs | 1 + crates/zed/src/zed/quick_action_bar.rs | 280 +++++++++--------- 7 files changed, 214 insertions(+), 161 deletions(-) create mode 100644 assets/icons/lock_outlined.svg diff --git a/assets/icons/lock_outlined.svg b/assets/icons/lock_outlined.svg new file mode 100644 index 0000000000000000000000000000000000000000..0bfd2fdc82ad6cfd21e9fd2c901a7604fb6c0ba9 --- /dev/null +++ b/assets/icons/lock_outlined.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 42c879a534722699147a59d768791b07a8f66b5d..48eebbeaef5af04bc861bcf95616a9cdec30cfde 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -122,7 +122,8 @@ "ctrl-i": "editor::ShowSignatureHelp", "alt-g b": "editor::ToggleGitBlame", "menu": "editor::OpenContextMenu", - "shift-f10": "editor::OpenContextMenu" + "shift-f10": "editor::OpenContextMenu", + "ctrl-shift-e": "editor::ToggleEditPrediction" } }, { @@ -535,8 +536,7 @@ { "bindings": { "ctrl-alt-shift-f": "workspace::FollowNextCollaborator", - "ctrl-alt-i": "zed::DebugElements", - "ctrl-:": "editor::ToggleInlayHints" + "ctrl-alt-i": "zed::DebugElements" } }, { @@ -554,7 +554,8 @@ "ctrl-shift-e": "pane::RevealInProjectPanel", "ctrl-f8": "editor::GoToHunk", "ctrl-shift-f8": "editor::GoToPrevHunk", - "ctrl-enter": "assistant::InlineAssist" + "ctrl-enter": "assistant::InlineAssist", + "ctrl-:": "editor::ToggleInlayHints" } }, { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 3f6799722431c57e4eb0d081fbd3db66f6be79fe..5fa14b940c592f19f708b198ebe47dc41d917174 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -39,8 +39,8 @@ "cmd-m": "zed::Minimize", "fn-f": "zed::ToggleFullScreen", "ctrl-cmd-f": "zed::ToggleFullScreen", - "ctrl-shift-z": "zeta::RateCompletions", - "ctrl-shift-i": "edit_prediction::ToggleMenu" + "ctrl-cmd-z": "zeta::RateCompletions", + "ctrl-cmd-i": "edit_prediction::ToggleMenu" } }, { @@ -132,7 +132,8 @@ "cmd-alt-g b": "editor::ToggleGitBlame", "cmd-i": "editor::ShowSignatureHelp", "ctrl-f12": "editor::GoToDeclaration", - "alt-ctrl-f12": "editor::GoToDeclarationSplit" + "alt-ctrl-f12": "editor::GoToDeclarationSplit", + "ctrl-cmd-e": "editor::ToggleEditPrediction" } }, { @@ -619,8 +620,7 @@ "ctrl-alt-cmd-f": "workspace::FollowNextCollaborator", // TODO: Move this to a dock open action "cmd-shift-c": "collab_panel::ToggleFocus", - "cmd-alt-i": "zed::DebugElements", - "ctrl-:": "editor::ToggleInlayHints" + "cmd-alt-i": "zed::DebugElements" } }, { @@ -633,7 +633,8 @@ "cmd-shift-e": "pane::RevealInProjectPanel", "cmd-f8": "editor::GoToHunk", "cmd-shift-f8": "editor::GoToPrevHunk", - "ctrl-enter": "assistant::InlineAssist" + "ctrl-enter": "assistant::InlineAssist", + "ctrl-:": "editor::ToggleInlayHints" } }, { diff --git a/crates/inline_completion_button/src/inline_completion_button.rs b/crates/inline_completion_button/src/inline_completion_button.rs index 1864e0c26603885738d73d6040b826a3435e62ef..ce1b7bcd839db698da6aa4a3dc2ceb0736bc5f06 100644 --- a/crates/inline_completion_button/src/inline_completion_button.rs +++ b/crates/inline_completion_button/src/inline_completion_button.rs @@ -1,7 +1,11 @@ use anyhow::Result; use client::UserStore; use copilot::{Copilot, Status}; -use editor::{actions::ShowEditPrediction, scroll::Autoscroll, Editor}; +use editor::{ + actions::{ShowEditPrediction, ToggleEditPrediction}, + scroll::Autoscroll, + Editor, +}; use feature_flags::{ FeatureFlagAppExt, PredictEditsFeatureFlag, PredictEditsRateCompletionsFeatureFlag, }; @@ -44,6 +48,7 @@ struct CopilotErrorToast; pub struct InlineCompletionButton { editor_subscription: Option<(Subscription, usize)>, editor_enabled: Option, + editor_show_predictions: bool, editor_focus_handle: Option, language: Option>, file: Option>, @@ -275,15 +280,29 @@ impl Render for InlineCompletionButton { ); } + let show_editor_predictions = self.editor_show_predictions; + let icon_button = IconButton::new("zed-predict-pending-button", zeta_icon) .shape(IconButtonShape::Square) + .when(enabled && !show_editor_predictions, |this| { + this.indicator(Indicator::dot().color(Color::Muted)) + .indicator_border_color(Some(cx.theme().colors().status_bar_background)) + }) .when(!self.popover_menu_handle.is_deployed(), |element| { - if enabled { - element.tooltip(|window, cx| { - Tooltip::for_action("Edit Prediction", &ToggleMenu, window, cx) - }) - } else { - element.tooltip(|window, cx| { + element.tooltip(move |window, cx| { + if enabled { + if show_editor_predictions { + Tooltip::for_action("Edit Prediction", &ToggleMenu, window, cx) + } else { + Tooltip::with_meta( + "Edit Prediction", + Some(&ToggleMenu), + "Hidden For This File", + window, + cx, + ) + } + } else { Tooltip::with_meta( "Edit Prediction", Some(&ToggleMenu), @@ -291,8 +310,8 @@ impl Render for InlineCompletionButton { window, cx, ) - }) - } + } + }) }); let this = cx.entity().clone(); @@ -347,6 +366,7 @@ impl InlineCompletionButton { Self { editor_subscription: None, editor_enabled: None, + editor_show_predictions: true, editor_focus_handle: None, language: None, file: None, @@ -384,6 +404,21 @@ impl InlineCompletionButton { menu = menu.header("Show Edit Predictions For"); + if let Some(editor_focus_handle) = self.editor_focus_handle.clone() { + menu = menu.toggleable_entry( + "This File", + self.editor_show_predictions, + IconPosition::Start, + Some(Box::new(ToggleEditPrediction)), + { + let editor_focus_handle = editor_focus_handle.clone(); + move |window, cx| { + editor_focus_handle.dispatch_action(&ToggleEditPrediction, window, cx); + } + }, + ); + } + if let Some(language) = self.language.clone() { let fs = fs.clone(); let language_enabled = @@ -393,7 +428,7 @@ impl InlineCompletionButton { menu = menu.toggleable_entry( language.name(), language_enabled, - IconPosition::End, + IconPosition::Start, None, move |_, cx| { toggle_show_inline_completions_for_language(language.clone(), fs.clone(), cx) @@ -406,7 +441,7 @@ impl InlineCompletionButton { menu = menu.toggleable_entry( "All Files", globally_enabled, - IconPosition::End, + IconPosition::Start, None, move |_, cx| toggle_inline_completions_globally(fs.clone(), cx), ); @@ -422,7 +457,7 @@ impl InlineCompletionButton { // TODO: We want to add something later that communicates whether // the current project is open-source. ContextMenuEntry::new("Share Training Data") - .toggleable(IconPosition::End, data_collection.is_enabled()) + .toggleable(IconPosition::Start, data_collection.is_enabled()) .documentation_aside(|_| { Label::new(indoc!{" Help us improve our open model by sharing data from open source repositories. \ @@ -450,6 +485,8 @@ impl InlineCompletionButton { menu = menu.item( ContextMenuEntry::new("Configure Excluded Files") + .icon(IconName::LockOutlined) + .icon_color(Color::Muted) .documentation_aside(|_| { Label::new(indoc!{" Open your settings to add sensitive paths for which Zed will never predict edits."}).into_any_element() @@ -486,7 +523,6 @@ impl InlineCompletionButton { Some(Box::new(ShowEditPrediction)), { let editor_focus_handle = editor_focus_handle.clone(); - move |window, cx| { editor_focus_handle.dispatch_action(&ShowEditPrediction, window, cx); } @@ -571,6 +607,7 @@ impl InlineCompletionButton { .unwrap_or(true), ) }; + self.editor_show_predictions = editor.should_show_inline_completions(cx); self.edit_prediction_provider = editor.edit_prediction_provider(); self.language = language.cloned(); self.file = file; diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index db9632d4ff31e36195c5216f0820d40c512ae47d..fe00c733f054ec2fab8a9cd0c2ff2630d1c0710b 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -674,7 +674,8 @@ impl Render for ContextMenu { let contents = if toggled { v_flex().flex_none().child( Icon::new(IconName::Check) - .color(Color::Accent), + .color(Color::Accent) + .size(*icon_size) ) } else { v_flex().flex_none().size( diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 12346026e81000cc820932ec668b56d10369f52f..4ea5ca9c5434a34f8fe1dbbc10e0460145e63d29 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -234,6 +234,7 @@ pub enum IconName { Link, ListTree, ListX, + LockOutlined, MagnifyingGlass, MailOpen, Maximize, diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs index 67161de75f3e95dbd87ccd792736dd257d2d42f6..bc523be4fdcd08743088cbf9fcfe3a8d2343b77f 100644 --- a/crates/zed/src/zed/quick_action_bar.rs +++ b/crates/zed/src/zed/quick_action_bar.rs @@ -213,6 +213,7 @@ impl Render for QuickActionBar { }) }); + let editor_focus_handle = editor.focus_handle(cx); let editor = editor.downgrade(); let editor_settings_dropdown = { let vim_mode_enabled = VimModeSetting::get_global(cx).0; @@ -231,20 +232,67 @@ impl Render for QuickActionBar { .anchor(Corner::TopRight) .with_handle(self.toggle_settings_handle.clone()) .menu(move |window, cx| { - let menu = ContextMenu::build(window, cx, |mut menu, _, _| { - if supports_inlay_hints { + let menu = ContextMenu::build(window, cx, { + let focus_handle = editor_focus_handle.clone(); + |mut menu, _, _| { + menu = menu.context(focus_handle); + + if supports_inlay_hints { + menu = menu.toggleable_entry( + "Inlay Hints", + inlay_hints_enabled, + IconPosition::Start, + Some(editor::actions::ToggleInlayHints.boxed_clone()), + { + let editor = editor.clone(); + move |window, cx| { + editor + .update(cx, |editor, cx| { + editor.toggle_inlay_hints( + &editor::actions::ToggleInlayHints, + window, + cx, + ); + }) + .ok(); + } + }, + ); + } + + menu = menu.toggleable_entry( + "Selection Menu", + selection_menu_enabled, + IconPosition::Start, + Some(editor::actions::ToggleSelectionMenu.boxed_clone()), + { + let editor = editor.clone(); + move |window, cx| { + editor + .update(cx, |editor, cx| { + editor.toggle_selection_menu( + &editor::actions::ToggleSelectionMenu, + window, + cx, + ) + }) + .ok(); + } + }, + ); + menu = menu.toggleable_entry( - "Inlay Hints", - inlay_hints_enabled, + "Auto Signature Help", + auto_signature_help_enabled, IconPosition::Start, - Some(editor::actions::ToggleInlayHints.boxed_clone()), + Some(editor::actions::ToggleAutoSignatureHelp.boxed_clone()), { let editor = editor.clone(); move |window, cx| { editor .update(cx, |editor, cx| { - editor.toggle_inlay_hints( - &editor::actions::ToggleInlayHints, + editor.toggle_auto_signature_help_menu( + &editor::actions::ToggleAutoSignatureHelp, window, cx, ); @@ -253,138 +301,96 @@ impl Render for QuickActionBar { } }, ); - } - menu = menu.toggleable_entry( - "Selection Menu", - selection_menu_enabled, - IconPosition::Start, - Some(editor::actions::ToggleSelectionMenu.boxed_clone()), - { - let editor = editor.clone(); - move |window, cx| { - editor - .update(cx, |editor, cx| { - editor.toggle_selection_menu( - &editor::actions::ToggleSelectionMenu, - window, - cx, - ) - }) - .ok(); - } - }, - ); - - menu = menu.toggleable_entry( - "Auto Signature Help", - auto_signature_help_enabled, - IconPosition::Start, - Some(editor::actions::ToggleAutoSignatureHelp.boxed_clone()), - { - let editor = editor.clone(); - move |window, cx| { - editor - .update(cx, |editor, cx| { - editor.toggle_auto_signature_help_menu( - &editor::actions::ToggleAutoSignatureHelp, - window, - cx, - ); - }) - .ok(); - } - }, - ); - - let mut inline_completion_entry = ContextMenuEntry::new("Edit Predictions") - .toggleable(IconPosition::Start, inline_completion_enabled && show_inline_completions) - .disabled(!inline_completion_enabled) - .action(Some( - editor::actions::ToggleEditPrediction.boxed_clone(), - )).handler({ - let editor = editor.clone(); - move |window, cx| { - editor - .update(cx, |editor, cx| { - editor.toggle_inline_completions( - &editor::actions::ToggleEditPrediction, - window, - cx, - ); - }) - .ok(); - } - }); - if !inline_completion_enabled { - inline_completion_entry = inline_completion_entry.documentation_aside(|_| { - Label::new("You can't toggle edit predictions for this file as it is within the excluded files list.").into_any_element() - }); - } + let mut inline_completion_entry = ContextMenuEntry::new("Edit Predictions") + .toggleable(IconPosition::Start, inline_completion_enabled && show_inline_completions) + .disabled(!inline_completion_enabled) + .action(Some( + editor::actions::ToggleEditPrediction.boxed_clone(), + )).handler({ + let editor = editor.clone(); + move |window, cx| { + editor + .update(cx, |editor, cx| { + editor.toggle_inline_completions( + &editor::actions::ToggleEditPrediction, + window, + cx, + ); + }) + .ok(); + } + }); + if !inline_completion_enabled { + inline_completion_entry = inline_completion_entry.documentation_aside(|_| { + Label::new("You can't toggle edit predictions for this file as it is within the excluded files list.").into_any_element() + }); + } + + menu = menu.item(inline_completion_entry); + + menu = menu.separator(); + + menu = menu.toggleable_entry( + "Inline Git Blame", + git_blame_inline_enabled, + IconPosition::Start, + Some(editor::actions::ToggleGitBlameInline.boxed_clone()), + { + let editor = editor.clone(); + move |window, cx| { + editor + .update(cx, |editor, cx| { + editor.toggle_git_blame_inline( + &editor::actions::ToggleGitBlameInline, + window, + cx, + ) + }) + .ok(); + } + }, + ); - menu = menu.item(inline_completion_entry); - - menu = menu.separator(); - - menu = menu.toggleable_entry( - "Inline Git Blame", - git_blame_inline_enabled, - IconPosition::Start, - Some(editor::actions::ToggleGitBlameInline.boxed_clone()), - { - let editor = editor.clone(); - move |window, cx| { - editor - .update(cx, |editor, cx| { - editor.toggle_git_blame_inline( - &editor::actions::ToggleGitBlameInline, - window, - cx, - ) - }) - .ok(); - } - }, - ); - - menu = menu.toggleable_entry( - "Column Git Blame", - show_git_blame_gutter, - IconPosition::Start, - Some(editor::actions::ToggleGitBlame.boxed_clone()), - { - let editor = editor.clone(); - move |window, cx| { - editor - .update(cx, |editor, cx| { - editor.toggle_git_blame( - &editor::actions::ToggleGitBlame, - window, - cx, - ) - }) - .ok(); - } - }, - ); - - menu = menu.separator(); - - menu = menu.toggleable_entry( - "Vim Mode", - vim_mode_enabled, - IconPosition::Start, - None, - { - move |window, cx| { - let new_value = !vim_mode_enabled; - VimModeSetting::override_global(VimModeSetting(new_value), cx); - window.refresh(); - } - }, - ); - - menu + menu = menu.toggleable_entry( + "Column Git Blame", + show_git_blame_gutter, + IconPosition::Start, + Some(editor::actions::ToggleGitBlame.boxed_clone()), + { + let editor = editor.clone(); + move |window, cx| { + editor + .update(cx, |editor, cx| { + editor.toggle_git_blame( + &editor::actions::ToggleGitBlame, + window, + cx, + ) + }) + .ok(); + } + }, + ); + + menu = menu.separator(); + + menu = menu.toggleable_entry( + "Vim Mode", + vim_mode_enabled, + IconPosition::Start, + None, + { + move |window, cx| { + let new_value = !vim_mode_enabled; + VimModeSetting::override_global(VimModeSetting(new_value), cx); + window.refresh(); + } + }, + ); + + menu + } }); Some(menu) }) From ed5656813cf9988f601656e3fb6c9c5b429911b4 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 7 Feb 2025 16:03:37 -0500 Subject: [PATCH 08/42] inline_completion: Add missing punctuation (#24477) This PR adds some missing punctuation. Release Notes: - N/A --- crates/inline_completion/src/inline_completion.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/inline_completion/src/inline_completion.rs b/crates/inline_completion/src/inline_completion.rs index 6a1754c3773e5f1361930d81e6c9be9fc0c3e8d9..bc123f0580fa08e05868aea564ca9ad9db6cfaaf 100644 --- a/crates/inline_completion/src/inline_completion.rs +++ b/crates/inline_completion/src/inline_completion.rs @@ -22,7 +22,7 @@ pub struct InlineCompletion { pub enum DataCollectionState { /// The provider doesn't support data collection. Unsupported, - /// Data collection is enabled + /// Data collection is enabled. Enabled, /// Data collection is disabled or unanswered. Disabled, From e17e838c07b9520e116af777826c6bb02b1da999 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 7 Feb 2025 17:06:37 -0500 Subject: [PATCH 09/42] Include prediction ID on edit prediction accepted/discarded events (#24480) This PR updates the edit predictions to include the prediction ID returned from the server on the resulting telemetry events indicating whether the prediction was accepted or discarded. The `prediction_id` on the events can then be correlated with the `request_id` on the server-side prediction events. Release Notes: - N/A --- Cargo.lock | 5 ++-- Cargo.toml | 2 +- .../src/copilot_completion_provider.rs | 1 + crates/editor/src/editor.rs | 24 ++++++++++++--- crates/editor/src/inline_completion_tests.rs | 1 + .../src/inline_completion.rs | 4 ++- .../src/supermaven_completion_provider.rs | 1 + crates/zeta/src/zeta.rs | 29 +++++++++++-------- 8 files changed, 47 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7817dcee3bb605205cf744f36699cc2eee412673..c99fa257127c04e594ea480077ce3e1fe193c8fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16757,12 +16757,13 @@ dependencies = [ [[package]] name = "zed_llm_client" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "656118e6b072924d28815cb892278f12c2548117794e733bd2c075ef4a0427e8" +checksum = "614669bead4741b2fc352ae1967318be16949cf46f59013e548c6dbfdfc01252" dependencies = [ "serde", "serde_json", + "uuid", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b203bd0af3f14f6ad2b096251be2e4a34376a6af..576f4bb797a0c8a2137b913bc0295b7dc95707a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -561,7 +561,7 @@ wasmtime = { version = "24", default-features = false, features = [ wasmtime-wasi = "24" which = "6.0.0" wit-component = "0.201" -zed_llm_client = "0.3" +zed_llm_client = "0.4" zstd = "0.11" metal = "0.31" diff --git a/crates/copilot/src/copilot_completion_provider.rs b/crates/copilot/src/copilot_completion_provider.rs index 0e494056ec4521e1fe24e31e68258812d501ba38..e6757e9d7fa4615af101cf14140442f3c4e61ed9 100644 --- a/crates/copilot/src/copilot_completion_provider.rs +++ b/crates/copilot/src/copilot_completion_provider.rs @@ -242,6 +242,7 @@ impl EditPredictionProvider for CopilotCompletionProvider { } else { let position = cursor_position.bias_right(buffer); Some(InlineCompletion { + id: None, edits: vec![(position..position, completion_text.into())], edit_preview: None, }) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b5ef81bfbabaf1019a31a4aa2e2e41cb37617455..15317e099938422cf2ddc457cfec7ad994faa579 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -490,6 +490,7 @@ enum InlineCompletion { struct InlineCompletionState { inlay_ids: Vec, completion: InlineCompletion, + completion_id: Option, invalidation_range: Range, } @@ -4893,7 +4894,11 @@ impl Editor { return; }; - self.report_inline_completion_event(true, cx); + self.report_inline_completion_event( + active_inline_completion.completion_id.clone(), + true, + cx, + ); match &active_inline_completion.completion { InlineCompletion::Move { target, .. } => { @@ -4942,7 +4947,11 @@ impl Editor { return; } - self.report_inline_completion_event(true, cx); + self.report_inline_completion_event( + active_inline_completion.completion_id.clone(), + true, + cx, + ); match &active_inline_completion.completion { InlineCompletion::Move { target, .. } => { @@ -5000,7 +5009,12 @@ impl Editor { cx: &mut Context, ) -> bool { if should_report_inline_completion_event { - self.report_inline_completion_event(false, cx); + let completion_id = self + .active_inline_completion + .as_ref() + .and_then(|active_completion| active_completion.completion_id.clone()); + + self.report_inline_completion_event(completion_id, false, cx); } if let Some(provider) = self.edit_prediction_provider() { @@ -5010,7 +5024,7 @@ impl Editor { self.take_active_inline_completion(cx) } - fn report_inline_completion_event(&self, accepted: bool, cx: &App) { + fn report_inline_completion_event(&self, id: Option, accepted: bool, cx: &App) { let Some(provider) = self.edit_prediction_provider() else { return; }; @@ -5035,6 +5049,7 @@ impl Editor { telemetry::event!( event_type, provider = provider.name(), + prediction_id = id, suggestion_accepted = accepted, file_extension = extension, ); @@ -5250,6 +5265,7 @@ impl Editor { self.active_inline_completion = Some(InlineCompletionState { inlay_ids, completion, + completion_id: inline_completion.id, invalidation_range, }); diff --git a/crates/editor/src/inline_completion_tests.rs b/crates/editor/src/inline_completion_tests.rs index 258a8780944052a1c398fba33cce843cc585de6d..c74de1fc9329a1984bc4c44b35815f13974e167b 100644 --- a/crates/editor/src/inline_completion_tests.rs +++ b/crates/editor/src/inline_completion_tests.rs @@ -333,6 +333,7 @@ fn propose_edits( cx.update(|_, cx| { provider.update(cx, |provider, _| { provider.set_inline_completion(Some(inline_completion::InlineCompletion { + id: None, edits: edits.collect(), edit_preview: None, })) diff --git a/crates/inline_completion/src/inline_completion.rs b/crates/inline_completion/src/inline_completion.rs index bc123f0580fa08e05868aea564ca9ad9db6cfaaf..cea21472ca3a95597df61bd9ec87cb4e31233aa2 100644 --- a/crates/inline_completion/src/inline_completion.rs +++ b/crates/inline_completion/src/inline_completion.rs @@ -1,4 +1,4 @@ -use gpui::{App, Context, Entity}; +use gpui::{App, Context, Entity, SharedString}; use language::Buffer; use project::Project; use std::ops::Range; @@ -15,6 +15,8 @@ pub enum Direction { #[derive(Clone)] pub struct InlineCompletion { + /// The ID of the completion, if it has one. + pub id: Option, pub edits: Vec<(Range, String)>, pub edit_preview: Option, } diff --git a/crates/supermaven/src/supermaven_completion_provider.rs b/crates/supermaven/src/supermaven_completion_provider.rs index 3e70a1c57672e1e6908404ce5ff95ca55f02be0e..4dc0ebbabbed6d9d16fa765c27c189de82fca8dd 100644 --- a/crates/supermaven/src/supermaven_completion_provider.rs +++ b/crates/supermaven/src/supermaven_completion_provider.rs @@ -92,6 +92,7 @@ fn completion_from_diff( } InlineCompletion { + id: None, edits, edit_preview: None, } diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index bdd5c6412bf94d393ba83ac87b5f2918406d571f..14cd32b300f76508044cd00bab1f6fe8427ba75c 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -81,12 +81,6 @@ impl std::fmt::Display for InlineCompletionId { } } -impl InlineCompletionId { - fn new() -> Self { - Self(Uuid::new_v4()) - } -} - #[derive(Clone)] struct ZetaGlobal(Entity); @@ -452,11 +446,10 @@ impl Zeta { let response = perform_predict_edits(client, llm_token, is_staff, body).await?; - let output_excerpt = response.output_excerpt; - log::debug!("completion response: {}", output_excerpt); + log::debug!("completion response: {}", &response.output_excerpt); Self::process_completion_response( - output_excerpt, + response, buffer, &snapshot, values.editable_range, @@ -495,6 +488,7 @@ impl Zeta { &buffer, position, PredictEditsResponse { + request_id: Uuid::parse_str("e7861db5-0cea-4761-b1c5-ad083ac53a80").unwrap(), output_excerpt: format!("{EDITABLE_REGION_START_MARKER} a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line [here's an edit] @@ -511,6 +505,7 @@ and then another &buffer, position, PredictEditsResponse { + request_id: Uuid::parse_str("077c556a-2c49-44e2-bbc6-dafc09032a5e").unwrap(), output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER} a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line And maybe a short line @@ -527,6 +522,7 @@ and then another &buffer, position, PredictEditsResponse { + request_id: Uuid::parse_str("df8c7b23-3d1d-4f99-a306-1f6264a41277").unwrap(), output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER} a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line And maybe a short line @@ -544,6 +540,7 @@ and then another &buffer, position, PredictEditsResponse { + request_id: Uuid::parse_str("c743958d-e4d8-44a8-aa5b-eb1e305c5f5c").unwrap(), output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER} a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line And maybe a short line @@ -561,6 +558,7 @@ and then another &buffer, position, PredictEditsResponse { + request_id: Uuid::parse_str("ff5cd7ab-ad06-4808-986e-d3391e7b8355").unwrap(), output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER} a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line And maybe a short line @@ -577,6 +575,7 @@ and then another &buffer, position, PredictEditsResponse { + request_id: Uuid::parse_str("83cafa55-cdba-4b27-8474-1865ea06be94").unwrap(), output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER} a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line And maybe a short line @@ -592,6 +591,7 @@ and then another &buffer, position, PredictEditsResponse { + request_id: Uuid::parse_str("d5bd3afd-8723-47c7-bd77-15a3a926867b").unwrap(), output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER} a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line And maybe a short line @@ -703,7 +703,7 @@ and then another #[allow(clippy::too_many_arguments)] fn process_completion_response( - output_excerpt: String, + prediction_response: PredictEditsResponse, buffer: Entity, snapshot: &BufferSnapshot, editable_range: Range, @@ -716,6 +716,8 @@ and then another cx: &AsyncApp, ) -> Task>> { let snapshot = snapshot.clone(); + let request_id = prediction_response.request_id; + let output_excerpt = prediction_response.output_excerpt; cx.spawn(|cx| async move { let output_excerpt: Arc = output_excerpt.into(); @@ -746,7 +748,7 @@ and then another let edit_preview = edit_preview.await; Ok(Some(InlineCompletion { - id: InlineCompletionId::new(), + id: InlineCompletionId(request_id), path, excerpt_range: editable_range, cursor_offset, @@ -1550,6 +1552,7 @@ impl inline_completion::EditPredictionProvider for ZetaInlineCompletionProvider } Some(inline_completion::InlineCompletion { + id: Some(completion.id.to_string().into()), edits: edits[edit_start_ix..edit_end_ix].to_vec(), edit_preview: Some(completion.edit_preview.clone()), }) @@ -1598,7 +1601,7 @@ mod tests { edit_preview, path: Path::new("").into(), snapshot: cx.read(|cx| buffer.read(cx).snapshot()), - id: InlineCompletionId::new(), + id: InlineCompletionId(Uuid::new_v4()), excerpt_range: 0..0, cursor_offset: 0, input_outline: "".into(), @@ -1717,6 +1720,8 @@ mod tests { .status(200) .body( serde_json::to_string(&PredictEditsResponse { + request_id: Uuid::parse_str("7e86480f-3536-4d2c-9334-8213e3445d45") + .unwrap(), output_excerpt: completion_response.to_string(), }) .unwrap() From 4be89ea60f3052eb68d0c80cb74fdb217a425b27 Mon Sep 17 00:00:00 2001 From: Beniamin Zagan <47153906+beniaminzagan@users.noreply.github.com> Date: Fri, 7 Feb 2025 23:33:00 +0100 Subject: [PATCH 10/42] title_bar: Add menu item to deploy icon theme selector (#24482) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added the icons option in the title bar between Themes and Extension. | Before | After | | --------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | Screenshot 2025-02-07 at 5 18 10 PM | Screenshot 2025-02-07 at 5 18 01 PM | Release Notes: - Added an option to open the icon theme selector from the user menu. --------- Co-authored-by: Marshall Bowers --- crates/title_bar/src/title_bar.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 801e701e785ab3ca5f95b2950d72d7fad7737ec2..cd28890b2e69d7d60217552a891edf235e1d7050 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -673,6 +673,10 @@ impl TitleBar { "Themes…", zed_actions::theme_selector::Toggle::default().boxed_clone(), ) + .action( + "Icon Themes…", + zed_actions::icon_theme_selector::Toggle::default().boxed_clone(), + ) .action("Extensions", zed_actions::Extensions.boxed_clone()) .separator() .link( @@ -716,6 +720,10 @@ impl TitleBar { "Themes…", zed_actions::theme_selector::Toggle::default().boxed_clone(), ) + .action( + "Icon Themes…", + zed_actions::icon_theme_selector::Toggle::default().boxed_clone(), + ) .action("Extensions", zed_actions::Extensions.boxed_clone()) .separator() .link( From be26accccabc58fc7b60195a3655e09ce064c0a7 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Fri, 7 Feb 2025 17:18:20 -0700 Subject: [PATCH 11/42] Cargo.lock update (#24486) Release Notes: - N/A --- Cargo.lock | 283 +++++++++++++++++++++++++++-------------------------- 1 file changed, 146 insertions(+), 137 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c99fa257127c04e594ea480077ce3e1fe193c8fb..fbd19d934ca5c4ef204ff4648516cbfd1e12d4f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -631,7 +631,7 @@ dependencies = [ "smol", "terminal_view", "text", - "toml 0.8.19", + "toml 0.8.20", "ui", "util", "workspace", @@ -1012,9 +1012,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.85" +version = "0.1.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" +checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" dependencies = [ "proc-macro2", "quote", @@ -1067,7 +1067,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a860072022177f903e59730004fb5dc13db9275b79bb2aef7ba8ce831956c233" dependencies = [ - "bytes 1.9.0", + "bytes 1.10.0", "futures-sink", "futures-util", "memchr", @@ -1181,9 +1181,9 @@ dependencies = [ [[package]] name = "aws-config" -version = "1.5.15" +version = "1.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc47e70fc35d054c8fcd296d47a61711f043ac80534a10b4f741904f81e73a90" +checksum = "50236e4d60fe8458de90a71c0922c761e41755adf091b1b03de1cef537179915" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1197,7 +1197,7 @@ dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", "aws-types", - "bytes 1.9.0", + "bytes 1.10.0", "fastrand 2.3.0", "hex", "http 0.2.12", @@ -1234,9 +1234,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.25.1" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54ac4f13dad353b209b34cbec082338202cbc01c8f00336b55c750c13ac91f8f" +checksum = "71b2ddd3ada61a305e1d8bb6c005d1eaa7d14d903681edfc400406d523a9b491" dependencies = [ "bindgen 0.69.5", "cc", @@ -1248,9 +1248,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.5.4" +version = "1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee7643696e7fdd74c10f9eb42848a87fe469d35eae9c3323f80aa98f350baac" +checksum = "76dd04d39cc12844c0994f2c9c5a6f5184c22e9188ec1ff723de41910a21dcad" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -1261,7 +1261,7 @@ dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", "aws-types", - "bytes 1.9.0", + "bytes 1.10.0", "fastrand 2.3.0", "http 0.2.12", "http-body 0.4.6", @@ -1274,9 +1274,9 @@ dependencies = [ [[package]] name = "aws-sdk-kinesis" -version = "1.59.0" +version = "1.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7963cf7a0f49ba4f8351044751f4d42c003c4a5f31d9e084f0d0e68b6fb8b8cf" +checksum = "9b8052335b6ba19b08ba2b363c7505f8ed34074ac23fa14a652ff6a0a02a4c06" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1287,7 +1287,7 @@ dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", "aws-types", - "bytes 1.9.0", + "bytes 1.10.0", "http 0.2.12", "once_cell", "regex-lite", @@ -1296,9 +1296,9 @@ dependencies = [ [[package]] name = "aws-sdk-s3" -version = "1.72.0" +version = "1.73.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c7ce6d85596c4bcb3aba8ad5bb134b08e204c8a475c9999c1af9290f80aa8ad" +checksum = "3978e0a211bdc5cddecfd91fb468665a662a27fbdaef39ddf36a2a18fef12cb4" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1313,7 +1313,7 @@ dependencies = [ "aws-smithy-types", "aws-smithy-xml", "aws-types", - "bytes 1.9.0", + "bytes 1.10.0", "fastrand 2.3.0", "hex", "hmac", @@ -1330,9 +1330,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.57.0" +version = "1.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c54bab121fe1881a74c338c5f723d1592bf3b53167f80268a1274f404e1acc38" +checksum = "16ff718c9ee45cc1ebd4774a0e086bb80a6ab752b4902edf1c9f56b86ee1f770" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1343,7 +1343,7 @@ dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", "aws-types", - "bytes 1.9.0", + "bytes 1.10.0", "http 0.2.12", "once_cell", "regex-lite", @@ -1352,9 +1352,9 @@ dependencies = [ [[package]] name = "aws-sdk-ssooidc" -version = "1.58.0" +version = "1.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c8234fd024f7ac61c4e44ea008029bde934250f371efe7d4a39708397b1080c" +checksum = "5183e088715cc135d8d396fdd3bc02f018f0da4c511f53cb8d795b6a31c55809" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1365,7 +1365,7 @@ dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", "aws-types", - "bytes 1.9.0", + "bytes 1.10.0", "http 0.2.12", "once_cell", "regex-lite", @@ -1374,9 +1374,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.58.0" +version = "1.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba60e1d519d6f23a9df712c04fdeadd7872ac911c84b2f62a8bda92e129b7962" +checksum = "c9f944ef032717596639cea4a2118a3a457268ef51bbb5fde9637e54c465da00" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1397,16 +1397,16 @@ dependencies = [ [[package]] name = "aws-sigv4" -version = "1.2.7" +version = "1.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "690118821e46967b3c4501d67d7d52dd75106a9c54cf36cefa1985cedbe94e05" +checksum = "0bc5bbd1e4a2648fd8c5982af03935972c24a2f9846b396de661d351ee3ce837" dependencies = [ "aws-credential-types", "aws-smithy-eventstream", "aws-smithy-http", "aws-smithy-runtime-api", "aws-smithy-types", - "bytes 1.9.0", + "bytes 1.10.0", "crypto-bigint 0.5.5", "form_urlencoded", "hex", @@ -1443,7 +1443,7 @@ checksum = "f2f45a1c384d7a393026bc5f5c177105aa9fa68e4749653b985707ac27d77295" dependencies = [ "aws-smithy-http", "aws-smithy-types", - "bytes 1.9.0", + "bytes 1.10.0", "crc32c", "crc32fast", "crc64fast-nvme", @@ -1464,7 +1464,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b18559a41e0c909b77625adf2b8c50de480a8041e5e4a3f5f7d177db70abc5a" dependencies = [ "aws-smithy-types", - "bytes 1.9.0", + "bytes 1.10.0", "crc32fast", ] @@ -1477,7 +1477,7 @@ dependencies = [ "aws-smithy-eventstream", "aws-smithy-runtime-api", "aws-smithy-types", - "bytes 1.9.0", + "bytes 1.10.0", "bytes-utils", "futures-core", "http 0.2.12", @@ -1510,15 +1510,15 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.7.7" +version = "1.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "865f7050bbc7107a6c98a397a9fcd9413690c27fa718446967cf03b2d3ac517e" +checksum = "d526a12d9ed61fadefda24abe2e682892ba288c2018bcb38b1b4c111d13f6d92" dependencies = [ "aws-smithy-async", "aws-smithy-http", "aws-smithy-runtime-api", "aws-smithy-types", - "bytes 1.9.0", + "bytes 1.10.0", "fastrand 2.3.0", "h2 0.3.26", "http 0.2.12", @@ -1543,7 +1543,7 @@ checksum = "92165296a47a812b267b4f41032ff8069ab7ff783696d217f0994a0d7ab585cd" dependencies = [ "aws-smithy-async", "aws-smithy-types", - "bytes 1.9.0", + "bytes 1.10.0", "http 0.2.12", "http 1.2.0", "pin-project-lite", @@ -1554,12 +1554,12 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.2.12" +version = "1.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28f6feb647fb5e0d5b50f0472c19a7db9462b74e2fec01bb0b44eedcc834e97" +checksum = "c7b8a53819e42f10d0821f56da995e1470b199686a1809168db6ca485665f042" dependencies = [ "base64-simd", - "bytes 1.9.0", + "bytes 1.10.0", "bytes-utils", "futures-core", "http 0.2.12", @@ -1589,9 +1589,9 @@ dependencies = [ [[package]] name = "aws-types" -version = "1.3.4" +version = "1.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0df5a18c4f951c645300d365fec53a61418bcf4650f604f85fe2a665bfaa0c2" +checksum = "dfbd0a668309ec1f66c0f6bda4840dd6d4796ae26d699ebc266d7cc95c6d040f" dependencies = [ "aws-credential-types", "aws-smithy-async", @@ -1611,7 +1611,7 @@ dependencies = [ "axum-core", "base64 0.21.7", "bitflags 1.3.2", - "bytes 1.9.0", + "bytes 1.10.0", "futures-util", "headers", "http 0.2.12", @@ -1644,7 +1644,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" dependencies = [ "async-trait", - "bytes 1.9.0", + "bytes 1.10.0", "futures-util", "http 0.2.12", "http-body 0.4.6", @@ -1661,7 +1661,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9a320103719de37b7b4da4c8eb629d4573f6bcfd3dfe80d3208806895ccf81d" dependencies = [ "axum", - "bytes 1.9.0", + "bytes 1.10.0", "futures-util", "http 0.2.12", "mime", @@ -2108,9 +2108,9 @@ dependencies = [ [[package]] name = "bytes" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" [[package]] name = "bytes-utils" @@ -2118,7 +2118,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" dependencies = [ - "bytes 1.9.0", + "bytes 1.10.0", "either", ] @@ -2311,7 +2311,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fbd1fe9db3ebf71b89060adaf7b0504c2d6a425cf061313099547e382c2e472" dependencies = [ "serde", - "toml 0.8.19", + "toml 0.8.20", ] [[package]] @@ -2345,7 +2345,7 @@ dependencies = [ "serde_json", "syn 2.0.90", "tempfile", - "toml 0.8.19", + "toml 0.8.20", ] [[package]] @@ -2363,7 +2363,7 @@ dependencies = [ "serde_json", "syn 2.0.90", "tempfile", - "toml 0.8.19", + "toml 0.8.20", ] [[package]] @@ -2515,9 +2515,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.23" +version = "4.5.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" +checksum = "3e77c3243bd94243c03672cb5154667347c457ca271254724f9f393aee1c05ff" dependencies = [ "clap_builder", "clap_derive", @@ -2525,9 +2525,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.23" +version = "4.5.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" +checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" dependencies = [ "anstream", "anstyle", @@ -2547,9 +2547,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.18" +version = "4.5.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -2818,7 +2818,7 @@ dependencies = [ "thiserror 1.0.69", "time", "tokio", - "toml 0.8.19", + "toml 0.8.20", "tower", "tower-http 0.4.4", "tracing", @@ -2879,7 +2879,7 @@ name = "collections" version = "0.1.0" dependencies = [ "indexmap", - "rustc-hash 2.1.0", + "rustc-hash 2.1.1", ] [[package]] @@ -2900,7 +2900,7 @@ version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ - "bytes 1.9.0", + "bytes 1.10.0", "memchr", ] @@ -3776,9 +3776,9 @@ dependencies = [ [[package]] name = "derive_more" -version = "0.99.18" +version = "0.99.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +checksum = "3da29a38df43d6f156149c9b43ded5e018ddff2a855cf2cfd62e8cd7d079c69f" dependencies = [ "convert_case 0.4.0", "proc-macro2", @@ -4132,7 +4132,7 @@ dependencies = [ "cc", "memchr", "rustc_version", - "toml 0.8.19", + "toml 0.8.20", "vswhom", "winreg 0.52.0", ] @@ -4423,7 +4423,7 @@ dependencies = [ "semantic_version", "serde", "serde_json", - "toml 0.8.19", + "toml 0.8.20", "util", "wasm-encoder 0.215.0", "wasmparser 0.215.0", @@ -4447,7 +4447,7 @@ dependencies = [ "serde_json", "theme", "tokio", - "toml 0.8.19", + "toml 0.8.20", "tree-sitter", "wasmtime", ] @@ -4492,7 +4492,7 @@ dependencies = [ "tempfile", "theme", "theme_extension", - "toml 0.8.19", + "toml 0.8.20", "url", "util", "wasmparser 0.215.0", @@ -5595,7 +5595,7 @@ version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ - "bytes 1.9.0", + "bytes 1.10.0", "fnv", "futures-core", "futures-sink", @@ -5615,7 +5615,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" dependencies = [ "atomic-waker", - "bytes 1.9.0", + "bytes 1.10.0", "fnv", "futures-core", "futures-sink", @@ -5731,7 +5731,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" dependencies = [ "base64 0.21.7", - "bytes 1.9.0", + "bytes 1.10.0", "headers-core", "http 0.2.12", "httpdate", @@ -5910,7 +5910,7 @@ version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ - "bytes 1.9.0", + "bytes 1.10.0", "fnv", "itoa", ] @@ -5921,7 +5921,7 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" dependencies = [ - "bytes 1.9.0", + "bytes 1.10.0", "fnv", "itoa", ] @@ -5932,7 +5932,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ - "bytes 1.9.0", + "bytes 1.10.0", "http 0.2.12", "pin-project-lite", ] @@ -5943,7 +5943,7 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ - "bytes 1.9.0", + "bytes 1.10.0", "http 1.2.0", ] @@ -5953,7 +5953,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ - "bytes 1.9.0", + "bytes 1.10.0", "futures-util", "http 1.2.0", "http-body 1.0.1", @@ -5992,7 +5992,7 @@ name = "http_client" version = "0.1.0" dependencies = [ "anyhow", - "bytes 1.9.0", + "bytes 1.10.0", "derive_more", "futures 0.3.31", "http 1.2.0", @@ -6032,7 +6032,7 @@ version = "0.14.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" dependencies = [ - "bytes 1.9.0", + "bytes 1.10.0", "futures-channel", "futures-core", "futures-util", @@ -6056,7 +6056,7 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" dependencies = [ - "bytes 1.9.0", + "bytes 1.10.0", "futures-channel", "futures-util", "h2 0.4.7", @@ -6110,7 +6110,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ - "bytes 1.9.0", + "bytes 1.10.0", "hyper 0.14.32", "native-tls", "tokio", @@ -6123,7 +6123,7 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" dependencies = [ - "bytes 1.9.0", + "bytes 1.10.0", "futures-channel", "futures-util", "http 1.2.0", @@ -6778,7 +6778,7 @@ checksum = "c9ae6296f9476658b3550293c113996daf75fa542cd8d078abb4c60207bded14" dependencies = [ "anyhow", "async-trait", - "bytes 1.9.0", + "bytes 1.10.0", "chrono", "futures 0.3.31", "serde", @@ -7089,7 +7089,7 @@ dependencies = [ "task", "text", "theme", - "toml 0.8.19", + "toml 0.8.20", "tree-sitter", "tree-sitter-bash", "tree-sitter-c", @@ -9137,7 +9137,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18f596653ba4ac51bdecbb4ef6773bc7f56042dc13927910de1684ad3d32aa12" dependencies = [ - "bytes 1.9.0", + "bytes 1.10.0", "chrono", "pbjson", "pbjson-build", @@ -9481,7 +9481,7 @@ dependencies = [ "serde", "serde_json", "sha2", - "toml 0.8.19", + "toml 0.8.20", ] [[package]] @@ -10092,7 +10092,7 @@ dependencies = [ "tempfile", "terminal", "text", - "toml 0.8.19", + "toml 0.8.20", "unindent", "url", "util", @@ -10210,7 +10210,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" dependencies = [ - "bytes 1.9.0", + "bytes 1.10.0", "prost-derive 0.9.0", ] @@ -10220,7 +10220,7 @@ version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" dependencies = [ - "bytes 1.9.0", + "bytes 1.10.0", "prost-derive 0.12.6", ] @@ -10230,7 +10230,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62941722fb675d463659e49c4f3fe1fe792ff24fe5bbaa9c08cd3b98a1c354f5" dependencies = [ - "bytes 1.9.0", + "bytes 1.10.0", "heck 0.3.3", "itertools 0.10.5", "lazy_static", @@ -10250,7 +10250,7 @@ version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" dependencies = [ - "bytes 1.9.0", + "bytes 1.10.0", "heck 0.5.0", "itertools 0.12.1", "log", @@ -10297,7 +10297,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "534b7a0e836e3c482d2693070f982e39e7611da9695d4d1f5a4b186b51faef0a" dependencies = [ - "bytes 1.9.0", + "bytes 1.10.0", "prost 0.9.0", ] @@ -10411,9 +10411,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.36.2" +version = "0.37.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" +checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003" dependencies = [ "memchr", ] @@ -10424,11 +10424,11 @@ version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" dependencies = [ - "bytes 1.9.0", + "bytes 1.10.0", "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.0", + "rustc-hash 2.1.1", "rustls 0.23.22", "socket2", "thiserror 2.0.6", @@ -10442,11 +10442,11 @@ version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" dependencies = [ - "bytes 1.9.0", + "bytes 1.10.0", "getrandom 0.2.15", "rand 0.8.5", "ring", - "rustc-hash 2.1.0", + "rustc-hash 2.1.1", "rustls 0.23.22", "rustls-pki-types", "slab", @@ -10874,7 +10874,7 @@ dependencies = [ "smol", "sysinfo", "telemetry_events", - "toml 0.8.19", + "toml 0.8.20", "unindent", "util", "worktree", @@ -10947,7 +10947,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ "base64 0.21.7", - "bytes 1.9.0", + "bytes 1.10.0", "encoding_rs", "futures-core", "futures-util", @@ -10990,7 +10990,7 @@ version = "0.12.8" source = "git+https://github.com/zed-industries/reqwest.git?rev=fd110f6998da16bbca97b6dddda9be7827c50e29#fd110f6998da16bbca97b6dddda9be7827c50e29" dependencies = [ "base64 0.22.1", - "bytes 1.9.0", + "bytes 1.10.0", "encoding_rs", "futures-core", "futures-util", @@ -11036,7 +11036,7 @@ name = "reqwest_client" version = "0.1.0" dependencies = [ "anyhow", - "bytes 1.9.0", + "bytes 1.10.0", "futures 0.3.31", "gpui", "http_client", @@ -11118,7 +11118,7 @@ checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" dependencies = [ "bitvec", "bytecheck", - "bytes 1.9.0", + "bytes 1.10.0", "hashbrown 0.12.3", "ptr_meta", "rend", @@ -11249,7 +11249,7 @@ dependencies = [ "async-dispatcher", "async-std", "base64 0.22.1", - "bytes 1.9.0", + "bytes 1.10.0", "chrono", "data-encoding", "dirs 5.0.1", @@ -11308,7 +11308,7 @@ checksum = "b082d80e3e3cc52b2ed634388d436fe1f4de6af5786cc2de9ba9737527bdf555" dependencies = [ "arrayvec", "borsh", - "bytes 1.9.0", + "bytes 1.10.0", "num-traits", "rand 0.8.5", "rkyv", @@ -11330,9 +11330,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc-hash" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustc_version" @@ -12446,7 +12446,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a007b6936676aa9ab40207cde35daab0a04b823be8ae004368c0793b96a61e0" dependencies = [ "bigdecimal", - "bytes 1.9.0", + "bytes 1.10.0", "chrono", "crc", "crossbeam-queue", @@ -12530,7 +12530,7 @@ dependencies = [ "bigdecimal", "bitflags 2.8.0", "byteorder", - "bytes 1.9.0", + "bytes 1.10.0", "chrono", "crc", "digest", @@ -13065,7 +13065,7 @@ dependencies = [ "cfg-expr", "heck 0.5.0", "pkg-config", - "toml 0.8.19", + "toml 0.8.20", "version-compare", ] @@ -13650,7 +13650,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" dependencies = [ "backtrace", - "bytes 1.9.0", + "bytes 1.10.0", "libc", "mio 1.0.3", "parking_lot", @@ -13770,7 +13770,7 @@ version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" dependencies = [ - "bytes 1.9.0", + "bytes 1.10.0", "futures-core", "futures-io", "futures-sink", @@ -13789,9 +13789,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" dependencies = [ "serde", "serde_spanned", @@ -13810,15 +13810,15 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.22" +version = "0.22.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", - "winnow", + "winnow 0.7.1", ] [[package]] @@ -13865,7 +13865,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f873044bf02dd1e8239e9c1293ea39dad76dc594ec16185d0a1bf31d8dc8d858" dependencies = [ "bitflags 1.3.2", - "bytes 1.9.0", + "bytes 1.10.0", "futures-core", "futures-util", "http 0.2.12", @@ -13883,7 +13883,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" dependencies = [ "bitflags 2.8.0", - "bytes 1.9.0", + "bytes 1.10.0", "futures-core", "futures-util", "http 0.2.12", @@ -14234,7 +14234,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" dependencies = [ "byteorder", - "bytes 1.9.0", + "bytes 1.10.0", "data-encoding", "http 0.2.12", "httparse", @@ -14254,7 +14254,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" dependencies = [ "byteorder", - "bytes 1.9.0", + "bytes 1.10.0", "data-encoding", "http 1.2.0", "httparse", @@ -14273,7 +14273,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" dependencies = [ "byteorder", - "bytes 1.9.0", + "bytes 1.10.0", "data-encoding", "http 1.2.0", "httparse", @@ -14775,7 +14775,7 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4378d202ff965b011c64817db11d5829506d3404edeadb61f190d111da3f231c" dependencies = [ - "bytes 1.9.0", + "bytes 1.10.0", "futures-channel", "futures-util", "headers", @@ -15192,7 +15192,7 @@ dependencies = [ "anyhow", "async-trait", "bitflags 2.8.0", - "bytes 1.9.0", + "bytes 1.10.0", "cap-fs-ext", "cap-net-ext", "cap-rand", @@ -15254,9 +15254,9 @@ dependencies = [ [[package]] name = "wayland-backend" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "056535ced7a150d45159d3a8dc30f91a2e2d588ca0b23f70e56033622b8016f6" +checksum = "b7208998eaa3870dad37ec8836979581506e0c5c64c20c9e79e9d2a10d6f47bf" dependencies = [ "cc", "downcast-rs", @@ -15268,9 +15268,9 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.7" +version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66249d3fc69f76fd74c82cc319300faa554e9d865dab1f7cd66cc20db10b280" +checksum = "c2120de3d33638aaef5b9f4472bff75f07c56379cf76ea320bd3a3d65ecaf73f" dependencies = [ "bitflags 2.8.0", "rustix", @@ -15280,9 +15280,9 @@ dependencies = [ [[package]] name = "wayland-cursor" -version = "0.31.7" +version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b08bc3aafdb0035e7fe0fdf17ba0c09c268732707dca4ae098f60cb28c9e4c" +checksum = "a93029cbb6650748881a00e4922b076092a6a08c11e7fbdb923f064b23968c5d" dependencies = [ "rustix", "wayland-client", @@ -15316,20 +15316,20 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.5" +version = "0.31.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597f2001b2e5fc1121e3d5b9791d3e78f05ba6bfa4641053846248e3a13661c3" +checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" dependencies = [ "proc-macro2", - "quick-xml 0.36.2", + "quick-xml 0.37.2", "quote", ] [[package]] name = "wayland-sys" -version = "0.31.5" +version = "0.31.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efa8ac0d8e8ed3e3b5c9fc92c7881406a268e11555abe36493efabe649a29e09" +checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615" dependencies = [ "dlib", "log", @@ -15927,6 +15927,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86e376c75f4f43f44db463cf729e0d3acbf954d13e22c51e26e4c264b4ab545f" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.50.0" @@ -15953,7 +15962,7 @@ version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7276691b353ad4547af8c3268488d1311f4be791ffdc0c65b8cfa8f41eed693b" dependencies = [ - "toml 0.8.19", + "toml 0.8.20", "version_check", ] @@ -16472,7 +16481,7 @@ dependencies = [ "tracing", "uds_windows", "windows-sys 0.59.0", - "winnow", + "winnow 0.6.20", "xdg-home", "zbus_macros 5.1.1", "zbus_names 4.1.0", @@ -16526,7 +16535,7 @@ checksum = "856b7a38811f71846fd47856ceee8bccaec8399ff53fb370247e66081ace647b" dependencies = [ "serde", "static_assertions", - "winnow", + "winnow 0.6.20", "zvariant 5.1.0", ] @@ -16922,7 +16931,7 @@ dependencies = [ "async-std", "async-trait", "asynchronous-codec", - "bytes 1.9.0", + "bytes 1.10.0", "crossbeam-queue", "dashmap 5.5.3", "futures 0.3.31", @@ -17106,7 +17115,7 @@ dependencies = [ "serde", "static_assertions", "url", - "winnow", + "winnow 0.6.20", "zvariant_derive 5.1.0", "zvariant_utils 3.0.2", ] @@ -17159,5 +17168,5 @@ dependencies = [ "serde", "static_assertions", "syn 2.0.90", - "winnow", + "winnow 0.6.20", ] From 146b9c232c3444667c118d3c5513991c63ad4f58 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Fri, 7 Feb 2025 19:17:17 -0700 Subject: [PATCH 12/42] Sort and dedupe .gitignore files (#24491) Release Notes: - N/A --- .gitignore | 47 ++++++++++++++++++------------------- extensions/emmet/.gitignore | 2 +- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/.gitignore b/.gitignore index a8e2e1e8b67699be4ab98f7568886d7a19dcb276..99a6184f564ead8b08e50248e42c945b4328ca52 100644 --- a/.gitignore +++ b/.gitignore @@ -1,36 +1,35 @@ -/.direnv -.envrc -.idea -**/target +**/*.db **/cargo-target -/zed.xcworkspace -.DS_Store -/plugins/bin -/script/node_modules -/crates/theme/schemas/theme.json -/crates/collab/seed.json -/crates/zed/resources/flatpak/flatpak-cargo-sources.json -/dev.zed.Zed*.json -/assets/*licenses.* +**/target **/venv -.build *.wasm -Packages *.xcodeproj -xcuserdata/ -DerivedData/ -.swiftpm/config/registries.json -.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.DS_Store +.blob_store +.build +.envrc +.flatpak-builder +.idea .netrc -.swiftpm -**/*.db .pytest_cache +.swiftpm +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata .venv -.blob_store .vscode .wrangler -.flatpak-builder -.envrc +/.direnv +/assets/*licenses.* +/crates/collab/seed.json +/crates/theme/schemas/theme.json +/crates/zed/resources/flatpak/flatpak-cargo-sources.json +/dev.zed.Zed*.json +/plugins/bin +/script/node_modules +/zed.xcworkspace +DerivedData/ +Packages +xcuserdata/ # Don't commit any secrets to the repo. .env.secret.toml diff --git a/extensions/emmet/.gitignore b/extensions/emmet/.gitignore index 6aba30215ee94f1b5af1ca029da3056fe1062781..62c0add260e0ad28057d36f9575ef66e430a5a20 100644 --- a/extensions/emmet/.gitignore +++ b/extensions/emmet/.gitignore @@ -1,3 +1,3 @@ -target *.wasm grammars +target From 7bddb390cabefb177d9996dc580749d64e6ca3b6 Mon Sep 17 00:00:00 2001 From: 5brian Date: Fri, 7 Feb 2025 21:50:34 -0500 Subject: [PATCH 13/42] vim: Preserve trailing whitespace in inner text object selections (#24481) Closes #24438 Changes: Adjusted loop to only trim whitespace between last newline and closing marker, when using inner objects like `y/d/c i b` | Start | Fixed `vib` | Previous `vib` | | ---------- | ---------- | ---------- | | ![image](https://github.com/user-attachments/assets/3d64dd7d-ed3d-4a85-9f98-f2f83799a738) | ![image](https://github.com/user-attachments/assets/841beb59-31b1-475e-93f0-f4deaf18939c) | ![image](https://github.com/user-attachments/assets/736d4c6f-20e1-4563-9471-1e8195455df4) | Release Notes: - vim: Preserve trailing whitespace in inner text object selections --------- Co-authored-by: Conrad Irwin --- crates/vim/src/object.rs | 24 +++++++++++++++++-- .../test_anybrackets_trailing_space.json | 11 +++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 crates/vim/test_data/test_anybrackets_trailing_space.json diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index 285f79095a904e86cc361bc561e20a22f0b37a5f..eb2999cb7266065c7b6be5dbca30ddc5a4ffd948 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -1339,14 +1339,20 @@ fn surrounding_markers( } } + let mut last_newline_end = None; for (ch, range) in movement::chars_before(map, closing.start) { if !ch.is_whitespace() { break; } - if ch != '\n' { - closing.start = range.start + if ch == '\n' { + last_newline_end = Some(range.end); + break; } } + // Adjust closing.start to exclude whitespace after a newline, if present + if let Some(end) = last_newline_end { + closing.start = end; + } } let result = if around { @@ -2254,6 +2260,20 @@ mod test { } } + #[gpui::test] + async fn test_anybrackets_trailing_space(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("(trailingˇ whitespace )") + .await; + cx.simulate_shared_keystrokes("v i b").await; + cx.shared_state().await.assert_matches(); + cx.simulate_shared_keystrokes("escape y i b").await; + cx.shared_clipboard() + .await + .assert_eq("trailing whitespace "); + } + #[gpui::test] async fn test_tags(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new_html(cx).await; diff --git a/crates/vim/test_data/test_anybrackets_trailing_space.json b/crates/vim/test_data/test_anybrackets_trailing_space.json new file mode 100644 index 0000000000000000000000000000000000000000..ed3f47df6c8c7c9f9249a52bcad9f008f79076d2 --- /dev/null +++ b/crates/vim/test_data/test_anybrackets_trailing_space.json @@ -0,0 +1,11 @@ +{"Put":{"state":"(trailingˇ whitespace )"}} +{"Key":"v"} +{"Key":"i"} +{"Key":"b"} +{"Get":{"state":"(«trailing whitespace ˇ»)","mode":"Visual"}} +{"Key":"escape"} +{"Key":"y"} +{"Key":"i"} +{"Key":"b"} +{"Get":{"state":"(ˇtrailing whitespace )","mode":"Normal"}} +{"ReadRegister":{"name":"\"","value":"trailing whitespace "}} From d9183c7669a0ea12d231a010e676fe9aaf0e263b Mon Sep 17 00:00:00 2001 From: roycrippen4 <54562558+roycrippen4@users.noreply.github.com> Date: Fri, 7 Feb 2025 22:23:10 -0500 Subject: [PATCH 14/42] vim: Escape to normal mode when visual surround operation pending (#24484) Closes #24382 Release Notes: Added a default keymap that returns the user to `normal` mode after pressing escape during a pending `visual-surround` operation. - N/A --------- Co-authored-by: roy.crippen4 Co-authored-by: Conrad Irwin --- assets/keymaps/vim.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index af1822d706aee72f84e73981758c5c7b2c8328e0..aa3e44892c18a30878091cbe9e59c9b616879200 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -381,6 +381,12 @@ "ctrl-q": ["vim::PushLiteral", {}] } }, + { + "context": "Editor && vim_mode == waiting && (vim_operator == ys || vim_operator == cs)", + "bindings": { + "escape": "vim::SwitchToNormalMode" + } + }, { "context": "vim_mode == operator", "bindings": { From ca4e8043d4eb60a5f36fae7da48a7e341363a5ed Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 7 Feb 2025 19:27:58 -0800 Subject: [PATCH 15/42] Add branch to git panel (#24485) This PR adds the branch selector to the git panel and fixes a few bugs in the repository selector. Release Notes: - N/A --------- Co-authored-by: ConradIrwin Co-authored-by: Conrad --- Cargo.lock | 19 +- Cargo.toml | 3 - crates/git_ui/Cargo.toml | 2 + .../lib.rs => git_ui/src/branch_picker.rs} | 123 ++++----- crates/git_ui/src/git_panel.rs | 78 +++--- crates/git_ui/src/git_ui.rs | 2 + crates/git_ui/src/repository_selector.rs | 1 + crates/project/src/git.rs | 28 +- crates/title_bar/src/title_bar.rs | 4 +- crates/ui/src/components/tooltip.rs | 16 ++ crates/vcs_menu/Cargo.toml | 21 -- crates/vcs_menu/LICENSE-GPL | 1 - crates/worktree/Cargo.toml | 6 +- crates/worktree/src/worktree.rs | 258 ++++++++++-------- crates/worktree/src/worktree_tests.rs | 51 ++-- crates/zed/Cargo.toml | 1 - crates/zed/src/main.rs | 1 - crates/zed_actions/src/lib.rs | 6 +- 18 files changed, 309 insertions(+), 312 deletions(-) rename crates/{vcs_menu/src/lib.rs => git_ui/src/branch_picker.rs} (77%) delete mode 100644 crates/vcs_menu/Cargo.toml delete mode 120000 crates/vcs_menu/LICENSE-GPL diff --git a/Cargo.lock b/Cargo.lock index fbd19d934ca5c4ef204ff4648516cbfd1e12d4f1..a3b967c76d2571545e4d9917c7184bbb43a1737d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5330,6 +5330,7 @@ dependencies = [ "editor", "feature_flags", "futures 0.3.31", + "fuzzy", "git", "gpui", "language", @@ -5349,6 +5350,7 @@ dependencies = [ "util", "windows 0.58.0", "workspace", + "zed_actions", ] [[package]] @@ -14616,22 +14618,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "vcs_menu" -version = "0.1.0" -dependencies = [ - "anyhow", - "fuzzy", - "git", - "gpui", - "picker", - "project", - "ui", - "util", - "workspace", - "zed_actions", -] - [[package]] name = "version-compare" version = "0.2.0" @@ -16657,7 +16643,6 @@ dependencies = [ "urlencoding", "util", "uuid", - "vcs_menu", "vim", "vim_mode_setting", "welcome", diff --git a/Cargo.toml b/Cargo.toml index 576f4bb797a0c8a2137b913bc0295b7dc95707a0..ee6a66f909888bb98a2e9adab7eecd6a55e20c3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -147,7 +147,6 @@ members = [ "crates/ui_macros", "crates/util", "crates/util_macros", - "crates/vcs_menu", "crates/vim", "crates/vim_mode_setting", "crates/welcome", @@ -346,7 +345,6 @@ ui_input = { path = "crates/ui_input" } ui_macros = { path = "crates/ui_macros" } util = { path = "crates/util" } util_macros = { path = "crates/util_macros" } -vcs_menu = { path = "crates/vcs_menu" } vim = { path = "crates/vim" } vim_mode_setting = { path = "crates/vim_mode_setting" } welcome = { path = "crates/welcome" } @@ -676,7 +674,6 @@ telemetry_events = { codegen-units = 1 } theme_selector = { codegen-units = 1 } time_format = { codegen-units = 1 } ui_input = { codegen-units = 1 } -vcs_menu = { codegen-units = 1 } zed_actions = { codegen-units = 1 } [profile.release] diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index ad4dbdf9905e40e7667738c591a3ae6b47bc1664..a30792fe1051b2a16f9de3d04297756eefe00cfd 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -20,6 +20,7 @@ diff.workspace = true editor.workspace = true feature_flags.workspace = true futures.workspace = true +fuzzy.workspace = true git.workspace = true gpui.workspace = true language.workspace = true @@ -38,6 +39,7 @@ theme.workspace = true ui.workspace = true util.workspace = true workspace.workspace = true +zed_actions.workspace = true [target.'cfg(windows)'.dependencies] windows.workspace = true diff --git a/crates/vcs_menu/src/lib.rs b/crates/git_ui/src/branch_picker.rs similarity index 77% rename from crates/vcs_menu/src/lib.rs rename to crates/git_ui/src/branch_picker.rs index e4a63d9f0f8ef20030c63ec662245ead1307db2f..bff1c8bf52431cab9746e2558b549de931aefb69 100644 --- a/crates/vcs_menu/src/lib.rs +++ b/crates/git_ui/src/branch_picker.rs @@ -1,27 +1,49 @@ use anyhow::{anyhow, Context as _, Result}; use fuzzy::{StringMatch, StringMatchCandidate}; + use git::repository::Branch; use gpui::{ - rems, AnyElement, App, AsyncApp, Context, DismissEvent, Entity, EventEmitter, FocusHandle, - Focusable, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, - Subscription, Task, WeakEntity, Window, + rems, App, AsyncApp, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, + InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, + Task, WeakEntity, Window, }; use picker::{Picker, PickerDelegate}; use project::ProjectPath; -use std::{ops::Not, sync::Arc}; +use std::sync::Arc; use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing}; use util::ResultExt; use workspace::notifications::DetachAndPromptErr; use workspace::{ModalView, Workspace}; -use zed_actions::branches::OpenRecent; pub fn init(cx: &mut App) { cx.observe_new(|workspace: &mut Workspace, _, _| { - workspace.register_action(BranchList::open); + workspace.register_action(open); }) .detach(); } +pub fn open( + _: &mut Workspace, + _: &zed_actions::git::Branch, + window: &mut Window, + cx: &mut Context, +) { + let this = cx.entity().clone(); + cx.spawn_in(window, |_, mut cx| async move { + // Modal branch picker has a longer trailoff than a popover one. + let delegate = BranchListDelegate::new(this.clone(), 70, &cx).await?; + + this.update_in(&mut cx, |workspace, window, cx| { + workspace.toggle_modal(window, cx, |window, cx| { + BranchList::new(delegate, 34., window, cx) + }) + })?; + + Ok(()) + }) + .detach_and_prompt_err("Failed to read branches", window, cx, |_, _, _| None) +} + pub struct BranchList { pub picker: Entity>, rem_width: f32, @@ -29,29 +51,7 @@ pub struct BranchList { } impl BranchList { - pub fn open( - _: &mut Workspace, - _: &OpenRecent, - window: &mut Window, - cx: &mut Context, - ) { - let this = cx.entity().clone(); - cx.spawn_in(window, |_, mut cx| async move { - // Modal branch picker has a longer trailoff than a popover one. - let delegate = BranchListDelegate::new(this.clone(), 70, &cx).await?; - - this.update_in(&mut cx, |workspace, window, cx| { - workspace.toggle_modal(window, cx, |window, cx| { - BranchList::new(delegate, 34., window, cx) - }) - })?; - - Ok(()) - }) - .detach_and_prompt_err("Failed to read branches", window, cx, |_, _, _| None) - } - - fn new( + pub fn new( delegate: BranchListDelegate, rem_width: f32, window: &mut Window, @@ -91,6 +91,7 @@ impl Render for BranchList { #[derive(Debug, Clone)] enum BranchEntry { Branch(StringMatch), + History(String), NewBranch { name: String }, } @@ -98,6 +99,7 @@ impl BranchEntry { fn name(&self) -> &str { match self { Self::Branch(branch) => &branch.string, + Self::History(branch) => &branch, Self::NewBranch { name } => &name, } } @@ -114,7 +116,7 @@ pub struct BranchListDelegate { } impl BranchListDelegate { - async fn new( + pub async fn new( workspace: Entity, branch_name_trailoff_after: usize, cx: &AsyncApp, @@ -141,7 +143,7 @@ impl BranchListDelegate { }) } - fn branch_count(&self) -> usize { + pub fn branch_count(&self) -> usize { self.matches .iter() .filter(|item| matches!(item, BranchEntry::Branch(_))) @@ -207,16 +209,10 @@ impl PickerDelegate for BranchListDelegate { let Some(candidates) = candidates.log_err() else { return; }; - let matches = if query.is_empty() { + let matches: Vec = if query.is_empty() { candidates .into_iter() - .enumerate() - .map(|(index, candidate)| StringMatch { - candidate_id: index, - string: candidate.string, - positions: Vec::new(), - score: 0.0, - }) + .map(|candidate| BranchEntry::History(candidate.string)) .collect() } else { fuzzy::match_strings( @@ -228,11 +224,15 @@ impl PickerDelegate for BranchListDelegate { cx.background_executor().clone(), ) .await + .iter() + .cloned() + .map(BranchEntry::Branch) + .collect() }; picker .update(&mut cx, |picker, _| { let delegate = &mut picker.delegate; - delegate.matches = matches.into_iter().map(BranchEntry::Branch).collect(); + delegate.matches = matches; if delegate.matches.is_empty() { if !query.is_empty() { delegate.matches.push(BranchEntry::NewBranch { @@ -268,6 +268,7 @@ impl PickerDelegate for BranchListDelegate { let project = workspace.read(cx).project().read(cx); let branch_to_checkout = match branch { BranchEntry::Branch(branch) => branch.string, + BranchEntry::History(string) => string, BranchEntry::NewBranch { name: branch_name } => branch_name, }; let worktree = project @@ -311,7 +312,14 @@ impl PickerDelegate for BranchListDelegate { .inset(true) .spacing(ListItemSpacing::Sparse) .toggle_state(selected) - .map(|parent| match hit { + .when(matches!(hit, BranchEntry::History(_)), |el| { + el.end_slot( + Icon::new(IconName::HistoryRerun) + .color(Color::Muted) + .size(IconSize::Small), + ) + }) + .map(|el| match hit { BranchEntry::Branch(branch) => { let highlights: Vec<_> = branch .positions @@ -320,40 +328,13 @@ impl PickerDelegate for BranchListDelegate { .copied() .collect(); - parent.child(HighlightedLabel::new(shortened_branch_name, highlights)) + el.child(HighlightedLabel::new(shortened_branch_name, highlights)) } + BranchEntry::History(_) => el.child(Label::new(shortened_branch_name)), BranchEntry::NewBranch { name } => { - parent.child(Label::new(format!("Create branch '{name}'"))) + el.child(Label::new(format!("Create branch '{name}'"))) } }), ) } - - fn render_header( - &self, - _window: &mut Window, - _: &mut Context>, - ) -> Option { - let label = if self.last_query.is_empty() { - Label::new("Recent Branches") - .size(LabelSize::Small) - .mt_1() - .ml_3() - .into_any_element() - } else { - let match_label = self.matches.is_empty().not().then(|| { - let suffix = if self.branch_count() == 1 { "" } else { "es" }; - Label::new(format!("{} match{}", self.branch_count(), suffix)) - .color(Color::Muted) - .size(LabelSize::Small) - }); - h_flex() - .px_3() - .justify_between() - .child(Label::new("Branches").size(LabelSize::Small)) - .children(match_label) - .into_any_element() - }; - Some(v_flex().mt_1().child(label).into_any_element()) - } } diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 50ed70b3b40fa13e8a74f7152c97795a448bb255..d7aa40f8d9d6eb2ed0130333292878d518089bb9 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -1110,33 +1110,43 @@ impl GitPanel { .git_state() .read(cx) .all_repositories(); - let entry_count = self + + let branch = self .active_repository .as_ref() - .map_or(0, |repo| repo.read(cx).entry_count()); + .and_then(|repository| repository.read(cx).branch()) + .unwrap_or_else(|| "(no current branch)".into()); + + let has_repo_above = all_repositories.iter().any(|repo| { + repo.read(cx) + .repository_entry + .work_directory + .is_above_project() + }); - let changes_string = match entry_count { - 0 => "No changes".to_string(), - 1 => "1 change".to_string(), - n => format!("{} changes", n), - }; + let icon_button = Button::new("branch-selector", branch) + .color(Color::Muted) + .style(ButtonStyle::Subtle) + .icon(IconName::GitBranch) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .size(ButtonSize::Compact) + .icon_position(IconPosition::Start) + .tooltip(Tooltip::for_action_title( + "Switch Branch", + &zed_actions::git::Branch, + )) + .on_click(cx.listener(|_, _, window, cx| { + window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx); + })) + .style(ButtonStyle::Transparent); self.panel_header_container(window, cx) - .child(h_flex().gap_2().child(if all_repositories.len() <= 1 { - div() - .id("changes-label") - .text_buffer(cx) - .text_ui_sm(cx) - .child( - Label::new(changes_string) - .single_line() - .size(LabelSize::Small), - ) - .into_any_element() - } else { - self.render_repository_selector(cx).into_any_element() - })) + .child(h_flex().pl_1().child(icon_button)) .child(div().flex_grow()) + .when(all_repositories.len() > 1 || has_repo_above, |el| { + el.child(self.render_repository_selector(cx)) + }) } pub fn render_repository_selector(&self, cx: &mut Context) -> impl IntoElement { @@ -1146,35 +1156,11 @@ impl GitPanel { .map(|repo| repo.read(cx).display_name(self.project.read(cx), cx)) .unwrap_or_default(); - let entry_count = self.entries.len(); - RepositorySelectorPopoverMenu::new( self.repository_selector.clone(), ButtonLike::new("active-repository") .style(ButtonStyle::Subtle) - .child( - h_flex().w_full().gap_0p5().child( - div() - .overflow_x_hidden() - .flex_grow() - .whitespace_nowrap() - .child( - h_flex() - .gap_1() - .child( - Label::new(repository_display_name).size(LabelSize::Small), - ) - .when(entry_count > 0, |flex| { - flex.child( - Label::new(format!("({})", entry_count)) - .size(LabelSize::Small) - .color(Color::Muted), - ) - }) - .into_any_element(), - ), - ), - ), + .child(Label::new(repository_display_name).size(LabelSize::Small)), ) } diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index 3757daaf7e64ae46e6c80317adc3e973c9674d38..a8313aa9d5b3cf284f42cdc6ec6e8191c0f10082 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -5,6 +5,7 @@ use gpui::App; use project_diff::ProjectDiff; use ui::{ActiveTheme, Color, Icon, IconName, IntoElement}; +pub mod branch_picker; pub mod git_panel; mod git_panel_settings; pub mod project_diff; @@ -12,6 +13,7 @@ pub mod repository_selector; pub fn init(cx: &mut App) { GitPanelSettings::register(cx); + branch_picker::init(cx); cx.observe_new(ProjectDiff::register).detach(); } diff --git a/crates/git_ui/src/repository_selector.rs b/crates/git_ui/src/repository_selector.rs index 81d5f06635d6a7c387fb8ad44cf1b3d8c47f02d1..ff8cfa406eb94360259eeefd866d439d49ed8ad7 100644 --- a/crates/git_ui/src/repository_selector.rs +++ b/crates/git_ui/src/repository_selector.rs @@ -34,6 +34,7 @@ impl RepositorySelector { let picker = cx.new(|cx| { Picker::nonsearchable_uniform_list(delegate, window, cx) .max_height(Some(rems(20.).into())) + .width(rems(15.)) }); let _subscriptions = diff --git a/crates/project/src/git.rs b/crates/project/src/git.rs index 2c24a63079d9e2e4b19b37f33da335b0c634efdd..debc89b3210e731ad8f94394a8bf2de49c4b2ab2 100644 --- a/crates/project/src/git.rs +++ b/crates/project/src/git.rs @@ -15,7 +15,7 @@ use gpui::{ use language::{Buffer, LanguageRegistry}; use rpc::{proto, AnyProtoClient}; use settings::WorktreeId; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::sync::Arc; use text::BufferId; use util::{maybe, ResultExt}; @@ -299,19 +299,25 @@ impl Repository { (self.worktree_id, self.repository_entry.work_directory_id()) } + pub fn branch(&self) -> Option> { + self.repository_entry.branch() + } + pub fn display_name(&self, project: &Project, cx: &App) -> SharedString { maybe!({ - let path = self.repo_path_to_project_path(&"".into())?; - Some( - project - .absolute_path(&path, cx)? - .file_name()? - .to_string_lossy() - .to_string() - .into(), - ) + let project_path = self.repo_path_to_project_path(&"".into())?; + let worktree_name = project + .worktree_for_id(project_path.worktree_id, cx)? + .read(cx) + .root_name(); + + let mut path = PathBuf::new(); + path = path.join(worktree_name); + path = path.join(project_path.path); + Some(path.to_string_lossy().to_string()) }) - .unwrap_or("".into()) + .unwrap_or_else(|| self.repository_entry.work_directory.display_name()) + .into() } pub fn activate(&self, cx: &mut Context) { diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index cd28890b2e69d7d60217552a891edf235e1d7050..b396e770141f06570a7a338aa5074629be9c9482 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -530,7 +530,7 @@ impl TitleBar { .tooltip(move |window, cx| { Tooltip::with_meta( "Recent Branches", - Some(&zed_actions::branches::OpenRecent), + Some(&zed_actions::git::Branch), "Local branches only", window, cx, @@ -538,7 +538,7 @@ impl TitleBar { }) .on_click(move |_, window, cx| { let _ = workspace.update(cx, |_this, cx| { - window.dispatch_action(zed_actions::branches::OpenRecent.boxed_clone(), cx); + window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx); }); }), ) diff --git a/crates/ui/src/components/tooltip.rs b/crates/ui/src/components/tooltip.rs index 753937810f2e603f8f64b5ec9ef5676a918c6e81..640bb8dc90389442821aa9c23c2b158f43a60757 100644 --- a/crates/ui/src/components/tooltip.rs +++ b/crates/ui/src/components/tooltip.rs @@ -35,6 +35,22 @@ impl Tooltip { } } + pub fn for_action_title( + title: impl Into, + action: &dyn Action, + ) -> impl Fn(&mut Window, &mut App) -> AnyView { + let title = title.into(); + let action = action.boxed_clone(); + move |window, cx| { + cx.new(|_| Self { + title: title.clone(), + meta: None, + key_binding: KeyBinding::for_action(action.as_ref(), window), + }) + .into() + } + } + pub fn for_action( title: impl Into, action: &dyn Action, diff --git a/crates/vcs_menu/Cargo.toml b/crates/vcs_menu/Cargo.toml deleted file mode 100644 index 1e9826d53d3e988611cea980b37954bd399413ce..0000000000000000000000000000000000000000 --- a/crates/vcs_menu/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "vcs_menu" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[dependencies] -anyhow.workspace = true -fuzzy.workspace = true -git.workspace = true -gpui.workspace = true -picker.workspace = true -project.workspace = true -ui.workspace = true -util.workspace = true -workspace.workspace = true -zed_actions.workspace = true diff --git a/crates/vcs_menu/LICENSE-GPL b/crates/vcs_menu/LICENSE-GPL deleted file mode 120000 index 89e542f750cd3860a0598eff0dc34b56d7336dc4..0000000000000000000000000000000000000000 --- a/crates/vcs_menu/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/worktree/Cargo.toml b/crates/worktree/Cargo.toml index 0f76e6e4bbbfd503fd243f89fe59ef614884d4a1..8630f2019449faddace855728a0611f9e877c5e2 100644 --- a/crates/worktree/Cargo.toml +++ b/crates/worktree/Cargo.toml @@ -14,11 +14,12 @@ workspace = true [features] test-support = [ + "gpui/test-support", + "http_client/test-support", "language/test-support", "settings/test-support", "text/test-support", - "gpui/test-support", - "http_client/test-support", + "util/test-support", ] [dependencies] @@ -59,3 +60,4 @@ pretty_assertions.workspace = true rand.workspace = true rpc = { workspace = true, features = ["test-support"] } settings = { workspace = true, features = ["test-support"] } +util = { workspace = true, features = ["test-support"] } diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index ed559eea177d852b54324f6c7a54e50973671a8b..eee87f3cc51d199b1197f1e5a82c96ced3c9404c 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -213,12 +213,6 @@ impl Deref for RepositoryEntry { } } -impl AsRef for RepositoryEntry { - fn as_ref(&self) -> &Path { - &self.path - } -} - impl RepositoryEntry { pub fn branch(&self) -> Option> { self.branch.clone() @@ -326,33 +320,53 @@ impl RepositoryEntry { /// But if a sub-folder of a git repository is opened, this corresponds to the /// project root and the .git folder is located in a parent directory. #[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)] -pub struct WorkDirectory { - path: Arc, - - /// If location_in_repo is set, it means the .git folder is external - /// and in a parent folder of the project root. - /// In that case, the work_directory field will point to the - /// project-root and location_in_repo contains the location of the - /// project-root in the repository. - /// - /// Example: - /// - /// my_root_folder/ <-- repository root - /// .git - /// my_sub_folder_1/ - /// project_root/ <-- Project root, Zed opened here - /// ... - /// - /// For this setup, the attributes will have the following values: - /// - /// work_directory: pointing to "" entry - /// location_in_repo: Some("my_sub_folder_1/project_root") - pub(crate) location_in_repo: Option>, +pub enum WorkDirectory { + InProject { + relative_path: Arc, + }, + AboveProject { + absolute_path: Arc, + location_in_repo: Arc, + }, } impl WorkDirectory { - pub fn path_key(&self) -> PathKey { - PathKey(self.path.clone()) + #[cfg(test)] + fn in_project(path: &str) -> Self { + let path = Path::new(path); + Self::InProject { + relative_path: path.into(), + } + } + + #[cfg(test)] + fn canonicalize(&self) -> Self { + match self { + WorkDirectory::InProject { relative_path } => WorkDirectory::InProject { + relative_path: relative_path.clone(), + }, + WorkDirectory::AboveProject { + absolute_path, + location_in_repo, + } => WorkDirectory::AboveProject { + absolute_path: absolute_path.canonicalize().unwrap().into(), + location_in_repo: location_in_repo.clone(), + }, + } + } + + pub fn is_above_project(&self) -> bool { + match self { + WorkDirectory::InProject { .. } => false, + WorkDirectory::AboveProject { .. } => true, + } + } + + fn path_key(&self) -> PathKey { + match self { + WorkDirectory::InProject { relative_path } => PathKey(relative_path.clone()), + WorkDirectory::AboveProject { .. } => PathKey(Path::new("").into()), + } } /// Returns true if the given path is a child of the work directory. @@ -360,9 +374,14 @@ impl WorkDirectory { /// Note that the path may not be a member of this repository, if there /// is a repository in a directory between these two paths /// external .git folder in a parent folder of the project root. + #[track_caller] pub fn directory_contains(&self, path: impl AsRef) -> bool { let path = path.as_ref(); - path.starts_with(&self.path) + debug_assert!(path.is_relative()); + match self { + WorkDirectory::InProject { relative_path } => path.starts_with(relative_path), + WorkDirectory::AboveProject { .. } => true, + } } /// relativize returns the given project path relative to the root folder of the @@ -371,53 +390,71 @@ impl WorkDirectory { /// of the project root folder, then the returned RepoPath is relative to the root /// of the repository and not a valid path inside the project. pub fn relativize(&self, path: &Path) -> Result { - let repo_path = if let Some(location_in_repo) = &self.location_in_repo { - // Avoid joining a `/` to location_in_repo in the case of a single-file worktree. - if path == Path::new("") { - RepoPath(location_in_repo.clone()) - } else { - location_in_repo.join(path).into() + // path is assumed to be relative to worktree root. + debug_assert!(path.is_relative()); + match self { + WorkDirectory::InProject { relative_path } => Ok(path + .strip_prefix(relative_path) + .map_err(|_| { + anyhow!( + "could not relativize {:?} against {:?}", + path, + relative_path + ) + })? + .into()), + WorkDirectory::AboveProject { + location_in_repo, .. + } => { + // Avoid joining a `/` to location_in_repo in the case of a single-file worktree. + if path == Path::new("") { + Ok(RepoPath(location_in_repo.clone())) + } else { + Ok(location_in_repo.join(path).into()) + } } - } else { - path.strip_prefix(&self.path) - .map_err(|_| anyhow!("could not relativize {:?} against {:?}", path, self.path))? - .into() - }; - Ok(repo_path) + } } /// This is the opposite operation to `relativize` above pub fn unrelativize(&self, path: &RepoPath) -> Option> { - if let Some(location) = &self.location_in_repo { - // If we fail to strip the prefix, that means this status entry is - // external to this worktree, and we definitely won't have an entry_id - path.strip_prefix(location).ok().map(Into::into) - } else { - Some(self.path.join(path).into()) + match self { + WorkDirectory::InProject { relative_path } => Some(relative_path.join(path).into()), + WorkDirectory::AboveProject { + location_in_repo, .. + } => { + // If we fail to strip the prefix, that means this status entry is + // external to this worktree, and we definitely won't have an entry_id + path.strip_prefix(location_in_repo).ok().map(Into::into) + } } } -} -impl Default for WorkDirectory { - fn default() -> Self { - Self { - path: Arc::from(Path::new("")), - location_in_repo: None, + pub fn display_name(&self) -> String { + match self { + WorkDirectory::InProject { relative_path } => relative_path.display().to_string(), + WorkDirectory::AboveProject { + absolute_path, + location_in_repo, + } => { + let num_of_dots = location_in_repo.components().count(); + + "../".repeat(num_of_dots) + + &absolute_path + .file_name() + .map(|s| s.to_string_lossy()) + .unwrap_or_default() + + "/" + } } } } -impl Deref for WorkDirectory { - type Target = Path; - - fn deref(&self) -> &Self::Target { - self.as_ref() - } -} - -impl AsRef for WorkDirectory { - fn as_ref(&self) -> &Path { - self.path.as_ref() +impl Default for WorkDirectory { + fn default() -> Self { + Self::InProject { + relative_path: Arc::from(Path::new("")), + } } } @@ -487,7 +524,7 @@ impl sum_tree::Item for LocalRepositoryEntry { fn summary(&self, _: &::Context) -> Self::Summary { PathSummary { - max_path: self.work_directory.path.clone(), + max_path: self.work_directory.path_key().0, item_summary: Unit, } } @@ -497,7 +534,7 @@ impl KeyedItem for LocalRepositoryEntry { type Key = PathKey; fn key(&self) -> Self::Key { - PathKey(self.work_directory.path.clone()) + self.work_directory.path_key() } } @@ -2574,12 +2611,11 @@ impl Snapshot { self.repositories.insert_or_replace( RepositoryEntry { work_directory_id, - work_directory: WorkDirectory { - path: work_dir_entry.path.clone(), - // When syncing repository entries from a peer, we don't need - // the location_in_repo field, since git operations don't happen locally - // anyway. - location_in_repo: None, + // When syncing repository entries from a peer, we don't need + // the location_in_repo field, since git operations don't happen locally + // anyway. + work_directory: WorkDirectory::InProject { + relative_path: work_dir_entry.path.clone(), }, branch: repository.branch.map(Into::into), statuses_by_path: statuses, @@ -2690,23 +2726,13 @@ impl Snapshot { &self.repositories } - pub fn repositories_with_abs_paths( - &self, - ) -> impl '_ + Iterator { - let base = self.abs_path(); - self.repositories.iter().map(|repo| { - let path = repo.work_directory.location_in_repo.as_deref(); - let path = path.unwrap_or(repo.work_directory.as_ref()); - (repo, base.join(path)) - }) - } - /// Get the repository whose work directory corresponds to the given path. pub(crate) fn repository(&self, work_directory: PathKey) -> Option { self.repositories.get(&work_directory, &()).cloned() } /// Get the repository whose work directory contains the given path. + #[track_caller] pub fn repository_for_path(&self, path: &Path) -> Option<&RepositoryEntry> { self.repositories .iter() @@ -2716,6 +2742,7 @@ impl Snapshot { /// Given an ordered iterator of entries, returns an iterator of those entries, /// along with their containing git repository. + #[track_caller] pub fn entries_with_repositories<'a>( &'a self, entries: impl 'a + Iterator, @@ -3081,7 +3108,7 @@ impl LocalSnapshot { let work_dir_paths = self .repositories .iter() - .map(|repo| repo.work_directory.path.clone()) + .map(|repo| repo.work_directory.path_key()) .collect::>(); assert_eq!(dotgit_paths.len(), work_dir_paths.len()); assert_eq!(self.repositories.iter().count(), work_dir_paths.len()); @@ -3289,7 +3316,7 @@ impl BackgroundScannerState { .git_repositories .retain(|id, _| removed_ids.binary_search(id).is_err()); self.snapshot.repositories.retain(&(), |repository| { - !repository.work_directory.starts_with(path) + !repository.work_directory.path_key().0.starts_with(path) }); #[cfg(test)] @@ -3327,20 +3354,26 @@ impl BackgroundScannerState { } }; - self.insert_git_repository_for_path(work_dir_path, dot_git_path, None, fs, watcher) + self.insert_git_repository_for_path( + WorkDirectory::InProject { + relative_path: work_dir_path, + }, + dot_git_path, + fs, + watcher, + ) } fn insert_git_repository_for_path( &mut self, - work_dir_path: Arc, + work_directory: WorkDirectory, dot_git_path: Arc, - location_in_repo: Option>, fs: &dyn Fs, watcher: &dyn Watcher, ) -> Option { let work_dir_id = self .snapshot - .entry_for_path(work_dir_path.clone()) + .entry_for_path(work_directory.path_key().0) .map(|entry| entry.id)?; if self.snapshot.git_repositories.get(&work_dir_id).is_some() { @@ -3374,10 +3407,6 @@ impl BackgroundScannerState { }; log::trace!("constructed libgit2 repo in {:?}", t0.elapsed()); - let work_directory = WorkDirectory { - path: work_dir_path.clone(), - location_in_repo, - }; if let Some(git_hosting_provider_registry) = self.git_hosting_provider_registry.clone() { git_hosting_providers::register_additional_providers( @@ -3840,7 +3869,7 @@ impl sum_tree::Item for RepositoryEntry { fn summary(&self, _: &::Context) -> Self::Summary { PathSummary { - max_path: self.work_directory.path.clone(), + max_path: self.work_directory.path_key().0, item_summary: Unit, } } @@ -3850,7 +3879,7 @@ impl sum_tree::KeyedItem for RepositoryEntry { type Key = PathKey; fn key(&self) -> Self::Key { - PathKey(self.work_directory.path.clone()) + self.work_directory.path_key() } } @@ -4089,7 +4118,7 @@ impl<'a> sum_tree::Dimension<'a, PathEntrySummary> for ProjectEntryId { } } -#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] +#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct PathKey(Arc); impl Default for PathKey { @@ -4168,15 +4197,15 @@ impl BackgroundScanner { // We associate the external git repo with our root folder and // also mark where in the git repo the root folder is located. self.state.lock().insert_git_repository_for_path( - Path::new("").into(), - ancestor_dot_git.into(), - Some( - root_abs_path + WorkDirectory::AboveProject { + absolute_path: ancestor.into(), + location_in_repo: root_abs_path .as_path() .strip_prefix(ancestor) .unwrap() .into(), - ), + }, + ancestor_dot_git.into(), self.fs.as_ref(), self.watcher.as_ref(), ); @@ -4385,13 +4414,6 @@ impl BackgroundScanner { dot_git_abs_paths.push(dot_git_abs_path); } } - if abs_path.0.file_name() == Some(*GITIGNORE) { - for (_, repo) in snapshot.git_repositories.iter().filter(|(_, repo)| repo.directory_contains(&abs_path.0)) { - if !dot_git_abs_paths.iter().any(|dot_git_abs_path| dot_git_abs_path == repo.dot_git_dir_abs_path.as_ref()) { - dot_git_abs_paths.push(repo.dot_git_dir_abs_path.to_path_buf()); - } - } - } let relative_path: Arc = if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) { @@ -4409,6 +4431,14 @@ impl BackgroundScanner { return false; }; + if abs_path.0.file_name() == Some(*GITIGNORE) { + for (_, repo) in snapshot.git_repositories.iter().filter(|(_, repo)| repo.directory_contains(&relative_path)) { + if !dot_git_abs_paths.iter().any(|dot_git_abs_path| dot_git_abs_path == repo.dot_git_dir_abs_path.as_ref()) { + dot_git_abs_paths.push(repo.dot_git_dir_abs_path.to_path_buf()); + } + } + } + let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| { snapshot .entry_for_path(parent) @@ -4992,7 +5022,7 @@ impl BackgroundScanner { snapshot .snapshot .repositories - .remove(&PathKey(repository.work_directory.path.clone()), &()); + .remove(&repository.work_directory.path_key(), &()); return Some(()); } } @@ -5286,7 +5316,7 @@ impl BackgroundScanner { fn update_git_statuses(&self, job: UpdateGitStatusesJob) { log::trace!( "updating git statuses for repo {:?}", - job.local_repository.work_directory.path + job.local_repository.work_directory.display_name() ); let t0 = Instant::now(); @@ -5300,7 +5330,7 @@ impl BackgroundScanner { }; log::trace!( "computed git statuses for repo {:?} in {:?}", - job.local_repository.work_directory.path, + job.local_repository.work_directory.display_name(), t0.elapsed() ); @@ -5364,7 +5394,7 @@ impl BackgroundScanner { log::trace!( "applied git status updates for repo {:?} in {:?}", - job.local_repository.work_directory.path, + job.local_repository.work_directory.display_name(), t0.elapsed(), ); } diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index 2cee728aec89e40500700c182ed617400085739e..f4e6da23455c5b10f44a337c2f011a80656e534d 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -1,6 +1,6 @@ use crate::{ - worktree_settings::WorktreeSettings, Entry, EntryKind, Event, PathChange, Snapshot, Worktree, - WorktreeModelHandle, + worktree_settings::WorktreeSettings, Entry, EntryKind, Event, PathChange, Snapshot, + WorkDirectory, Worktree, WorktreeModelHandle, }; use anyhow::Result; use fs::{FakeFs, Fs, RealFs, RemoveOptions}; @@ -2200,7 +2200,10 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) { cx.read(|cx| { let tree = tree.read(cx); let repo = tree.repositories().iter().next().unwrap(); - assert_eq!(repo.path.as_ref(), Path::new("projects/project1")); + assert_eq!( + repo.work_directory, + WorkDirectory::in_project("projects/project1") + ); assert_eq!( tree.status_for_file(Path::new("projects/project1/a")), Some(StatusCode::Modified.worktree()), @@ -2221,7 +2224,10 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) { cx.read(|cx| { let tree = tree.read(cx); let repo = tree.repositories().iter().next().unwrap(); - assert_eq!(repo.path.as_ref(), Path::new("projects/project2")); + assert_eq!( + repo.work_directory, + WorkDirectory::in_project("projects/project2") + ); assert_eq!( tree.status_for_file(Path::new("projects/project2/a")), Some(StatusCode::Modified.worktree()), @@ -2275,12 +2281,15 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) { assert!(tree.repository_for_path("c.txt".as_ref()).is_none()); let repo = tree.repository_for_path("dir1/src/b.txt".as_ref()).unwrap(); - assert_eq!(repo.path.as_ref(), Path::new("dir1")); + assert_eq!(repo.work_directory, WorkDirectory::in_project("dir1")); let repo = tree .repository_for_path("dir1/deps/dep1/src/a.txt".as_ref()) .unwrap(); - assert_eq!(repo.path.as_ref(), Path::new("dir1/deps/dep1")); + assert_eq!( + repo.work_directory, + WorkDirectory::in_project("dir1/deps/dep1") + ); let entries = tree.files(false, 0); @@ -2289,7 +2298,7 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) { .map(|(entry, repo)| { ( entry.path.as_ref(), - repo.map(|repo| repo.path.to_path_buf()), + repo.map(|repo| repo.work_directory.clone()), ) }) .collect::>(); @@ -2300,9 +2309,12 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) { (Path::new("c.txt"), None), ( Path::new("dir1/deps/dep1/src/a.txt"), - Some(Path::new("dir1/deps/dep1").into()) + Some(WorkDirectory::in_project("dir1/deps/dep1")) + ), + ( + Path::new("dir1/src/b.txt"), + Some(WorkDirectory::in_project("dir1")) ), - (Path::new("dir1/src/b.txt"), Some(Path::new("dir1").into())), ] ); }); @@ -2408,8 +2420,10 @@ async fn test_file_status(cx: &mut TestAppContext) { let snapshot = tree.snapshot(); assert_eq!(snapshot.repositories().iter().count(), 1); let repo_entry = snapshot.repositories().iter().next().unwrap(); - assert_eq!(repo_entry.path.as_ref(), Path::new("project")); - assert!(repo_entry.location_in_repo.is_none()); + assert_eq!( + repo_entry.work_directory, + WorkDirectory::in_project("project") + ); assert_eq!( snapshot.status_for_file(project_path.join(B_TXT)), @@ -2760,15 +2774,14 @@ async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) { let snapshot = tree.snapshot(); assert_eq!(snapshot.repositories().iter().count(), 1); let repo = snapshot.repositories().iter().next().unwrap(); - // Path is blank because the working directory of - // the git repository is located at the root of the project - assert_eq!(repo.path.as_ref(), Path::new("")); - - // This is the missing path between the root of the project (sub-folder-2) and its - // location relative to the root of the repository. assert_eq!( - repo.location_in_repo, - Some(Arc::from(Path::new("sub-folder-1/sub-folder-2"))) + repo.work_directory.canonicalize(), + WorkDirectory::AboveProject { + absolute_path: Arc::from(root.path().join("my-repo").canonicalize().unwrap()), + location_in_repo: Arc::from(Path::new(util::separator!( + "sub-folder-1/sub-folder-2" + ))) + } ); assert_eq!(snapshot.status_for_file("c.txt"), None); diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 5677203d1d6c4825f727a373dd2d4973b178d928..6106a382e17cc9c4a42a7a2a802090e5dd28705d 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -126,7 +126,6 @@ url.workspace = true urlencoding = "2.1.2" util.workspace = true uuid.workspace = true -vcs_menu.workspace = true vim.workspace = true vim_mode_setting.workspace = true welcome.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 019af54c541a0d42b90de72c19af624b9d7c26ed..78cd4d19cdd8315c175bb4611b9bc0b0165952e0 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -505,7 +505,6 @@ fn main() { notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx); collab_ui::init(&app_state, cx); git_ui::init(cx); - vcs_menu::init(cx); feedback::init(cx); markdown_preview::init(cx); welcome::init(cx); diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 2299bf58bc2c7bd537f7af5bd65e1a6eec3ab870..08ec86afa09fe42edc6e4affbce0e917cea450ec 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -47,10 +47,10 @@ actions!( ] ); -pub mod branches { - use gpui::actions; +pub mod git { + use gpui::action_with_deprecated_aliases; - actions!(branches, [OpenRecent]); + action_with_deprecated_aliases!(git, Branch, ["branches::OpenRecent"]); } pub mod command_palette { From 3582fc463663af00b2d0f9848cc5df35834a6974 Mon Sep 17 00:00:00 2001 From: Sanjeev Shrestha Date: Sat, 8 Feb 2025 17:18:01 +0545 Subject: [PATCH 16/42] File icons add icon association for Prettier config (#24496) This PR adds icon association for more Prettier's config files. Here is the list: ``` .prettierrc.cjs .prettierrc.js .prettierrc.json5 .prettierrc.mjs .prettierrc.toml .prettierrc.yaml .prettierrc.yml prettier.config.cjs prettier.config.js prettier.config.mjs ``` Release Notes: - Added icon support for additional Prettier config file types. --- assets/icons/file_icons/file_types.json | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/assets/icons/file_icons/file_types.json b/assets/icons/file_icons/file_types.json index f36ef2737ff4d246b8b73160e93a88a9902787ca..63bf22e8c6a43335a83893d44d470ae74ad39136 100644 --- a/assets/icons/file_icons/file_types.json +++ b/assets/icons/file_icons/file_types.json @@ -152,6 +152,17 @@ "pptx": "document", "prettierignore": "prettier", "prettierrc": "prettier", + "prettierrc.cjs": "prettier", + "prettierrc.json": "prettier", + "prettierrc.js": "prettier", + "prettierrc.json5": "prettier", + "prettierrc.mjs": "prettier", + "prettierrc.toml": "prettier", + "prettierrc.yaml": "prettier", + "prettierrc.yml": "prettier", + "prettier.config.cjs": "prettier", + "prettier.config.js": "prettier", + "prettier.config.mjs": "prettier", "prisma": "prisma", "profile": "terminal", "ps1": "terminal", From b1055878c7065795eb4aa550afd4de9cc85b8d2b Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 8 Feb 2025 14:33:47 +0200 Subject: [PATCH 17/42] Improve outline panel initial update (#24500) Closes https://github.com/zed-industries/zed/issues/24128 * removed unnecessary debounces when updating the panel data * removed all "loading"-related messages to snow nothing when initial data is loaded, thus reducing flickering Release Notes: - Improved outline panel initial update --- crates/outline_panel/src/outline_panel.rs | 130 ++++++++++++++-------- 1 file changed, 82 insertions(+), 48 deletions(-) diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 212d13555f3e580db9d66bc21296aa23aed62ed1..d1be0368228cd1578977605eafd88cda5869c2f2 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -5,7 +5,10 @@ use std::{ hash::Hash, ops::Range, path::{Path, PathBuf, MAIN_SEPARATOR_STR}, - sync::{atomic::AtomicBool, Arc, OnceLock}, + sync::{ + atomic::{self, AtomicBool}, + Arc, OnceLock, + }, time::Duration, u32, }; @@ -103,6 +106,7 @@ pub struct OutlinePanel { active_item: Option, _subscriptions: Vec, updating_fs_entries: bool, + updating_cached_entries: bool, new_entries_for_fs_update: HashSet, fs_entries_update_task: Task<()>, cached_entries_update_task: Task<()>, @@ -777,7 +781,10 @@ impl OutlinePanel { excerpt.invalidate_outlines(); } } - outline_panel.update_non_fs_items(window, cx); + let update_cached_items = outline_panel.update_non_fs_items(window, cx); + if update_cached_items { + outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx); + } } else if &outline_panel_settings != new_settings { outline_panel_settings = *new_settings; cx.notify(); @@ -814,6 +821,7 @@ impl OutlinePanel { active_item: None, pending_serialization: Task::ready(None), updating_fs_entries: false, + updating_cached_entries: false, new_entries_for_fs_update: HashSet::default(), preserve_selection_on_buffer_fold_toggles: HashSet::default(), fs_entries_update_task: Task::ready(()), @@ -2896,8 +2904,8 @@ impl OutlinePanel { outline_panel.fs_entries = new_fs_entries; outline_panel.fs_entries_depth = new_depth_map; outline_panel.fs_children_count = new_children_count; - outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx); outline_panel.update_non_fs_items(window, cx); + outline_panel.update_cached_entries(debounce, window, cx); cx.notify(); }) @@ -2922,7 +2930,11 @@ impl OutlinePanel { window: &mut Window, cx: &mut Context| { if matches!(e, SearchEvent::MatchesInvalidated) { - outline_panel.update_search_matches(window, cx); + let update_cached_items = outline_panel.update_search_matches(window, cx); + if update_cached_items { + outline_panel.selected_entry.invalidate(); + outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx); + } }; outline_panel.autoscroll(cx); }, @@ -3188,10 +3200,12 @@ impl OutlinePanel { } let syntax_theme = cx.theme().syntax().clone(); + let first_update = Arc::new(AtomicBool::new(true)); for (buffer_id, (buffer_snapshot, excerpt_ranges)) in excerpt_fetch_ranges { for (excerpt_id, excerpt_range) in excerpt_ranges { let syntax_theme = syntax_theme.clone(); let buffer_snapshot = buffer_snapshot.clone(); + let first_update = first_update.clone(); self.outline_fetch_tasks.insert( (buffer_id, excerpt_id), cx.spawn_in(window, |outline_panel, mut cx| async move { @@ -3215,13 +3229,16 @@ impl OutlinePanel { .or_default() .get_mut(&excerpt_id) { + let debounce = if first_update + .fetch_and(false, atomic::Ordering::AcqRel) + { + None + } else { + Some(UPDATE_DEBOUNCE) + }; excerpt.outlines = ExcerptOutlines::Outlines(fetched_outlines); + outline_panel.update_cached_entries(debounce, window, cx); } - outline_panel.update_cached_entries( - Some(UPDATE_DEBOUNCE), - window, - cx, - ); }) .ok(); }), @@ -3376,6 +3393,7 @@ impl OutlinePanel { let is_singleton = self.is_singleton_active(cx); let query = self.query(cx); + self.updating_cached_entries = true; self.cached_entries_update_task = cx.spawn_in(window, |outline_panel, mut cx| async move { if let Some(debounce) = debounce { cx.background_executor().timer(debounce).await; @@ -3410,6 +3428,7 @@ impl OutlinePanel { } outline_panel.autoscroll(cx); + outline_panel.updating_cached_entries = false; cx.notify(); }) .ok(); @@ -3915,19 +3934,27 @@ impl OutlinePanel { !self.collapsed_entries.contains(&entry_to_check) } - fn update_non_fs_items(&mut self, window: &mut Window, cx: &mut Context) { + fn update_non_fs_items(&mut self, window: &mut Window, cx: &mut Context) -> bool { if !self.active { - return; + return false; } - self.update_search_matches(window, cx); + let mut update_cached_items = false; + update_cached_items |= self.update_search_matches(window, cx); self.fetch_outdated_outlines(window, cx); - self.autoscroll(cx); + if update_cached_items { + self.selected_entry.invalidate(); + } + update_cached_items } - fn update_search_matches(&mut self, window: &mut Window, cx: &mut Context) { + fn update_search_matches( + &mut self, + window: &mut Window, + cx: &mut Context, + ) -> bool { if !self.active { - return; + return false; } let project_search = self @@ -4010,10 +4037,7 @@ impl OutlinePanel { cx, )); } - if update_cached_entries { - self.selected_entry.invalidate(); - self.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx); - } + update_cached_entries } #[allow(clippy::too_many_arguments)] @@ -4426,41 +4450,42 @@ impl OutlinePanel { cx: &mut Context, ) -> Div { let contents = if self.cached_entries.is_empty() { - let header = if self.updating_fs_entries { - "Loading outlines" + let header = if self.updating_fs_entries || self.updating_cached_entries { + None } else if query.is_some() { - "No matches for query" + Some("No matches for query") } else { - "No outlines available" + Some("No outlines available") }; v_flex() .flex_1() .justify_center() .size_full() - .child(h_flex().justify_center().child(Label::new(header))) - .when_some(query.clone(), |panel, query| { - panel.child(h_flex().justify_center().child(Label::new(query))) + .when_some(header, |panel, header| { + panel + .child(h_flex().justify_center().child(Label::new(header))) + .when_some(query.clone(), |panel, query| { + panel.child(h_flex().justify_center().child(Label::new(query))) + }) + .child( + h_flex() + .pt(DynamicSpacing::Base04.rems(cx)) + .justify_center() + .child({ + let keystroke = + match self.position(window, cx) { + DockPosition::Left => window + .keystroke_text_for(&workspace::ToggleLeftDock), + DockPosition::Bottom => window + .keystroke_text_for(&workspace::ToggleBottomDock), + DockPosition::Right => window + .keystroke_text_for(&workspace::ToggleRightDock), + }; + Label::new(format!("Toggle this panel with {keystroke}")) + }), + ) }) - .child( - h_flex() - .pt(DynamicSpacing::Base04.rems(cx)) - .justify_center() - .child({ - let keystroke = match self.position(window, cx) { - DockPosition::Left => { - window.keystroke_text_for(&workspace::ToggleLeftDock) - } - DockPosition::Bottom => { - window.keystroke_text_for(&workspace::ToggleBottomDock) - } - DockPosition::Right => { - window.keystroke_text_for(&workspace::ToggleRightDock) - } - }; - Label::new(format!("Toggle this panel with {keystroke}")) - }), - ) } else { let list_contents = { let items_len = self.cached_entries.len(); @@ -4995,11 +5020,17 @@ fn subscribe_for_editor_events( } EditorEvent::ExcerptsExpanded { ids } => { outline_panel.invalidate_outlines(ids); - outline_panel.update_non_fs_items(window, cx); + let update_cached_items = outline_panel.update_non_fs_items(window, cx); + if update_cached_items { + outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx); + } } EditorEvent::ExcerptsEdited { ids } => { outline_panel.invalidate_outlines(ids); - outline_panel.update_non_fs_items(window, cx); + let update_cached_items = outline_panel.update_non_fs_items(window, cx); + if update_cached_items { + outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx); + } } EditorEvent::BufferFoldToggled { ids, .. } => { outline_panel.invalidate_outlines(ids); @@ -5073,7 +5104,10 @@ fn subscribe_for_editor_events( excerpt.invalidate_outlines(); } } - outline_panel.update_non_fs_items(window, cx); + let update_cached_items = outline_panel.update_non_fs_items(window, cx); + if update_cached_items { + outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx); + } } _ => {} } From 0294b19694013a4e6870c91d71ad2ff3eec09bb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Marcos?= Date: Sat, 8 Feb 2025 12:29:29 -0300 Subject: [PATCH 18/42] Track caller on `::to_offset` (#24503) To get useful logs when reporting bugs involving offsets out of range Release Notes: - N/A --- crates/gpui/src/arena.rs | 1 + crates/multi_buffer/src/multi_buffer.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/crates/gpui/src/arena.rs b/crates/gpui/src/arena.rs index 4ddeaaff65eb6247595fd15a263a985a01d2ae23..0e78feca7bfa4c514f233415ba3e4f6f323a6655 100644 --- a/crates/gpui/src/arena.rs +++ b/crates/gpui/src/arena.rs @@ -116,6 +116,7 @@ impl ArenaBox { } } + #[track_caller] fn validate(&self) { assert!( self.valid.get(), diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 9c80fcedd3ebbaf443d1cce3c5c7ae81a59763d5..0e8359412a8d0a4eeb047ad3fcefe112e035f85f 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -7320,6 +7320,7 @@ impl ToOffset for Point { } impl ToOffset for usize { + #[track_caller] fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> usize { assert!(*self <= snapshot.len(), "offset is out of range"); *self From fe6d180a1ab89159bb1d6405349b4f255cbbfb2d Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Sat, 8 Feb 2025 11:11:31 -0500 Subject: [PATCH 19/42] Sort Prettier files in `file_types.json` (#24505) This PR sorts the Prettier files added in #24496. Release Notes: - N/A --- assets/icons/file_icons/file_types.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/icons/file_icons/file_types.json b/assets/icons/file_icons/file_types.json index 63bf22e8c6a43335a83893d44d470ae74ad39136..ffed3d680dd6a5686c6963c4074e7f2bff382fd7 100644 --- a/assets/icons/file_icons/file_types.json +++ b/assets/icons/file_icons/file_types.json @@ -150,19 +150,19 @@ "postcss": "css", "ppt": "document", "pptx": "document", + "prettier.config.cjs": "prettier", + "prettier.config.js": "prettier", + "prettier.config.mjs": "prettier", "prettierignore": "prettier", "prettierrc": "prettier", "prettierrc.cjs": "prettier", - "prettierrc.json": "prettier", "prettierrc.js": "prettier", + "prettierrc.json": "prettier", "prettierrc.json5": "prettier", "prettierrc.mjs": "prettier", "prettierrc.toml": "prettier", "prettierrc.yaml": "prettier", "prettierrc.yml": "prettier", - "prettier.config.cjs": "prettier", - "prettier.config.js": "prettier", - "prettier.config.mjs": "prettier", "prisma": "prisma", "profile": "terminal", "ps1": "terminal", From 4207b194e35e87edc56a8d045aa5711b29ab8859 Mon Sep 17 00:00:00 2001 From: Affan Shahid Date: Sun, 9 Feb 2025 04:35:43 +0500 Subject: [PATCH 20/42] docs: Fix typo in the Icon Themes page (#24516) Release Notes: - N/A --- docs/src/extensions/icon-themes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/extensions/icon-themes.md b/docs/src/extensions/icon-themes.md index c8321e19388360b495e4ac62fd3fe569f4f23a48..56d50213f5bc7fb5780e9627fe20bb26acb435d8 100644 --- a/docs/src/extensions/icon-themes.md +++ b/docs/src/extensions/icon-themes.md @@ -30,7 +30,7 @@ Here is an example of the structure of an icon theme: "collapsed": "./icons/folder.svg", "expanded": "./icons/folder-open.svg" }, - "chevon_icons": { + "chevron_icons": { "collapsed": "./icons/chevron-right.svg", "expanded": "./icons/chevron-down.svg" }, From f1693e6129adcd31ccef31c49e24eede090ca14f Mon Sep 17 00:00:00 2001 From: smit <0xtimsb@gmail.com> Date: Sun, 9 Feb 2025 14:16:27 +0530 Subject: [PATCH 21/42] project_panel: Fix worktree root rename (#24487) Closes #7923 This PR fixes root worktree renaming by: 1. Handling the case where `new_path` is the new root name instead of a relative path from the root. 2. [#20313](https://github.com/zed-industries/zed/pull/20313) added functionality to watch for root worktree renames made externally, e.g., via Finder. This PR avoids relying on that watcher because, when renaming explicitly from Zed, we can eagerly perform the necessary work (of course after fs rename) instead of waiting for the watcher to detect the rename. This prevents UI glitches during renaming root. Todo: - [x] Fix wrong abs paths when root is renamed - [x] Fix explicit scan entry func to handle renamed root dir - [x] Tests - [x] Test on Linux - [x] Tested with single and multipe worktrees - [x] Tested when single file is root file Release Notes: - Fixed an issue where worktree root name couldn't be renamed in project panel. --- crates/project/src/project.rs | 12 +++- crates/project_panel/src/project_panel.rs | 86 ++++++++++++++++++++++- crates/worktree/src/worktree.rs | 62 ++++++++++++---- 3 files changed, 143 insertions(+), 17 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 9d670291b6d7d29649a249b09b83193fb6237115..da2eeb857834dd644407bdd15b240549cafaa072 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1535,6 +1535,10 @@ impl Project { }) } + /// Renames the project entry with given `entry_id`. + /// + /// `new_path` is a relative path to worktree root. + /// If root entry is renamed then its new root name is used instead. pub fn rename_entry( &mut self, entry_id: ProjectEntryId, @@ -1551,12 +1555,18 @@ impl Project { }; let worktree_id = worktree.read(cx).id(); + let is_root_entry = self.entry_is_worktree_root(entry_id, cx); let lsp_store = self.lsp_store().downgrade(); cx.spawn(|_, mut cx| async move { let (old_abs_path, new_abs_path) = { let root_path = worktree.update(&mut cx, |this, _| this.abs_path())?; - (root_path.join(&old_path), root_path.join(&new_path)) + let new_abs_path = if is_root_entry { + root_path.parent().unwrap().join(&new_path) + } else { + root_path.join(&new_path) + }; + (root_path.join(&old_path), new_abs_path) }; LspStore::will_rename_entry( lsp_store.clone(), diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index c308e8ca4eacd51ff234dfbac1d8481293d40653..ffdab5bcb0b9c6af5c43b9673bf5f1ec117f2bc8 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -733,7 +733,9 @@ impl ProjectPanel { .action("Copy Path", Box::new(CopyPath)) .action("Copy Relative Path", Box::new(CopyRelativePath)) .separator() - .action("Rename", Box::new(Rename)) + .when(!is_root || !cfg!(target_os = "windows"), |menu| { + menu.action("Rename", Box::new(Rename)) + }) .when(!is_root & !is_remote, |menu| { menu.action("Trash", Box::new(Trash { skip_prompt: false })) }) @@ -1348,6 +1350,10 @@ impl ProjectPanel { if let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx) { let sub_entry_id = self.unflatten_entry_id(entry_id); if let Some(entry) = worktree.read(cx).entry_for_id(sub_entry_id) { + #[cfg(target_os = "windows")] + if Some(entry) == worktree.read(cx).root_entry() { + return; + } self.edit_state = Some(EditState { worktree_id, entry_id: sub_entry_id, @@ -7280,6 +7286,84 @@ mod tests { ); } + #[gpui::test] + #[cfg_attr(target_os = "windows", ignore)] + async fn test_rename_root_of_worktree(cx: &mut gpui::TestAppContext) { + init_test_with_editor(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/root1", + json!({ + "dir1": { + "file1.txt": "content 1", + }, + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await; + let workspace = + cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + + toggle_expand_dir(&panel, "root1/dir1", cx); + + assert_eq!( + visible_entries_as_strings(&panel, 0..20, cx), + &["v root1", " v dir1 <== selected", " file1.txt",], + "Initial state with worktrees" + ); + + select_path(&panel, "root1", cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..20, cx), + &["v root1 <== selected", " v dir1", " file1.txt",], + ); + + // Rename root1 to new_root1 + panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx)); + + assert_eq!( + visible_entries_as_strings(&panel, 0..20, cx), + &[ + "v [EDITOR: 'root1'] <== selected", + " v dir1", + " file1.txt", + ], + ); + + let confirm = panel.update_in(cx, |panel, window, cx| { + panel + .filename_editor + .update(cx, |editor, cx| editor.set_text("new_root1", window, cx)); + panel.confirm_edit(window, cx).unwrap() + }); + confirm.await.unwrap(); + assert_eq!( + visible_entries_as_strings(&panel, 0..20, cx), + &[ + "v new_root1 <== selected", + " v dir1", + " file1.txt", + ], + "Should update worktree name" + ); + + // Ensure internal paths have been updated + select_path(&panel, "new_root1/dir1/file1.txt", cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..20, cx), + &[ + "v new_root1", + " v dir1", + " file1.txt <== selected", + ], + "Files in renamed worktree are selectable" + ); + } + #[gpui::test] async fn test_multiple_marked_entries(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index eee87f3cc51d199b1197f1e5a82c96ced3c9404c..08d55e0540e6df6e931a36a874336ad0e4ffa73b 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -1432,16 +1432,7 @@ impl LocalWorktree { drop(barrier); } ScanState::RootUpdated { new_path } => { - if let Some(new_path) = new_path { - this.snapshot.git_repositories = Default::default(); - this.snapshot.ignores_by_parent_abs_path = Default::default(); - let root_name = new_path - .as_path() - .file_name() - .map_or(String::new(), |f| f.to_string_lossy().to_string()); - this.snapshot.update_abs_path(new_path, root_name); - } - this.restart_background_scanners(cx); + this.update_abs_path_and_refresh(new_path, cx); } } cx.notify(); @@ -1881,6 +1872,10 @@ impl LocalWorktree { })) } + /// Rename an entry. + /// + /// `new_path` is the new relative path to the worktree root. + /// If the root entry is renamed then `new_path` is the new root name instead. fn rename_entry( &self, entry_id: ProjectEntryId, @@ -1893,8 +1888,18 @@ impl LocalWorktree { }; let new_path = new_path.into(); let abs_old_path = self.absolutize(&old_path); - let Ok(abs_new_path) = self.absolutize(&new_path) else { - return Task::ready(Err(anyhow!("absolutizing path {new_path:?}"))); + + let is_root_entry = self.root_entry().is_some_and(|e| e.id == entry_id); + let abs_new_path = if is_root_entry { + let Some(root_parent_path) = self.abs_path().parent() else { + return Task::ready(Err(anyhow!("no parent for path {:?}", self.abs_path))); + }; + root_parent_path.join(&new_path) + } else { + let Ok(absolutize_path) = self.absolutize(&new_path) else { + return Task::ready(Err(anyhow!("absolutizing path {new_path:?}"))); + }; + absolutize_path }; let abs_path = abs_new_path.clone(); let fs = self.fs.clone(); @@ -1928,9 +1933,19 @@ impl LocalWorktree { rename.await?; Ok(this .update(&mut cx, |this, cx| { - this.as_local_mut() - .unwrap() - .refresh_entry(new_path.clone(), Some(old_path), cx) + let local = this.as_local_mut().unwrap(); + if is_root_entry { + // We eagerly update `abs_path` and refresh this worktree. + // Otherwise, the FS watcher would do it on the `RootUpdated` event, + // but with a noticeable delay, so we handle it proactively. + local.update_abs_path_and_refresh( + Some(SanitizedPath::from(abs_path.clone())), + cx, + ); + Task::ready(Ok(this.root_entry().cloned())) + } else { + local.refresh_entry(new_path.clone(), Some(old_path), cx) + } })? .await? .map(CreatedEntry::Included) @@ -2195,6 +2210,23 @@ impl LocalWorktree { self.share_private_files = true; self.restart_background_scanners(cx); } + + fn update_abs_path_and_refresh( + &mut self, + new_path: Option, + cx: &Context, + ) { + if let Some(new_path) = new_path { + self.snapshot.git_repositories = Default::default(); + self.snapshot.ignores_by_parent_abs_path = Default::default(); + let root_name = new_path + .as_path() + .file_name() + .map_or(String::new(), |f| f.to_string_lossy().to_string()); + self.snapshot.update_abs_path(new_path, root_name); + } + self.restart_background_scanners(cx); + } } impl RemoteWorktree { From e84d77e8791fc62d0ea7a854d33c88711e3ec241 Mon Sep 17 00:00:00 2001 From: Henrikh Kantuni Date: Sun, 9 Feb 2025 04:43:09 -0500 Subject: [PATCH 22/42] Fix typo in elm.md (#24519) Removes duplicate mention of `elm`. --- docs/src/languages/elm.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/languages/elm.md b/docs/src/languages/elm.md index f1f4047d9e6e6d8fa11edea8b246338670738107..e5355a3e807c20ef2e296a186224b0a2c3e8e0c8 100644 --- a/docs/src/languages/elm.md +++ b/docs/src/languages/elm.md @@ -7,7 +7,7 @@ Elm support is available through the [Elm extension](https://github.com/zed-exte ## Setup -Zed support for Elm requires installation of `elm`, `elm-format`, `elm-review` and `elm`. +Zed support for Elm requires installation of `elm`, `elm-format`, and `elm-review`. 1. [Install Elm](https://guide.elm-lang.org/install/elm.html) (or run `brew install elm` on macOS). 2. Install `elm-review` to support code linting: From 065fdcb86b72ac2e3bf0125d8bab2d15989c9e57 Mon Sep 17 00:00:00 2001 From: Caleb! <48127194+kaf-lamed-beyt@users.noreply.github.com> Date: Sun, 9 Feb 2025 16:54:14 +0100 Subject: [PATCH 23/42] language_tools: Add background color to syntax tree view (#24524) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #22830 @jansol, please take a look. I don't know if this is correct as I couldn't really tell the difference. I just added the active theme's background color to the main container of the tree view. Screenshot 2025-02-09 at 10 29 15 AM Release Notes: - Added an explicit background color to the syntax tree view. cc: @iamnbutler --------- Co-authored-by: Marshall Bowers --- crates/language_tools/src/syntax_tree_view.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index 9db4a97fa9e3b998fc7052b36e2f4fda420ef829..3dbdfa2b9125d686816345c225adadf7ce1c6e0f 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/crates/language_tools/src/syntax_tree_view.rs @@ -293,7 +293,7 @@ impl SyntaxTreeView { impl Render for SyntaxTreeView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let mut rendered = div().flex_1(); + let mut rendered = div().flex_1().bg(cx.theme().colors().editor_background); if let Some(layer) = self .editor From 072d2b061ab88cf347fcdc839366e978bfab1de6 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Sun, 9 Feb 2025 11:07:40 -0500 Subject: [PATCH 24/42] ui: Remove `ToolStrip` component (#24529) This PR removes the `ToolStrip` component. Pulling this change out of https://github.com/zed-industries/zed/pull/24456. Release Notes: - N/A --- crates/storybook/src/story_selector.rs | 2 - crates/ui/src/components.rs | 2 - crates/ui/src/components/stories.rs | 2 - .../ui/src/components/stories/tool_strip.rs | 33 ----------- crates/ui/src/components/tool_strip.rs | 58 ------------------- 5 files changed, 97 deletions(-) delete mode 100644 crates/ui/src/components/stories/tool_strip.rs delete mode 100644 crates/ui/src/components/tool_strip.rs diff --git a/crates/storybook/src/story_selector.rs b/crates/storybook/src/story_selector.rs index f9af57f8b52ae64e521bf19ab55fd4b30e75bd93..6f844a65b5808cb990d9c79af2e3f6609220b32f 100644 --- a/crates/storybook/src/story_selector.rs +++ b/crates/storybook/src/story_selector.rs @@ -36,7 +36,6 @@ pub enum ComponentStory { TabBar, Text, ToggleButton, - ToolStrip, ViewportUnits, WithRemSize, Vector, @@ -73,7 +72,6 @@ impl ComponentStory { Self::TabBar => cx.new(|_| ui::TabBarStory).into(), Self::Text => TextStory::model(cx).into(), Self::ToggleButton => cx.new(|_| ui::ToggleButtonStory).into(), - Self::ToolStrip => cx.new(|_| ui::ToolStripStory).into(), Self::ViewportUnits => cx.new(|_| crate::stories::ViewportUnitsStory).into(), Self::WithRemSize => cx.new(|_| crate::stories::WithRemSizeStory).into(), Self::Vector => cx.new(|_| ui::VectorStory).into(), diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index 94ace5632c664bbd04dc2fa7be58b3c2dff2bcc0..184d841bb1caf023b8f1989fd091896d43be5d17 100644 --- a/crates/ui/src/components.rs +++ b/crates/ui/src/components.rs @@ -29,7 +29,6 @@ mod tab; mod tab_bar; mod table; mod toggle; -mod tool_strip; mod tooltip; #[cfg(feature = "stories")] @@ -66,7 +65,6 @@ pub use tab::*; pub use tab_bar::*; pub use table::*; pub use toggle::*; -pub use tool_strip::*; pub use tooltip::*; #[cfg(feature = "stories")] diff --git a/crates/ui/src/components/stories.rs b/crates/ui/src/components/stories.rs index b55aa064f92c3e2749fdcdfa53c476d92392825b..9161b14b47df7d278eb0fe2e41af2cf07bb1f269 100644 --- a/crates/ui/src/components/stories.rs +++ b/crates/ui/src/components/stories.rs @@ -15,7 +15,6 @@ mod list_item; mod tab; mod tab_bar; mod toggle_button; -mod tool_strip; pub use avatar::*; pub use button::*; @@ -31,4 +30,3 @@ pub use list_item::*; pub use tab::*; pub use tab_bar::*; pub use toggle_button::*; -pub use tool_strip::*; diff --git a/crates/ui/src/components/stories/tool_strip.rs b/crates/ui/src/components/stories/tool_strip.rs deleted file mode 100644 index 0a6a6b7ad0343be3fb8616129bfd212fc8f3acf9..0000000000000000000000000000000000000000 --- a/crates/ui/src/components/stories/tool_strip.rs +++ /dev/null @@ -1,33 +0,0 @@ -use gpui::Render; -use story::{Story, StoryItem, StorySection}; - -use crate::{prelude::*, ToolStrip, Tooltip}; - -pub struct ToolStripStory; - -impl Render for ToolStripStory { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - Story::container() - .child(Story::title_for::()) - .child( - StorySection::new().child(StoryItem::new( - "Vertical Tool Strip", - h_flex().child( - ToolStrip::vertical("tool_strip_example") - .tool( - IconButton::new("example_tool", IconName::AudioOn) - .tooltip(Tooltip::text("Example tool")), - ) - .tool( - IconButton::new("example_tool_2", IconName::MicMute) - .tooltip(Tooltip::text("Example tool 2")), - ) - .tool( - IconButton::new("example_tool_3", IconName::Screen) - .tooltip(Tooltip::text("Example tool 3")), - ), - ), - )), - ) - } -} diff --git a/crates/ui/src/components/tool_strip.rs b/crates/ui/src/components/tool_strip.rs deleted file mode 100644 index 00166c8d7ef8c3b71e40179bcecdc9867dd73efd..0000000000000000000000000000000000000000 --- a/crates/ui/src/components/tool_strip.rs +++ /dev/null @@ -1,58 +0,0 @@ -#![allow(missing_docs)] - -use gpui::Axis; - -use crate::prelude::*; - -#[derive(IntoElement)] -pub struct ToolStrip { - id: ElementId, - tools: Vec, - axis: Axis, -} - -impl ToolStrip { - fn new(id: ElementId, axis: Axis) -> Self { - Self { - id, - tools: vec![], - axis, - } - } - - pub fn vertical(id: impl Into) -> Self { - Self::new(id.into(), Axis::Vertical) - } - - pub fn tools(mut self, tools: Vec) -> Self { - self.tools = tools; - self - } - - pub fn tool(mut self, tool: IconButton) -> Self { - self.tools.push(tool); - self - } -} - -impl RenderOnce for ToolStrip { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let group = format!("tool_strip_{}", self.id.clone()); - - div() - .id(self.id.clone()) - .group(group) - .map(|element| match self.axis { - Axis::Vertical => element.v_flex(), - Axis::Horizontal => element.h_flex(), - }) - .flex_none() - .gap(DynamicSpacing::Base04.rems(cx)) - .p(DynamicSpacing::Base02.rems(cx)) - .border_1() - .border_color(cx.theme().colors().border) - .rounded(rems_from_px(6.0)) - .bg(cx.theme().colors().elevated_surface_background) - .children(self.tools) - } -} From f42177a912389eab57dc4a39857748e54ae3bc33 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Sun, 9 Feb 2025 11:46:33 -0500 Subject: [PATCH 25/42] ci: Pin Prettier to a specific version for docs formatting (#24531) This PR pins Prettier to a specific version when we run the docs formatting check. This should prevent drift when new Prettier versions are released that may impact the formatting. Release Notes: - N/A --- .github/workflows/docs.yml | 6 ++++-- docs/theme/css/variables.css | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 0870a55e60057cb8440c7d911ab78aef18c04045..383ba93a8fba8173c0e8025b5730c6496fc49c66 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -24,11 +24,13 @@ jobs: - name: Prettier Check on /docs working-directory: ./docs run: | - pnpm dlx prettier . --check || { + pnpm dlx prettier@${PRETTIER_VERSION} . --check || { echo "To fix, run from the root of the zed repo:" - echo " cd docs && pnpm dlx prettier . --write && cd .." + echo " cd docs && pnpm dlx prettier@${PRETTIER_VERSION} . --write && cd .." false } + env: + PRETTIER_VERSION: 3.5.0 - name: Check for Typos with Typos-CLI uses: crate-ci/typos@8e6a4285bcbde632c5d79900a7779746e8b7ea3f # v1.24.6 diff --git a/docs/theme/css/variables.css b/docs/theme/css/variables.css index 55ae4a427da269620cb9b15d10ff33d0f4ace958..6604545c45616b012fd569517ab41ca27e834800 100644 --- a/docs/theme/css/variables.css +++ b/docs/theme/css/variables.css @@ -13,8 +13,9 @@ --menu-bar-height: 64px; --font: "IA Writer Quattro S", sans-serif; --title-font: "Lora", "Helvetica Neue", Helvetica, Arial, sans-serif; - --mono-font: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - Liberation Mono, Courier New, monospace; + --mono-font: + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, + Courier New, monospace; --code-font-size: 0.875em /* please adjust the ace font size accordingly in editor.js */; From 6ee447ee589ba1c1bea941d4c597e3aecb637880 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sun, 9 Feb 2025 19:27:41 +0200 Subject: [PATCH 26/42] Move focus into editor for `outline_panel::Open` action on outlines and search results (#24535) Follow-up of https://github.com/zed-industries/zed/discussions/19782#discussioncomment-12055976 Release Notes: - Fixed outline panel not focusing editor when outlines and search results were opened with `outline_panel::Open` --- crates/outline_panel/src/outline_panel.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index d1be0368228cd1578977605eafd88cda5869c2f2..54e4a2bde0178f3c79b6edc4abecd3a58f6b53d4 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -930,7 +930,7 @@ impl OutlinePanel { cx.propagate() } else if let Some(selected_entry) = self.selected_entry().cloned() { self.toggle_expanded(&selected_entry, window, cx); - self.scroll_editor_to_entry(&selected_entry, true, false, window, cx); + self.scroll_editor_to_entry(&selected_entry, true, true, window, cx); } } @@ -985,7 +985,7 @@ impl OutlinePanel { &mut self, entry: &PanelEntry, prefer_selection_change: bool, - change_focus: bool, + prefer_focus_change: bool, window: &mut Window, cx: &mut Context, ) { @@ -995,9 +995,13 @@ impl OutlinePanel { let active_multi_buffer = active_editor.read(cx).buffer().clone(); let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx); let mut change_selection = prefer_selection_change; + let mut change_focus = prefer_focus_change; let mut scroll_to_buffer = None; let scroll_target = match entry { - PanelEntry::FoldedDirs(..) | PanelEntry::Fs(FsEntry::Directory(..)) => None, + PanelEntry::FoldedDirs(..) | PanelEntry::Fs(FsEntry::Directory(..)) => { + change_focus = false; + None + } PanelEntry::Fs(FsEntry::ExternalFile(file)) => { change_selection = false; scroll_to_buffer = Some(file.buffer_id); @@ -1041,6 +1045,7 @@ impl OutlinePanel { }), PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => { change_selection = false; + change_focus = false; multi_buffer_snapshot.anchor_in_excerpt(excerpt.id, excerpt.range.context.start) } PanelEntry::Search(search_entry) => Some(search_entry.match_range.start), From 56cfc60875f6105fb3bd2fece6427bdaa1cbde4e Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Sun, 9 Feb 2025 15:23:39 -0300 Subject: [PATCH 27/42] ui: Add `buffer_font` method to labels (#24479) Now you don't need to wrap the `Label` in a `div` anymore Release Notes: - N/A Co-authored-by: Danilo --- crates/ui/src/components/label/highlighted_label.rs | 5 +++++ crates/ui/src/components/label/label.rs | 5 +++++ crates/ui/src/components/label/label_like.rs | 10 ++++++++++ crates/ui/src/styles/color.rs | 6 ++++++ 4 files changed, 26 insertions(+) diff --git a/crates/ui/src/components/label/highlighted_label.rs b/crates/ui/src/components/label/highlighted_label.rs index d528f47218a46689611329360574d61613f9d31f..14ea7a5cf165a8867d95e15f6c136577980a3a58 100644 --- a/crates/ui/src/components/label/highlighted_label.rs +++ b/crates/ui/src/components/label/highlighted_label.rs @@ -75,6 +75,11 @@ impl LabelCommon for HighlightedLabel { self.base = self.base.single_line(); self } + + fn buffer_font(mut self, cx: &App) -> Self { + self.base = self.base.buffer_font(cx); + self + } } pub fn highlight_ranges( diff --git a/crates/ui/src/components/label/label.rs b/crates/ui/src/components/label/label.rs index 5f170b9a1520363893bef78b520242bc83c0f6eb..ff2687d0478a1d5e2a804a7198e8cea91e4ef1a2 100644 --- a/crates/ui/src/components/label/label.rs +++ b/crates/ui/src/components/label/label.rs @@ -172,6 +172,11 @@ impl LabelCommon for Label { self.base = self.base.single_line(); self } + + fn buffer_font(mut self, cx: &App) -> Self { + self.base = self.base.buffer_font(cx); + self + } } impl RenderOnce for Label { diff --git a/crates/ui/src/components/label/label_like.rs b/crates/ui/src/components/label/label_like.rs index c9674f10a0173beb8823ceacc20bc99cfadab8d9..fad24d8699c0cbc81034cce48389d10d5e71b223 100644 --- a/crates/ui/src/components/label/label_like.rs +++ b/crates/ui/src/components/label/label_like.rs @@ -55,6 +55,9 @@ pub trait LabelCommon { /// Sets the label to render as a single line. fn single_line(self) -> Self; + + /// Sets the font to the buffer's + fn buffer_font(self, cx: &App) -> Self; } #[derive(IntoElement)] @@ -159,6 +162,13 @@ impl LabelCommon for LabelLike { self.single_line = true; self } + + fn buffer_font(mut self, cx: &App) -> Self { + self.base = self + .base + .font(theme::ThemeSettings::get_global(cx).buffer_font.clone()); + self + } } impl ParentElement for LabelLike { diff --git a/crates/ui/src/styles/color.rs b/crates/ui/src/styles/color.rs index a8cf1d51e50adf8d9733fb77ace01791300e6903..0d234ad50d9bcd6f1a39f1f052d8a361479664f7 100644 --- a/crates/ui/src/styles/color.rs +++ b/crates/ui/src/styles/color.rs @@ -86,3 +86,9 @@ impl Color { } } } + +impl From for Color { + fn from(color: Hsla) -> Self { + Color::Custom(color) + } +} From 8f1ff189ccf698d87735366a8a1d9c2bbbd58331 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Sun, 9 Feb 2025 13:25:03 -0500 Subject: [PATCH 28/42] component: Add `component` and `component_preview` crates to power UI components (#24456) This PR formalizes design components with the Component and ComponentPreview traits. You can open the preview UI with `workspace: open component preview`. Component previews no longer need to return `Self` allowing for more complex previews, and previews of components like `ui::Tooltip` that supplement other components rather than are rendered by default. `cargo-machete` incorrectly identifies `linkme` as an unused dep on crates that have components deriving `IntoComponent`, so you may need to add this to that crate's `Cargo.toml`: ```toml # cargo-machete doesn't understand that linkme is used in the component macro [package.metadata.cargo-machete] ignored = ["linkme"] ``` Release Notes: - N/A --------- Co-authored-by: Marshall Bowers --- Cargo.lock | 51 +- Cargo.toml | 5 + crates/component/Cargo.toml | 23 + crates/component/LICENSE-GPL | 1 + crates/component/src/component.rs | 305 ++++++++++++ crates/component_preview/Cargo.toml | 21 + crates/component_preview/LICENSE-GPL | 1 + .../src/component_preview.rs | 178 +++++++ crates/ui/Cargo.toml | 6 + crates/ui/src/components/avatar/avatar.rs | 61 ++- crates/ui/src/components/button/button.rs | 221 +++++---- crates/ui/src/components/content_group.rs | 27 +- crates/ui/src/components/facepile.rs | 112 ++--- crates/ui/src/components/icon.rs | 65 ++- .../ui/src/components/icon/decorated_icon.rs | 59 +-- .../ui/src/components/icon/icon_decoration.rs | 22 +- crates/ui/src/components/indicator.rs | 31 -- crates/ui/src/components/keybinding_hint.rs | 187 ++++---- crates/ui/src/components/label/label.rs | 54 ++- crates/ui/src/components/radio.rs | 3 - crates/ui/src/components/tab.rs | 47 +- crates/ui/src/components/table.rs | 186 ++++---- crates/ui/src/components/toggle.rs | 443 +++++++----------- crates/ui/src/components/tooltip.rs | 15 +- crates/ui/src/prelude.rs | 4 +- crates/ui/src/styles/typography.rs | 47 +- crates/ui/src/traits.rs | 1 - crates/ui/src/traits/component_preview.rs | 205 -------- crates/ui_macros/Cargo.toml | 3 +- crates/ui_macros/src/derive_component.rs | 97 ++++ crates/ui_macros/src/ui_macros.rs | 25 + crates/workspace/Cargo.toml | 1 + crates/workspace/src/theme_preview.rs | 27 -- crates/workspace/src/workspace.rs | 2 + crates/zed/Cargo.toml | 3 +- crates/zed/src/main.rs | 1 + 36 files changed, 1573 insertions(+), 967 deletions(-) create mode 100644 crates/component/Cargo.toml create mode 120000 crates/component/LICENSE-GPL create mode 100644 crates/component/src/component.rs create mode 100644 crates/component_preview/Cargo.toml create mode 120000 crates/component_preview/LICENSE-GPL create mode 100644 crates/component_preview/src/component_preview.rs delete mode 100644 crates/ui/src/traits/component_preview.rs create mode 100644 crates/ui_macros/src/derive_component.rs diff --git a/Cargo.lock b/Cargo.lock index a3b967c76d2571545e4d9917c7184bbb43a1737d..3fb5ee2f9f575739e81aa7ba0e6276abdb260315 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2942,6 +2942,28 @@ dependencies = [ "gpui", ] +[[package]] +name = "component" +version = "0.1.0" +dependencies = [ + "collections", + "gpui", + "linkme", + "once_cell", + "parking_lot", + "theme", +] + +[[package]] +name = "component_preview" +version = "0.1.0" +dependencies = [ + "component", + "gpui", + "ui", + "workspace", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -7280,6 +7302,26 @@ dependencies = [ "memchr", ] +[[package]] +name = "linkme" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "566336154b9e58a4f055f6dd4cbab62c7dc0826ce3c0a04e63b2d2ecd784cdae" +dependencies = [ + "linkme-impl", +] + +[[package]] +name = "linkme-impl" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edbe595006d355eaf9ae11db92707d4338cd2384d16866131cc1afdbdd35d8d9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -8693,9 +8735,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.2" +version = "1.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" [[package]] name = "oo7" @@ -14320,8 +14362,10 @@ name = "ui" version = "0.1.0" dependencies = [ "chrono", + "component", "gpui", "itertools 0.14.0", + "linkme", "menu", "serde", "settings", @@ -14349,6 +14393,7 @@ name = "ui_macros" version = "0.1.0" dependencies = [ "convert_case 0.7.1", + "linkme", "proc-macro2", "quote", "syn 1.0.109", @@ -16120,6 +16165,7 @@ dependencies = [ "client", "clock", "collections", + "component", "db", "derive_more", "env_logger 0.11.6", @@ -16554,6 +16600,7 @@ dependencies = [ "collections", "command_palette", "command_palette_hooks", + "component_preview", "copilot", "db", "diagnostics", diff --git a/Cargo.toml b/Cargo.toml index ee6a66f909888bb98a2e9adab7eecd6a55e20c3a..147d2c32e138762681b81dee2486eae3fce5a603 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,8 @@ members = [ "crates/collections", "crates/command_palette", "crates/command_palette_hooks", + "crates/component", + "crates/component_preview", "crates/context_server", "crates/context_server_settings", "crates/copilot", @@ -226,6 +228,8 @@ collab_ui = { path = "crates/collab_ui" } collections = { path = "crates/collections" } command_palette = { path = "crates/command_palette" } command_palette_hooks = { path = "crates/command_palette_hooks" } +component = { path = "crates/component" } +component_preview = { path = "crates/component_preview" } context_server = { path = "crates/context_server" } context_server_settings = { path = "crates/context_server_settings" } copilot = { path = "crates/copilot" } @@ -426,6 +430,7 @@ jupyter-websocket-client = { version = "0.9.0" } libc = "0.2" libsqlite3-sys = { version = "0.30.1", features = ["bundled"] } linkify = "0.10.0" +linkme = "0.3.31" livekit = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "811ceae29fabee455f110c56cd66b3f49a7e5003", features = [ "dispatcher", "services-dispatcher", diff --git a/crates/component/Cargo.toml b/crates/component/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..33f951ff9520b2149c21c1494194414ce904881a --- /dev/null +++ b/crates/component/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "component" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/component.rs" + +[dependencies] +collections.workspace = true +gpui.workspace = true +linkme.workspace = true +once_cell = "1.20.3" +parking_lot.workspace = true +theme.workspace = true + +[features] +default = [] diff --git a/crates/component/LICENSE-GPL b/crates/component/LICENSE-GPL new file mode 120000 index 0000000000000000000000000000000000000000..89e542f750cd3860a0598eff0dc34b56d7336dc4 --- /dev/null +++ b/crates/component/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/component/src/component.rs b/crates/component/src/component.rs new file mode 100644 index 0000000000000000000000000000000000000000..e4a2ae7921f7cee42d99c11517b23e67aa6d3653 --- /dev/null +++ b/crates/component/src/component.rs @@ -0,0 +1,305 @@ +use std::ops::{Deref, DerefMut}; + +use collections::HashMap; +use gpui::{div, prelude::*, AnyElement, App, IntoElement, RenderOnce, SharedString, Window}; +use linkme::distributed_slice; +use once_cell::sync::Lazy; +use parking_lot::RwLock; +use theme::ActiveTheme; + +pub trait Component { + fn scope() -> Option<&'static str>; + fn name() -> &'static str { + std::any::type_name::() + } + fn description() -> Option<&'static str> { + None + } +} + +pub trait ComponentPreview: Component { + fn preview(_window: &mut Window, _cx: &App) -> AnyElement; +} + +#[distributed_slice] +pub static __ALL_COMPONENTS: [fn()] = [..]; + +#[distributed_slice] +pub static __ALL_PREVIEWS: [fn()] = [..]; + +pub static COMPONENT_DATA: Lazy> = + Lazy::new(|| RwLock::new(ComponentRegistry::new())); + +pub struct ComponentRegistry { + components: Vec<(Option<&'static str>, &'static str, Option<&'static str>)>, + previews: HashMap<&'static str, fn(&mut Window, &App) -> AnyElement>, +} + +impl ComponentRegistry { + fn new() -> Self { + ComponentRegistry { + components: Vec::new(), + previews: HashMap::default(), + } + } +} + +pub fn init() { + let component_fns: Vec<_> = __ALL_COMPONENTS.iter().cloned().collect(); + let preview_fns: Vec<_> = __ALL_PREVIEWS.iter().cloned().collect(); + + for f in component_fns { + f(); + } + for f in preview_fns { + f(); + } +} + +pub fn register_component() { + let component_data = (T::scope(), T::name(), T::description()); + COMPONENT_DATA.write().components.push(component_data); +} + +pub fn register_preview() { + let preview_data = (T::name(), T::preview as fn(&mut Window, &App) -> AnyElement); + COMPONENT_DATA + .write() + .previews + .insert(preview_data.0, preview_data.1); +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ComponentId(pub &'static str); + +#[derive(Clone)] +pub struct ComponentMetadata { + name: SharedString, + scope: Option, + description: Option, + preview: Option AnyElement>, +} + +impl ComponentMetadata { + pub fn name(&self) -> SharedString { + self.name.clone() + } + + pub fn scope(&self) -> Option { + self.scope.clone() + } + + pub fn description(&self) -> Option { + self.description.clone() + } + + pub fn preview(&self) -> Option AnyElement> { + self.preview + } +} + +pub struct AllComponents(pub HashMap); + +impl AllComponents { + pub fn new() -> Self { + AllComponents(HashMap::default()) + } + + /// Returns all components with previews + pub fn all_previews(&self) -> Vec<&ComponentMetadata> { + self.0.values().filter(|c| c.preview.is_some()).collect() + } + + /// Returns all components with previews sorted by name + pub fn all_previews_sorted(&self) -> Vec { + let mut previews: Vec = + self.all_previews().into_iter().cloned().collect(); + previews.sort_by_key(|a| a.name()); + previews + } + + /// Returns all components + pub fn all(&self) -> Vec<&ComponentMetadata> { + self.0.values().collect() + } + + /// Returns all components sorted by name + pub fn all_sorted(&self) -> Vec { + let mut components: Vec = self.all().into_iter().cloned().collect(); + components.sort_by_key(|a| a.name()); + components + } +} + +impl Deref for AllComponents { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for AllComponents { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +pub fn components() -> AllComponents { + let data = COMPONENT_DATA.read(); + let mut all_components = AllComponents::new(); + + for &(scope, name, description) in &data.components { + let scope = scope.map(Into::into); + let preview = data.previews.get(name).cloned(); + all_components.insert( + ComponentId(name), + ComponentMetadata { + name: name.into(), + scope, + description: description.map(Into::into), + preview, + }, + ); + } + + all_components +} + +/// Which side of the preview to show labels on +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub enum ExampleLabelSide { + /// Left side + Left, + /// Right side + Right, + #[default] + /// Top side + Top, + /// Bottom side + Bottom, +} + +/// A single example of a component. +#[derive(IntoElement)] +pub struct ComponentExample { + variant_name: SharedString, + element: AnyElement, + label_side: ExampleLabelSide, + grow: bool, +} + +impl RenderOnce for ComponentExample { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + let base = div().flex(); + + let base = match self.label_side { + ExampleLabelSide::Right => base.flex_row(), + ExampleLabelSide::Left => base.flex_row_reverse(), + ExampleLabelSide::Bottom => base.flex_col(), + ExampleLabelSide::Top => base.flex_col_reverse(), + }; + + base.gap_1() + .text_xs() + .text_color(cx.theme().colors().text_muted) + .when(self.grow, |this| this.flex_1()) + .child(self.element) + .child(self.variant_name) + .into_any_element() + } +} + +impl ComponentExample { + /// Create a new example with the given variant name and example value. + pub fn new(variant_name: impl Into, element: AnyElement) -> Self { + Self { + variant_name: variant_name.into(), + element, + label_side: ExampleLabelSide::default(), + grow: false, + } + } + + /// Set the example to grow to fill the available horizontal space. + pub fn grow(mut self) -> Self { + self.grow = true; + self + } +} + +/// A group of component examples. +#[derive(IntoElement)] +pub struct ComponentExampleGroup { + pub title: Option, + pub examples: Vec, + pub grow: bool, +} + +impl RenderOnce for ComponentExampleGroup { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + div() + .flex_col() + .text_sm() + .text_color(cx.theme().colors().text_muted) + .when(self.grow, |this| this.w_full().flex_1()) + .when_some(self.title, |this, title| this.gap_4().child(title)) + .child( + div() + .flex() + .items_start() + .w_full() + .gap_6() + .children(self.examples) + .into_any_element(), + ) + .into_any_element() + } +} + +impl ComponentExampleGroup { + /// Create a new group of examples with the given title. + pub fn new(examples: Vec) -> Self { + Self { + title: None, + examples, + grow: false, + } + } + + /// Create a new group of examples with the given title. + pub fn with_title(title: impl Into, examples: Vec) -> Self { + Self { + title: Some(title.into()), + examples, + grow: false, + } + } + + /// Set the group to grow to fill the available horizontal space. + pub fn grow(mut self) -> Self { + self.grow = true; + self + } +} + +/// Create a single example +pub fn single_example( + variant_name: impl Into, + example: AnyElement, +) -> ComponentExample { + ComponentExample::new(variant_name, example) +} + +/// Create a group of examples without a title +pub fn example_group(examples: Vec) -> ComponentExampleGroup { + ComponentExampleGroup::new(examples) +} + +/// Create a group of examples with a title +pub fn example_group_with_title( + title: impl Into, + examples: Vec, +) -> ComponentExampleGroup { + ComponentExampleGroup::with_title(title, examples) +} diff --git a/crates/component_preview/Cargo.toml b/crates/component_preview/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..d909991a1893912ecd777d4503d983605ac85f05 --- /dev/null +++ b/crates/component_preview/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "component_preview" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/component_preview.rs" + +[features] +default = [] + +[dependencies] +component.workspace = true +gpui.workspace = true +ui.workspace = true +workspace.workspace = true diff --git a/crates/component_preview/LICENSE-GPL b/crates/component_preview/LICENSE-GPL new file mode 120000 index 0000000000000000000000000000000000000000..89e542f750cd3860a0598eff0dc34b56d7336dc4 --- /dev/null +++ b/crates/component_preview/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/component_preview/src/component_preview.rs b/crates/component_preview/src/component_preview.rs new file mode 100644 index 0000000000000000000000000000000000000000..84e00f751c76d656ec977355f8ed2c4d5aa350bb --- /dev/null +++ b/crates/component_preview/src/component_preview.rs @@ -0,0 +1,178 @@ +//! # Component Preview +//! +//! A view for exploring Zed components. + +use component::{components, ComponentMetadata}; +use gpui::{prelude::*, App, EventEmitter, FocusHandle, Focusable, Window}; +use ui::prelude::*; + +use workspace::{item::ItemEvent, Item, Workspace, WorkspaceId}; + +pub fn init(cx: &mut App) { + cx.observe_new(|workspace: &mut Workspace, _, _cx| { + workspace.register_action( + |workspace, _: &workspace::OpenComponentPreview, window, cx| { + let component_preview = cx.new(ComponentPreview::new); + workspace.add_item_to_active_pane( + Box::new(component_preview), + None, + true, + window, + cx, + ) + }, + ); + }) + .detach(); +} + +struct ComponentPreview { + focus_handle: FocusHandle, +} + +impl ComponentPreview { + pub fn new(cx: &mut Context) -> Self { + Self { + focus_handle: cx.focus_handle(), + } + } + + fn render_sidebar(&self, _window: &Window, _cx: &Context) -> impl IntoElement { + let components = components().all_sorted(); + let sorted_components = components.clone(); + + v_flex().gap_px().p_1().children( + sorted_components + .into_iter() + .map(|component| self.render_sidebar_entry(&component, _cx)), + ) + } + + fn render_sidebar_entry( + &self, + component: &ComponentMetadata, + _cx: &Context, + ) -> impl IntoElement { + h_flex() + .w_40() + .px_1p5() + .py_1() + .child(component.name().clone()) + } + + fn render_preview( + &self, + component: &ComponentMetadata, + window: &mut Window, + cx: &Context, + ) -> impl IntoElement { + let name = component.name(); + let scope = component.scope(); + + let description = component.description(); + + v_group() + .w_full() + .gap_4() + .p_8() + .rounded_md() + .child( + v_flex() + .gap_1() + .child( + h_flex() + .gap_1() + .text_xl() + .child(div().child(name)) + .when_some(scope, |this, scope| { + this.child(div().opacity(0.5).child(format!("({})", scope))) + }), + ) + .when_some(description, |this, description| { + this.child( + div() + .text_ui_sm(cx) + .text_color(cx.theme().colors().text_muted) + .max_w(px(600.0)) + .child(description), + ) + }), + ) + .when_some(component.preview(), |this, preview| { + this.child(preview(window, cx)) + }) + .into_any_element() + } + + fn render_previews(&self, window: &mut Window, cx: &Context) -> impl IntoElement { + v_flex() + .id("component-previews") + .size_full() + .overflow_y_scroll() + .p_4() + .gap_2() + .children( + components() + .all_previews_sorted() + .iter() + .map(|component| self.render_preview(component, window, cx)), + ) + } +} + +impl Render for ComponentPreview { + fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement { + h_flex() + .id("component-preview") + .key_context("ComponentPreview") + .items_start() + .overflow_hidden() + .size_full() + .max_h_full() + .track_focus(&self.focus_handle) + .px_2() + .bg(cx.theme().colors().editor_background) + .child(self.render_sidebar(window, cx)) + .child(self.render_previews(window, cx)) + } +} + +impl EventEmitter for ComponentPreview {} + +impl Focusable for ComponentPreview { + fn focus_handle(&self, _: &App) -> gpui::FocusHandle { + self.focus_handle.clone() + } +} + +impl Item for ComponentPreview { + type Event = ItemEvent; + + fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option { + Some("Component Preview".into()) + } + + fn telemetry_event_text(&self) -> Option<&'static str> { + None + } + + fn show_toolbar(&self) -> bool { + false + } + + fn clone_on_split( + &self, + _workspace_id: Option, + _window: &mut Window, + cx: &mut Context, + ) -> Option> + where + Self: Sized, + { + Some(cx.new(Self::new)) + } + + fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) { + f(*event) + } +} diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index dc893d66439f4e48d8bb7372355ad877ee7e821c..ba7c89a8a6450b12d7ed3ac0f40b786107863b3d 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -14,8 +14,10 @@ path = "src/ui.rs" [dependencies] chrono.workspace = true +component.workspace = true gpui.workspace = true itertools = { workspace = true, optional = true } +linkme.workspace = true menu.workspace = true serde.workspace = true settings.workspace = true @@ -31,3 +33,7 @@ windows.workspace = true [features] default = [] stories = ["dep:itertools", "dep:story"] + +# cargo-machete doesn't understand that linkme is used in the component macro +[package.metadata.cargo-machete] +ignored = ["linkme"] diff --git a/crates/ui/src/components/avatar/avatar.rs b/crates/ui/src/components/avatar/avatar.rs index e7335a9e75a4b52bb14f322ce53432136537de88..82f3ea7ae2e4764471c8eb0df67827666848640b 100644 --- a/crates/ui/src/components/avatar/avatar.rs +++ b/crates/ui/src/components/avatar/avatar.rs @@ -1,4 +1,4 @@ -use crate::prelude::*; +use crate::{prelude::*, Indicator}; use gpui::{img, AnyElement, Hsla, ImageSource, Img, IntoElement, Styled}; @@ -14,7 +14,7 @@ use gpui::{img, AnyElement, Hsla, ImageSource, Img, IntoElement, Styled}; /// .grayscale(true) /// .border_color(gpui::red()); /// ``` -#[derive(IntoElement)] +#[derive(IntoElement, IntoComponent)] pub struct Avatar { image: Img, size: Option, @@ -96,3 +96,60 @@ impl RenderOnce for Avatar { .children(self.indicator.map(|indicator| div().child(indicator))) } } + +impl ComponentPreview for Avatar { + fn preview(_window: &mut Window, _cx: &App) -> AnyElement { + let example_avatar = "https://avatars.githubusercontent.com/u/1714999?v=4"; + + v_flex() + .gap_6() + .children(vec![ + example_group_with_title( + "Sizes", + vec![ + single_example( + "Default", + Avatar::new("https://avatars.githubusercontent.com/u/1714999?v=4") + .into_any_element(), + ), + single_example( + "Small", + Avatar::new(example_avatar).size(px(24.)).into_any_element(), + ), + single_example( + "Large", + Avatar::new(example_avatar).size(px(48.)).into_any_element(), + ), + ], + ), + example_group_with_title( + "Styles", + vec![ + single_example("Default", Avatar::new(example_avatar).into_any_element()), + single_example( + "Grayscale", + Avatar::new(example_avatar) + .grayscale(true) + .into_any_element(), + ), + single_example( + "With Border", + Avatar::new(example_avatar) + .border_color(gpui::red()) + .into_any_element(), + ), + ], + ), + example_group_with_title( + "With Indicator", + vec![single_example( + "Dot", + Avatar::new(example_avatar) + .indicator(Indicator::dot().color(Color::Success)) + .into_any_element(), + )], + ), + ]) + .into_any_element() + } +} diff --git a/crates/ui/src/components/button/button.rs b/crates/ui/src/components/button/button.rs index c9b61866617731191a53cb03ca7ef5e470ecb1cf..4194b3c8d299642815585f1831411dfe99216888 100644 --- a/crates/ui/src/components/button/button.rs +++ b/crates/ui/src/components/button/button.rs @@ -1,5 +1,7 @@ #![allow(missing_docs)] -use gpui::{AnyView, DefiniteLength}; +use component::{example_group_with_title, single_example, ComponentPreview}; +use gpui::{AnyElement, AnyView, DefiniteLength}; +use ui_macros::IntoComponent; use crate::{ prelude::*, Color, DynamicSpacing, ElevationIndex, IconPosition, KeyBinding, @@ -78,7 +80,7 @@ use super::button_icon::ButtonIcon; /// }); /// ``` /// -#[derive(IntoElement)] +#[derive(IntoElement, IntoComponent)] pub struct Button { base: ButtonLike, label: SharedString, @@ -455,101 +457,124 @@ impl RenderOnce for Button { } impl ComponentPreview for Button { - fn description() -> impl Into> { - "A button allows users to take actions, and make choices, with a single tap." - } - - fn examples(_window: &mut Window, _: &mut App) -> Vec> { - vec![ - example_group_with_title( - "Styles", - vec![ - single_example("Default", Button::new("default", "Default")), - single_example( - "Filled", - Button::new("filled", "Filled").style(ButtonStyle::Filled), - ), - single_example( - "Subtle", - Button::new("outline", "Subtle").style(ButtonStyle::Subtle), - ), - single_example( - "Transparent", - Button::new("transparent", "Transparent").style(ButtonStyle::Transparent), - ), - ], - ), - example_group_with_title( - "Tinted", - vec![ - single_example( - "Accent", - Button::new("tinted_accent", "Accent") - .style(ButtonStyle::Tinted(TintColor::Accent)), - ), - single_example( - "Error", - Button::new("tinted_negative", "Error") - .style(ButtonStyle::Tinted(TintColor::Error)), - ), - single_example( - "Warning", - Button::new("tinted_warning", "Warning") - .style(ButtonStyle::Tinted(TintColor::Warning)), - ), - single_example( - "Success", - Button::new("tinted_positive", "Success") - .style(ButtonStyle::Tinted(TintColor::Success)), - ), - ], - ), - example_group_with_title( - "States", - vec![ - single_example("Default", Button::new("default_state", "Default")), - single_example( - "Disabled", - Button::new("disabled", "Disabled").disabled(true), - ), - single_example( - "Selected", - Button::new("selected", "Selected").toggle_state(true), - ), - ], - ), - example_group_with_title( - "With Icons", - vec![ - single_example( - "Icon Start", - Button::new("icon_start", "Icon Start") - .icon(IconName::Check) - .icon_position(IconPosition::Start), - ), - single_example( - "Icon End", - Button::new("icon_end", "Icon End") - .icon(IconName::Check) - .icon_position(IconPosition::End), - ), - single_example( - "Icon Color", - Button::new("icon_color", "Icon Color") - .icon(IconName::Check) - .icon_color(Color::Accent), - ), - single_example( - "Tinted Icons", - Button::new("tinted_icons", "Error") - .style(ButtonStyle::Tinted(TintColor::Error)) - .color(Color::Error) - .icon_color(Color::Error) - .icon(IconName::Trash) - .icon_position(IconPosition::Start), - ), - ], - ), - ] + fn preview(_window: &mut Window, _cx: &App) -> AnyElement { + v_flex() + .gap_6() + .children(vec![ + example_group_with_title( + "Styles", + vec![ + single_example( + "Default", + Button::new("default", "Default").into_any_element(), + ), + single_example( + "Filled", + Button::new("filled", "Filled") + .style(ButtonStyle::Filled) + .into_any_element(), + ), + single_example( + "Subtle", + Button::new("outline", "Subtle") + .style(ButtonStyle::Subtle) + .into_any_element(), + ), + single_example( + "Transparent", + Button::new("transparent", "Transparent") + .style(ButtonStyle::Transparent) + .into_any_element(), + ), + ], + ), + example_group_with_title( + "Tinted", + vec![ + single_example( + "Accent", + Button::new("tinted_accent", "Accent") + .style(ButtonStyle::Tinted(TintColor::Accent)) + .into_any_element(), + ), + single_example( + "Error", + Button::new("tinted_negative", "Error") + .style(ButtonStyle::Tinted(TintColor::Error)) + .into_any_element(), + ), + single_example( + "Warning", + Button::new("tinted_warning", "Warning") + .style(ButtonStyle::Tinted(TintColor::Warning)) + .into_any_element(), + ), + single_example( + "Success", + Button::new("tinted_positive", "Success") + .style(ButtonStyle::Tinted(TintColor::Success)) + .into_any_element(), + ), + ], + ), + example_group_with_title( + "States", + vec![ + single_example( + "Default", + Button::new("default_state", "Default").into_any_element(), + ), + single_example( + "Disabled", + Button::new("disabled", "Disabled") + .disabled(true) + .into_any_element(), + ), + single_example( + "Selected", + Button::new("selected", "Selected") + .toggle_state(true) + .into_any_element(), + ), + ], + ), + example_group_with_title( + "With Icons", + vec![ + single_example( + "Icon Start", + Button::new("icon_start", "Icon Start") + .icon(IconName::Check) + .icon_position(IconPosition::Start) + .into_any_element(), + ), + single_example( + "Icon End", + Button::new("icon_end", "Icon End") + .icon(IconName::Check) + .icon_position(IconPosition::End) + .into_any_element(), + ), + single_example( + "Icon Color", + Button::new("icon_color", "Icon Color") + .icon(IconName::Check) + .icon_color(Color::Accent) + .into_any_element(), + ), + single_example( + "Tinted Icons", + Button::new("tinted_icons", "Error") + .style(ButtonStyle::Tinted(TintColor::Error)) + .color(Color::Error) + .icon_color(Color::Error) + .icon(IconName::Trash) + .icon_position(IconPosition::Start) + .into_any_element(), + ), + ], + ), + ]) + .into_any_element() } } diff --git a/crates/ui/src/components/content_group.rs b/crates/ui/src/components/content_group.rs index b1ffae1490cbddc45ee97f2dbc0067ef95f1d29f..1a57838c2e8102234f18f9241edc2349d8fb724a 100644 --- a/crates/ui/src/components/content_group.rs +++ b/crates/ui/src/components/content_group.rs @@ -1,4 +1,5 @@ use crate::prelude::*; +use component::{example_group, single_example, ComponentPreview}; use gpui::{AnyElement, IntoElement, ParentElement, StyleRefinement, Styled}; use smallvec::SmallVec; @@ -22,7 +23,8 @@ pub fn h_group() -> ContentGroup { } /// A flexible container component that can hold other elements. -#[derive(IntoElement)] +#[derive(IntoElement, IntoComponent)] +#[component(scope = "layout")] pub struct ContentGroup { base: Div, border: bool, @@ -87,16 +89,8 @@ impl RenderOnce for ContentGroup { } impl ComponentPreview for ContentGroup { - fn description() -> impl Into> { - "A flexible container component that can hold other elements. It can be customized with or without a border and background fill." - } - - fn example_label_side() -> ExampleLabelSide { - ExampleLabelSide::Bottom - } - - fn examples(_window: &mut Window, _: &mut App) -> Vec> { - vec![example_group(vec![ + fn preview(_window: &mut Window, _cx: &App) -> AnyElement { + example_group(vec![ single_example( "Default", ContentGroup::new() @@ -104,7 +98,8 @@ impl ComponentPreview for ContentGroup { .items_center() .justify_center() .h_48() - .child(Label::new("Default ContentBox")), + .child(Label::new("Default ContentBox")) + .into_any_element(), ) .grow(), single_example( @@ -115,7 +110,8 @@ impl ComponentPreview for ContentGroup { .justify_center() .h_48() .borderless() - .child(Label::new("Borderless ContentBox")), + .child(Label::new("Borderless ContentBox")) + .into_any_element(), ) .grow(), single_example( @@ -126,10 +122,11 @@ impl ComponentPreview for ContentGroup { .justify_center() .h_48() .unfilled() - .child(Label::new("Unfilled ContentBox")), + .child(Label::new("Unfilled ContentBox")) + .into_any_element(), ) .grow(), ]) - .grow()] + .into_any_element() } } diff --git a/crates/ui/src/components/facepile.rs b/crates/ui/src/components/facepile.rs index 875b1dfb2aed1ae580cda216716c5b4ebeac22b8..d965bc598a457b777e787f219b0702aa5fe21074 100644 --- a/crates/ui/src/components/facepile.rs +++ b/crates/ui/src/components/facepile.rs @@ -1,4 +1,4 @@ -use crate::{prelude::*, Avatar}; +use crate::prelude::*; use gpui::{AnyElement, StyleRefinement}; use smallvec::SmallVec; @@ -60,60 +60,60 @@ impl RenderOnce for Facepile { } } -impl ComponentPreview for Facepile { - fn description() -> impl Into> { - "A facepile is a collection of faces stacked horizontally–\ - always with the leftmost face on top and descending in z-index.\ - \n\nFacepiles are used to display a group of people or things,\ - such as a list of participants in a collaboration session." - } - fn examples(_window: &mut Window, _: &mut App) -> Vec> { - let few_faces: [&'static str; 3] = [ - "https://avatars.githubusercontent.com/u/1714999?s=60&v=4", - "https://avatars.githubusercontent.com/u/67129314?s=60&v=4", - "https://avatars.githubusercontent.com/u/482957?s=60&v=4", - ]; +// impl ComponentPreview for Facepile { +// fn description() -> impl Into> { +// "A facepile is a collection of faces stacked horizontally–\ +// always with the leftmost face on top and descending in z-index.\ +// \n\nFacepiles are used to display a group of people or things,\ +// such as a list of participants in a collaboration session." +// } +// fn examples(_window: &mut Window, _: &mut App) -> Vec> { +// let few_faces: [&'static str; 3] = [ +// "https://avatars.githubusercontent.com/u/1714999?s=60&v=4", +// "https://avatars.githubusercontent.com/u/67129314?s=60&v=4", +// "https://avatars.githubusercontent.com/u/482957?s=60&v=4", +// ]; - let many_faces: [&'static str; 6] = [ - "https://avatars.githubusercontent.com/u/326587?s=60&v=4", - "https://avatars.githubusercontent.com/u/2280405?s=60&v=4", - "https://avatars.githubusercontent.com/u/1789?s=60&v=4", - "https://avatars.githubusercontent.com/u/67129314?s=60&v=4", - "https://avatars.githubusercontent.com/u/482957?s=60&v=4", - "https://avatars.githubusercontent.com/u/1714999?s=60&v=4", - ]; +// let many_faces: [&'static str; 6] = [ +// "https://avatars.githubusercontent.com/u/326587?s=60&v=4", +// "https://avatars.githubusercontent.com/u/2280405?s=60&v=4", +// "https://avatars.githubusercontent.com/u/1789?s=60&v=4", +// "https://avatars.githubusercontent.com/u/67129314?s=60&v=4", +// "https://avatars.githubusercontent.com/u/482957?s=60&v=4", +// "https://avatars.githubusercontent.com/u/1714999?s=60&v=4", +// ]; - vec![example_group_with_title( - "Examples", - vec![ - single_example( - "Few Faces", - Facepile::new( - few_faces - .iter() - .map(|&url| Avatar::new(url).into_any_element()) - .collect(), - ), - ), - single_example( - "Many Faces", - Facepile::new( - many_faces - .iter() - .map(|&url| Avatar::new(url).into_any_element()) - .collect(), - ), - ), - single_example( - "Custom Size", - Facepile::new( - few_faces - .iter() - .map(|&url| Avatar::new(url).size(px(24.)).into_any_element()) - .collect(), - ), - ), - ], - )] - } -} +// vec![example_group_with_title( +// "Examples", +// vec![ +// single_example( +// "Few Faces", +// Facepile::new( +// few_faces +// .iter() +// .map(|&url| Avatar::new(url).into_any_element()) +// .collect(), +// ), +// ), +// single_example( +// "Many Faces", +// Facepile::new( +// many_faces +// .iter() +// .map(|&url| Avatar::new(url).into_any_element()) +// .collect(), +// ), +// ), +// single_example( +// "Custom Size", +// Facepile::new( +// few_faces +// .iter() +// .map(|&url| Avatar::new(url).size(px(24.)).into_any_element()) +// .collect(), +// ), +// ), +// ], +// )] +// } +// } diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 4ea5ca9c5434a34f8fe1dbbc10e0460145e63d29..c23c41cbcf7119b2c368857b8ffe953c6f445b3d 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -7,17 +7,13 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; pub use decorated_icon::*; -use gpui::{img, svg, AnimationElement, Hsla, IntoElement, Rems, Transformation}; +use gpui::{img, svg, AnimationElement, AnyElement, Hsla, IntoElement, Rems, Transformation}; pub use icon_decoration::*; use serde::{Deserialize, Serialize}; use strum::{EnumIter, EnumString, IntoStaticStr}; use ui_macros::DerivePathStr; -use crate::{ - prelude::*, - traits::component_preview::{ComponentExample, ComponentPreview}, - Indicator, -}; +use crate::{prelude::*, Indicator}; #[derive(IntoElement)] pub enum AnyIcon { @@ -364,7 +360,7 @@ impl IconSource { } } -#[derive(IntoElement)] +#[derive(IntoElement, IntoComponent)] pub struct Icon { source: IconSource, color: Color, @@ -494,24 +490,41 @@ impl RenderOnce for IconWithIndicator { } impl ComponentPreview for Icon { - fn examples(_window: &mut Window, _cx: &mut App) -> Vec> { - let arrow_icons = vec![ - IconName::ArrowDown, - IconName::ArrowLeft, - IconName::ArrowRight, - IconName::ArrowUp, - IconName::ArrowCircle, - ]; - - vec![example_group_with_title( - "Arrow Icons", - arrow_icons - .into_iter() - .map(|icon| { - let name = format!("{:?}", icon).to_string(); - ComponentExample::new(name, Icon::new(icon)) - }) - .collect(), - )] + fn preview(_window: &mut Window, _cx: &App) -> AnyElement { + v_flex() + .gap_6() + .children(vec![ + example_group_with_title( + "Sizes", + vec![ + single_example("Default", Icon::new(IconName::Star).into_any_element()), + single_example( + "Small", + Icon::new(IconName::Star) + .size(IconSize::Small) + .into_any_element(), + ), + single_example( + "Large", + Icon::new(IconName::Star) + .size(IconSize::XLarge) + .into_any_element(), + ), + ], + ), + example_group_with_title( + "Colors", + vec![ + single_example("Default", Icon::new(IconName::Bell).into_any_element()), + single_example( + "Custom Color", + Icon::new(IconName::Bell) + .color(Color::Error) + .into_any_element(), + ), + ], + ), + ]) + .into_any_element() } } diff --git a/crates/ui/src/components/icon/decorated_icon.rs b/crates/ui/src/components/icon/decorated_icon.rs index 1a441bf6eae6011a49560fadc2ea93ae22d252f9..c973dc60961103aff4dd31286a82303d7496757b 100644 --- a/crates/ui/src/components/icon/decorated_icon.rs +++ b/crates/ui/src/components/icon/decorated_icon.rs @@ -1,10 +1,8 @@ -use gpui::{IntoElement, Point}; +use gpui::{AnyElement, IntoElement, Point}; -use crate::{ - prelude::*, traits::component_preview::ComponentPreview, IconDecoration, IconDecorationKind, -}; +use crate::{prelude::*, IconDecoration, IconDecorationKind}; -#[derive(IntoElement)] +#[derive(IntoElement, IntoComponent)] pub struct DecoratedIcon { icon: Icon, decoration: Option, @@ -27,12 +25,7 @@ impl RenderOnce for DecoratedIcon { } impl ComponentPreview for DecoratedIcon { - fn examples(_: &mut Window, cx: &mut App) -> Vec> { - let icon_1 = Icon::new(IconName::FileDoc); - let icon_2 = Icon::new(IconName::FileDoc); - let icon_3 = Icon::new(IconName::FileDoc); - let icon_4 = Icon::new(IconName::FileDoc); - + fn preview(_window: &mut Window, cx: &App) -> AnyElement { let decoration_x = IconDecoration::new( IconDecorationKind::X, cx.theme().colors().surface_background, @@ -66,22 +59,32 @@ impl ComponentPreview for DecoratedIcon { y: px(-2.), }); - let examples = vec![ - single_example("no_decoration", DecoratedIcon::new(icon_1, None)), - single_example( - "with_decoration", - DecoratedIcon::new(icon_2, Some(decoration_x)), - ), - single_example( - "with_decoration", - DecoratedIcon::new(icon_3, Some(decoration_triangle)), - ), - single_example( - "with_decoration", - DecoratedIcon::new(icon_4, Some(decoration_dot)), - ), - ]; - - vec![example_group(examples)] + v_flex() + .gap_6() + .children(vec![example_group_with_title( + "Decorations", + vec![ + single_example( + "No Decoration", + DecoratedIcon::new(Icon::new(IconName::FileDoc), None).into_any_element(), + ), + single_example( + "X Decoration", + DecoratedIcon::new(Icon::new(IconName::FileDoc), Some(decoration_x)) + .into_any_element(), + ), + single_example( + "Triangle Decoration", + DecoratedIcon::new(Icon::new(IconName::FileDoc), Some(decoration_triangle)) + .into_any_element(), + ), + single_example( + "Dot Decoration", + DecoratedIcon::new(Icon::new(IconName::FileDoc), Some(decoration_dot)) + .into_any_element(), + ), + ], + )]) + .into_any_element() } } diff --git a/crates/ui/src/components/icon/icon_decoration.rs b/crates/ui/src/components/icon/icon_decoration.rs index 75a04265f9be1dda9c363b21e61c9bb471de12bc..ba73e5a2cb36b656fa9596ba409382ba2371a9fb 100644 --- a/crates/ui/src/components/icon/icon_decoration.rs +++ b/crates/ui/src/components/icon/icon_decoration.rs @@ -1,8 +1,8 @@ use gpui::{svg, Hsla, IntoElement, Point}; -use strum::{EnumIter, EnumString, IntoEnumIterator, IntoStaticStr}; +use strum::{EnumIter, EnumString, IntoStaticStr}; use ui_macros::DerivePathStr; -use crate::{prelude::*, traits::component_preview::ComponentPreview}; +use crate::prelude::*; const ICON_DECORATION_SIZE: Pixels = px(11.); @@ -149,21 +149,3 @@ impl RenderOnce for IconDecoration { .child(background) } } - -impl ComponentPreview for IconDecoration { - fn examples(_: &mut Window, cx: &mut App) -> Vec> { - let all_kinds = IconDecorationKind::iter().collect::>(); - - let examples = all_kinds - .iter() - .map(|kind| { - single_example( - format!("{kind:?}"), - IconDecoration::new(*kind, cx.theme().colors().surface_background, cx), - ) - }) - .collect(); - - vec![example_group(examples)] - } -} diff --git a/crates/ui/src/components/indicator.rs b/crates/ui/src/components/indicator.rs index bb275cd9410e76175e3046b159423a75dc46646e..0cf4cab72eba998be7bf47f68d0c95ef704ba538 100644 --- a/crates/ui/src/components/indicator.rs +++ b/crates/ui/src/components/indicator.rs @@ -83,34 +83,3 @@ impl RenderOnce for Indicator { } } } - -impl ComponentPreview for Indicator { - fn description() -> impl Into> { - "An indicator visually represents a status or state." - } - - fn examples(_window: &mut Window, _: &mut App) -> Vec> { - vec![ - example_group_with_title( - "Types", - vec![ - single_example("Dot", Indicator::dot().color(Color::Info)), - single_example("Bar", Indicator::bar().color(Color::Player(2))), - single_example( - "Icon", - Indicator::icon(Icon::new(IconName::Check).color(Color::Success)), - ), - ], - ), - example_group_with_title( - "Examples", - vec![ - single_example("Info", Indicator::dot().color(Color::Info)), - single_example("Success", Indicator::dot().color(Color::Success)), - single_example("Warning", Indicator::dot().color(Color::Warning)), - single_example("Error", Indicator::dot().color(Color::Error)), - ], - ), - ] - } -} diff --git a/crates/ui/src/components/keybinding_hint.rs b/crates/ui/src/components/keybinding_hint.rs index 2239cf0790608e5fb7953a0fb631543ad11147ec..2abb93ea40b890568a6b4aa9517a800c8fd72035 100644 --- a/crates/ui/src/components/keybinding_hint.rs +++ b/crates/ui/src/components/keybinding_hint.rs @@ -1,6 +1,6 @@ use crate::{h_flex, prelude::*}; use crate::{ElevationIndex, KeyBinding}; -use gpui::{point, App, BoxShadow, IntoElement, Window}; +use gpui::{point, AnyElement, App, BoxShadow, IntoElement, Window}; use smallvec::smallvec; /// Represents a hint for a keybinding, optionally with a prefix and suffix. @@ -17,7 +17,7 @@ use smallvec::smallvec; /// .prefix("Save:") /// .size(Pixels::from(14.0)); /// ``` -#[derive(Debug, IntoElement, Clone)] +#[derive(Debug, IntoElement, IntoComponent)] pub struct KeybindingHint { prefix: Option, suffix: Option, @@ -206,102 +206,99 @@ impl RenderOnce for KeybindingHint { } impl ComponentPreview for KeybindingHint { - fn description() -> impl Into> { - "Used to display hint text for keyboard shortcuts. Can have a prefix and suffix." - } - - fn examples(window: &mut Window, _cx: &mut App) -> Vec> { - let home_fallback = gpui::KeyBinding::new("home", menu::SelectFirst, None); - let home = KeyBinding::for_action(&menu::SelectFirst, window) - .unwrap_or(KeyBinding::new(home_fallback)); - - let end_fallback = gpui::KeyBinding::new("end", menu::SelectLast, None); - let end = KeyBinding::for_action(&menu::SelectLast, window) - .unwrap_or(KeyBinding::new(end_fallback)); - + fn preview(window: &mut Window, _cx: &App) -> AnyElement { let enter_fallback = gpui::KeyBinding::new("enter", menu::Confirm, None); let enter = KeyBinding::for_action(&menu::Confirm, window) .unwrap_or(KeyBinding::new(enter_fallback)); - let escape_fallback = gpui::KeyBinding::new("escape", menu::Cancel, None); - let escape = KeyBinding::for_action(&menu::Cancel, window) - .unwrap_or(KeyBinding::new(escape_fallback)); - - vec![ - example_group_with_title( - "Basic", - vec![ - single_example( - "With Prefix", - KeybindingHint::with_prefix("Go to Start:", home.clone()), - ), - single_example( - "With Suffix", - KeybindingHint::with_suffix(end.clone(), "Go to End"), - ), - single_example( - "With Prefix and Suffix", - KeybindingHint::new(enter.clone()) - .prefix("Confirm:") - .suffix("Execute selected action"), - ), - ], - ), - example_group_with_title( - "Sizes", - vec![ - single_example( - "Small", - KeybindingHint::new(home.clone()) - .size(Pixels::from(12.0)) - .prefix("Small:"), - ), - single_example( - "Medium", - KeybindingHint::new(end.clone()) - .size(Pixels::from(16.0)) - .suffix("Medium"), - ), - single_example( - "Large", - KeybindingHint::new(enter.clone()) - .size(Pixels::from(20.0)) - .prefix("Large:") - .suffix("Size"), - ), - ], - ), - example_group_with_title( - "Elevations", - vec![ - single_example( - "Surface", - KeybindingHint::new(home.clone()) - .elevation(ElevationIndex::Surface) - .prefix("Surface:"), - ), - single_example( - "Elevated Surface", - KeybindingHint::new(end.clone()) - .elevation(ElevationIndex::ElevatedSurface) - .suffix("Elevated"), - ), - single_example( - "Editor Surface", - KeybindingHint::new(enter.clone()) - .elevation(ElevationIndex::EditorSurface) - .prefix("Editor:") - .suffix("Surface"), - ), - single_example( - "Modal Surface", - KeybindingHint::new(escape.clone()) - .elevation(ElevationIndex::ModalSurface) - .prefix("Modal:") - .suffix("Escape"), - ), - ], - ), - ] + v_flex() + .gap_6() + .children(vec![ + example_group_with_title( + "Basic", + vec![ + single_example( + "With Prefix", + KeybindingHint::with_prefix("Go to Start:", enter.clone()) + .into_any_element(), + ), + single_example( + "With Suffix", + KeybindingHint::with_suffix(enter.clone(), "Go to End") + .into_any_element(), + ), + single_example( + "With Prefix and Suffix", + KeybindingHint::new(enter.clone()) + .prefix("Confirm:") + .suffix("Execute selected action") + .into_any_element(), + ), + ], + ), + example_group_with_title( + "Sizes", + vec![ + single_example( + "Small", + KeybindingHint::new(enter.clone()) + .size(Pixels::from(12.0)) + .prefix("Small:") + .into_any_element(), + ), + single_example( + "Medium", + KeybindingHint::new(enter.clone()) + .size(Pixels::from(16.0)) + .suffix("Medium") + .into_any_element(), + ), + single_example( + "Large", + KeybindingHint::new(enter.clone()) + .size(Pixels::from(20.0)) + .prefix("Large:") + .suffix("Size") + .into_any_element(), + ), + ], + ), + example_group_with_title( + "Elevations", + vec![ + single_example( + "Surface", + KeybindingHint::new(enter.clone()) + .elevation(ElevationIndex::Surface) + .prefix("Surface:") + .into_any_element(), + ), + single_example( + "Elevated Surface", + KeybindingHint::new(enter.clone()) + .elevation(ElevationIndex::ElevatedSurface) + .suffix("Elevated") + .into_any_element(), + ), + single_example( + "Editor Surface", + KeybindingHint::new(enter.clone()) + .elevation(ElevationIndex::EditorSurface) + .prefix("Editor:") + .suffix("Surface") + .into_any_element(), + ), + single_example( + "Modal Surface", + KeybindingHint::new(enter.clone()) + .elevation(ElevationIndex::ModalSurface) + .prefix("Modal:") + .suffix("Enter") + .into_any_element(), + ), + ], + ), + ]) + .into_any_element() } } diff --git a/crates/ui/src/components/label/label.rs b/crates/ui/src/components/label/label.rs index ff2687d0478a1d5e2a804a7198e8cea91e4ef1a2..59243998df40dd5fc857f6f3d9c26dffc4972476 100644 --- a/crates/ui/src/components/label/label.rs +++ b/crates/ui/src/components/label/label.rs @@ -1,6 +1,6 @@ #![allow(missing_docs)] -use gpui::{App, StyleRefinement, Window}; +use gpui::{AnyElement, App, StyleRefinement, Window}; use crate::{prelude::*, LabelCommon, LabelLike, LabelSize, LineHeightStyle}; @@ -32,7 +32,7 @@ use crate::{prelude::*, LabelCommon, LabelLike, LabelSize, LineHeightStyle}; /// /// let my_label = Label::new("Deleted").strikethrough(true); /// ``` -#[derive(IntoElement)] +#[derive(IntoElement, IntoComponent)] pub struct Label { base: LabelLike, label: SharedString, @@ -184,3 +184,53 @@ impl RenderOnce for Label { self.base.child(self.label) } } + +impl ComponentPreview for Label { + fn preview(_window: &mut Window, _cx: &App) -> AnyElement { + v_flex() + .gap_6() + .children(vec![ + example_group_with_title( + "Sizes", + vec![ + single_example("Default", Label::new("Default Label").into_any_element()), + single_example("Small", Label::new("Small Label").size(LabelSize::Small).into_any_element()), + single_example("Large", Label::new("Large Label").size(LabelSize::Large).into_any_element()), + ], + ), + example_group_with_title( + "Colors", + vec![ + single_example("Default", Label::new("Default Color").into_any_element()), + single_example("Accent", Label::new("Accent Color").color(Color::Accent).into_any_element()), + single_example("Error", Label::new("Error Color").color(Color::Error).into_any_element()), + ], + ), + example_group_with_title( + "Styles", + vec![ + single_example("Default", Label::new("Default Style").into_any_element()), + single_example("Bold", Label::new("Bold Style").weight(gpui::FontWeight::BOLD).into_any_element()), + single_example("Italic", Label::new("Italic Style").italic(true).into_any_element()), + single_example("Strikethrough", Label::new("Strikethrough Style").strikethrough(true).into_any_element()), + single_example("Underline", Label::new("Underline Style").underline(true).into_any_element()), + ], + ), + example_group_with_title( + "Line Height Styles", + vec![ + single_example("Default", Label::new("Default Line Height").into_any_element()), + single_example("UI Label", Label::new("UI Label Line Height").line_height_style(LineHeightStyle::UiLabel).into_any_element()), + ], + ), + example_group_with_title( + "Special Cases", + vec![ + single_example("Single Line", Label::new("Single\nLine\nText").single_line().into_any_element()), + single_example("Text Ellipsis", Label::new("This is a very long text that should be truncated with an ellipsis").text_ellipsis().into_any_element()), + ], + ), + ]) + .into_any_element() + } +} diff --git a/crates/ui/src/components/radio.rs b/crates/ui/src/components/radio.rs index 6e98a10e0b08741594016da37ac3bdca1cb9cc9c..d7ee106d2d5bb0eae255ec59947836915065eb5b 100644 --- a/crates/ui/src/components/radio.rs +++ b/crates/ui/src/components/radio.rs @@ -4,9 +4,6 @@ use std::sync::Arc; use crate::prelude::*; -/// A [`Checkbox`] that has a [`Label`]. -/// -/// [`Checkbox`]: crate::components::Checkbox #[derive(IntoElement)] pub struct RadioWithLabel { id: ElementId, diff --git a/crates/ui/src/components/tab.rs b/crates/ui/src/components/tab.rs index 4d991bd6ce8482a1c123ebf3df58ab48f26b646f..362f1a41a59610678bd0f96603fadec34d47cb3c 100644 --- a/crates/ui/src/components/tab.rs +++ b/crates/ui/src/components/tab.rs @@ -27,7 +27,7 @@ pub enum TabCloseSide { End, } -#[derive(IntoElement)] +#[derive(IntoElement, IntoComponent)] pub struct Tab { div: Stateful
, selected: bool, @@ -171,3 +171,48 @@ impl RenderOnce for Tab { ) } } + +impl ComponentPreview for Tab { + fn preview(_window: &mut Window, _cx: &App) -> AnyElement { + v_flex() + .gap_6() + .children(vec![example_group_with_title( + "Variations", + vec![ + single_example( + "Default", + Tab::new("default").child("Default Tab").into_any_element(), + ), + single_example( + "Selected", + Tab::new("selected") + .toggle_state(true) + .child("Selected Tab") + .into_any_element(), + ), + single_example( + "First", + Tab::new("first") + .position(TabPosition::First) + .child("First Tab") + .into_any_element(), + ), + single_example( + "Middle", + Tab::new("middle") + .position(TabPosition::Middle(Ordering::Equal)) + .child("Middle Tab") + .into_any_element(), + ), + single_example( + "Last", + Tab::new("last") + .position(TabPosition::Last) + .child("Last Tab") + .into_any_element(), + ), + ], + )]) + .into_any_element() + } +} diff --git a/crates/ui/src/components/table.rs b/crates/ui/src/components/table.rs index c1918829791a6e14b7e9f8dbaeae9307c5439c23..aa955a6d089ee30d922b5cbc2104391e164a51ba 100644 --- a/crates/ui/src/components/table.rs +++ b/crates/ui/src/components/table.rs @@ -2,7 +2,7 @@ use crate::{prelude::*, Indicator}; use gpui::{div, AnyElement, FontWeight, IntoElement, Length}; /// A table component -#[derive(IntoElement)] +#[derive(IntoElement, IntoComponent)] pub struct Table { column_headers: Vec, rows: Vec>, @@ -152,88 +152,110 @@ where } impl ComponentPreview for Table { - fn description() -> impl Into> { - "Used for showing tabular data. Tables may show both text and elements in their cells." - } - - fn example_label_side() -> ExampleLabelSide { - ExampleLabelSide::Top - } - - fn examples(_window: &mut Window, _: &mut App) -> Vec> { - vec![ - example_group(vec![ - single_example( - "Simple Table", - Table::new(vec!["Name", "Age", "City"]) - .width(px(400.)) - .row(vec!["Alice", "28", "New York"]) - .row(vec!["Bob", "32", "San Francisco"]) - .row(vec!["Charlie", "25", "London"]), + fn preview(_window: &mut Window, _cx: &App) -> AnyElement { + v_flex() + .gap_6() + .children(vec![ + example_group_with_title( + "Basic Tables", + vec![ + single_example( + "Simple Table", + Table::new(vec!["Name", "Age", "City"]) + .width(px(400.)) + .row(vec!["Alice", "28", "New York"]) + .row(vec!["Bob", "32", "San Francisco"]) + .row(vec!["Charlie", "25", "London"]) + .into_any_element(), + ), + single_example( + "Two Column Table", + Table::new(vec!["Category", "Value"]) + .width(px(300.)) + .row(vec!["Revenue", "$100,000"]) + .row(vec!["Expenses", "$75,000"]) + .row(vec!["Profit", "$25,000"]) + .into_any_element(), + ), + ], + ), + example_group_with_title( + "Styled Tables", + vec![ + single_example( + "Default", + Table::new(vec!["Product", "Price", "Stock"]) + .width(px(400.)) + .row(vec!["Laptop", "$999", "In Stock"]) + .row(vec!["Phone", "$599", "Low Stock"]) + .row(vec!["Tablet", "$399", "Out of Stock"]) + .into_any_element(), + ), + single_example( + "Striped", + Table::new(vec!["Product", "Price", "Stock"]) + .width(px(400.)) + .striped() + .row(vec!["Laptop", "$999", "In Stock"]) + .row(vec!["Phone", "$599", "Low Stock"]) + .row(vec!["Tablet", "$399", "Out of Stock"]) + .row(vec!["Headphones", "$199", "In Stock"]) + .into_any_element(), + ), + ], ), - single_example( - "Two Column Table", - Table::new(vec!["Category", "Value"]) - .width(px(300.)) - .row(vec!["Revenue", "$100,000"]) - .row(vec!["Expenses", "$75,000"]) - .row(vec!["Profit", "$25,000"]), + example_group_with_title( + "Mixed Content Table", + vec![single_example( + "Table with Elements", + Table::new(vec!["Status", "Name", "Priority", "Deadline", "Action"]) + .width(px(840.)) + .row(vec![ + element_cell( + Indicator::dot().color(Color::Success).into_any_element(), + ), + string_cell("Project A"), + string_cell("High"), + string_cell("2023-12-31"), + element_cell( + Button::new("view_a", "View") + .style(ButtonStyle::Filled) + .full_width() + .into_any_element(), + ), + ]) + .row(vec![ + element_cell( + Indicator::dot().color(Color::Warning).into_any_element(), + ), + string_cell("Project B"), + string_cell("Medium"), + string_cell("2024-03-15"), + element_cell( + Button::new("view_b", "View") + .style(ButtonStyle::Filled) + .full_width() + .into_any_element(), + ), + ]) + .row(vec![ + element_cell( + Indicator::dot().color(Color::Error).into_any_element(), + ), + string_cell("Project C"), + string_cell("Low"), + string_cell("2024-06-30"), + element_cell( + Button::new("view_c", "View") + .style(ButtonStyle::Filled) + .full_width() + .into_any_element(), + ), + ]) + .into_any_element(), + )], ), - ]), - example_group(vec![single_example( - "Striped Table", - Table::new(vec!["Product", "Price", "Stock"]) - .width(px(600.)) - .striped() - .row(vec!["Laptop", "$999", "In Stock"]) - .row(vec!["Phone", "$599", "Low Stock"]) - .row(vec!["Tablet", "$399", "Out of Stock"]) - .row(vec!["Headphones", "$199", "In Stock"]), - )]), - example_group_with_title( - "Mixed Content Table", - vec![single_example( - "Table with Elements", - Table::new(vec!["Status", "Name", "Priority", "Deadline", "Action"]) - .width(px(840.)) - .row(vec![ - element_cell(Indicator::dot().color(Color::Success).into_any_element()), - string_cell("Project A"), - string_cell("High"), - string_cell("2023-12-31"), - element_cell( - Button::new("view_a", "View") - .style(ButtonStyle::Filled) - .full_width() - .into_any_element(), - ), - ]) - .row(vec![ - element_cell(Indicator::dot().color(Color::Warning).into_any_element()), - string_cell("Project B"), - string_cell("Medium"), - string_cell("2024-03-15"), - element_cell( - Button::new("view_b", "View") - .style(ButtonStyle::Filled) - .full_width() - .into_any_element(), - ), - ]) - .row(vec![ - element_cell(Indicator::dot().color(Color::Error).into_any_element()), - string_cell("Project C"), - string_cell("Low"), - string_cell("2024-06-30"), - element_cell( - Button::new("view_c", "View") - .style(ButtonStyle::Filled) - .full_width() - .into_any_element(), - ), - ]), - )], - ), - ] + ]) + .into_any_element() } } diff --git a/crates/ui/src/components/toggle.rs b/crates/ui/src/components/toggle.rs index 0413891811d4baf07d243233db50d2a42b8d3cff..c287f2f8466c4812c82dd1d1718c18d122e1ab19 100644 --- a/crates/ui/src/components/toggle.rs +++ b/crates/ui/src/components/toggle.rs @@ -1,5 +1,6 @@ use gpui::{ - div, hsla, prelude::*, AnyView, CursorStyle, ElementId, Hsla, IntoElement, Styled, Window, + div, hsla, prelude::*, AnyElement, AnyView, CursorStyle, ElementId, Hsla, IntoElement, Styled, + Window, }; use std::sync::Arc; @@ -38,7 +39,8 @@ pub enum ToggleStyle { /// Checkboxes are used for multiple choices, not for mutually exclusive choices. /// Each checkbox works independently from other checkboxes in the list, /// therefore checking an additional box does not affect any other selections. -#[derive(IntoElement)] +#[derive(IntoElement, IntoComponent)] +#[component(scope = "input")] pub struct Checkbox { id: ElementId, toggle_state: ToggleState, @@ -237,7 +239,8 @@ impl RenderOnce for Checkbox { } /// A [`Checkbox`] that has a [`Label`]. -#[derive(IntoElement)] +#[derive(IntoElement, IntoComponent)] +#[component(scope = "input")] pub struct CheckboxWithLabel { id: ElementId, label: Label, @@ -314,7 +317,8 @@ impl RenderOnce for CheckboxWithLabel { /// # Switch /// /// Switches are used to represent opposite states, such as enabled or disabled. -#[derive(IntoElement)] +#[derive(IntoElement, IntoComponent)] +#[component(scope = "input")] pub struct Switch { id: ElementId, toggle_state: ToggleState, @@ -446,285 +450,190 @@ impl RenderOnce for Switch { } impl ComponentPreview for Checkbox { - fn description() -> impl Into> { - "A checkbox lets people choose between a pair of opposing states, like enabled and disabled, using a different appearance to indicate each state." + fn preview(_window: &mut Window, _cx: &App) -> AnyElement { + v_flex() + .gap_6() + .children(vec![ + example_group_with_title( + "States", + vec![ + single_example( + "Unselected", + Checkbox::new("checkbox_unselected", ToggleState::Unselected) + .into_any_element(), + ), + single_example( + "Indeterminate", + Checkbox::new("checkbox_indeterminate", ToggleState::Indeterminate) + .into_any_element(), + ), + single_example( + "Selected", + Checkbox::new("checkbox_selected", ToggleState::Selected) + .into_any_element(), + ), + ], + ), + example_group_with_title( + "Styles", + vec![ + single_example( + "Default", + Checkbox::new("checkbox_default", ToggleState::Selected) + .into_any_element(), + ), + single_example( + "Filled", + Checkbox::new("checkbox_filled", ToggleState::Selected) + .fill() + .into_any_element(), + ), + single_example( + "ElevationBased", + Checkbox::new("checkbox_elevation", ToggleState::Selected) + .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)) + .into_any_element(), + ), + single_example( + "Custom Color", + Checkbox::new("checkbox_custom", ToggleState::Selected) + .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))) + .into_any_element(), + ), + ], + ), + example_group_with_title( + "Disabled", + vec![ + single_example( + "Unselected", + Checkbox::new("checkbox_disabled_unselected", ToggleState::Unselected) + .disabled(true) + .into_any_element(), + ), + single_example( + "Selected", + Checkbox::new("checkbox_disabled_selected", ToggleState::Selected) + .disabled(true) + .into_any_element(), + ), + ], + ), + example_group_with_title( + "With Label", + vec![single_example( + "Default", + Checkbox::new("checkbox_with_label", ToggleState::Selected) + .label("Always save on quit") + .into_any_element(), + )], + ), + ]) + .into_any_element() } +} - fn examples(_window: &mut Window, _: &mut App) -> Vec> { - vec![ - example_group_with_title( - "Default", - vec![ - single_example( - "Unselected", - Checkbox::new("checkbox_unselected", ToggleState::Unselected), - ), - single_example( - "Indeterminate", - Checkbox::new("checkbox_indeterminate", ToggleState::Indeterminate), - ), - single_example( - "Selected", - Checkbox::new("checkbox_selected", ToggleState::Selected), - ), - ], - ), - example_group_with_title( - "Default (Filled)", - vec![ - single_example( - "Unselected", - Checkbox::new("checkbox_unselected", ToggleState::Unselected).fill(), - ), - single_example( - "Indeterminate", - Checkbox::new("checkbox_indeterminate", ToggleState::Indeterminate).fill(), - ), - single_example( - "Selected", - Checkbox::new("checkbox_selected", ToggleState::Selected).fill(), - ), - ], - ), - example_group_with_title( - "ElevationBased", - vec![ - single_example( - "Unselected", - Checkbox::new("checkbox_unfilled_unselected", ToggleState::Unselected) - .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)), - ), - single_example( - "Indeterminate", - Checkbox::new( - "checkbox_unfilled_indeterminate", - ToggleState::Indeterminate, - ) - .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)), - ), - single_example( - "Selected", - Checkbox::new("checkbox_unfilled_selected", ToggleState::Selected) - .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)), - ), - ], - ), - example_group_with_title( - "ElevationBased (Filled)", - vec![ - single_example( - "Unselected", - Checkbox::new("checkbox_filled_unselected", ToggleState::Unselected) - .fill() - .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)), - ), - single_example( - "Indeterminate", - Checkbox::new("checkbox_filled_indeterminate", ToggleState::Indeterminate) - .fill() - .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)), - ), - single_example( - "Selected", - Checkbox::new("checkbox_filled_selected", ToggleState::Selected) - .fill() - .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)), - ), - ], - ), - example_group_with_title( - "Custom Color", - vec![ - single_example( - "Unselected", - Checkbox::new("checkbox_custom_unselected", ToggleState::Unselected) - .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))), - ), - single_example( - "Indeterminate", - Checkbox::new("checkbox_custom_indeterminate", ToggleState::Indeterminate) - .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))), - ), - single_example( - "Selected", - Checkbox::new("checkbox_custom_selected", ToggleState::Selected) - .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))), - ), - ], - ), - example_group_with_title( - "Custom Color (Filled)", - vec![ - single_example( - "Unselected", - Checkbox::new("checkbox_custom_filled_unselected", ToggleState::Unselected) - .fill() - .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))), - ), - single_example( - "Indeterminate", - Checkbox::new( - "checkbox_custom_filled_indeterminate", - ToggleState::Indeterminate, - ) - .fill() - .style(ToggleStyle::Custom(hsla( - 142.0 / 360., - 0.68, - 0.45, - 0.7, - ))), - ), - single_example( - "Selected", - Checkbox::new("checkbox_custom_filled_selected", ToggleState::Selected) - .fill() - .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))), - ), - ], - ), - example_group_with_title( - "Disabled", - vec![ - single_example( - "Unselected", - Checkbox::new("checkbox_disabled_unselected", ToggleState::Unselected) - .disabled(true), - ), - single_example( - "Indeterminate", - Checkbox::new( - "checkbox_disabled_indeterminate", - ToggleState::Indeterminate, - ) - .disabled(true), - ), - single_example( - "Selected", - Checkbox::new("checkbox_disabled_selected", ToggleState::Selected) - .disabled(true), - ), - ], - ), - example_group_with_title( - "Disabled (Filled)", +impl ComponentPreview for Switch { + fn preview(_window: &mut Window, _cx: &App) -> AnyElement { + v_flex() + .gap_6() + .children(vec![ + example_group_with_title( + "States", + vec![ + single_example( + "Off", + Switch::new("switch_off", ToggleState::Unselected) + .on_click(|_, _, _cx| {}) + .into_any_element(), + ), + single_example( + "On", + Switch::new("switch_on", ToggleState::Selected) + .on_click(|_, _, _cx| {}) + .into_any_element(), + ), + ], + ), + example_group_with_title( + "Disabled", + vec![ + single_example( + "Off", + Switch::new("switch_disabled_off", ToggleState::Unselected) + .disabled(true) + .into_any_element(), + ), + single_example( + "On", + Switch::new("switch_disabled_on", ToggleState::Selected) + .disabled(true) + .into_any_element(), + ), + ], + ), + example_group_with_title( + "With Label", + vec![ + single_example( + "Label", + Switch::new("switch_with_label", ToggleState::Selected) + .label("Always save on quit") + .into_any_element(), + ), + // TODO: Where did theme_preview_keybinding go? + // single_example( + // "Keybinding", + // Switch::new("switch_with_keybinding", ToggleState::Selected) + // .key_binding(theme_preview_keybinding("cmd-shift-e")) + // .into_any_element(), + // ), + ], + ), + ]) + .into_any_element() + } +} + +impl ComponentPreview for CheckboxWithLabel { + fn preview(_window: &mut Window, _cx: &App) -> AnyElement { + v_flex() + .gap_6() + .children(vec![example_group_with_title( + "States", vec![ single_example( "Unselected", - Checkbox::new( - "checkbox_disabled_filled_unselected", + CheckboxWithLabel::new( + "checkbox_with_label_unselected", + Label::new("Always save on quit"), ToggleState::Unselected, + |_, _, _| {}, ) - .fill() - .disabled(true), + .into_any_element(), ), single_example( "Indeterminate", - Checkbox::new( - "checkbox_disabled_filled_indeterminate", + CheckboxWithLabel::new( + "checkbox_with_label_indeterminate", + Label::new("Always save on quit"), ToggleState::Indeterminate, + |_, _, _| {}, ) - .fill() - .disabled(true), + .into_any_element(), ), single_example( "Selected", - Checkbox::new("checkbox_disabled_filled_selected", ToggleState::Selected) - .fill() - .disabled(true), - ), - ], - ), - ] - } -} - -impl ComponentPreview for Switch { - fn description() -> impl Into> { - "A switch toggles between two mutually exclusive states, typically used for enabling or disabling a setting." - } - - fn examples(_window: &mut Window, _cx: &mut App) -> Vec> { - vec![ - example_group_with_title( - "Default", - vec![ - single_example( - "Off", - Switch::new("switch_off", ToggleState::Unselected).on_click(|_, _, _cx| {}), - ), - single_example( - "On", - Switch::new("switch_on", ToggleState::Selected).on_click(|_, _, _cx| {}), - ), - ], - ), - example_group_with_title( - "Disabled", - vec![ - single_example( - "Off", - Switch::new("switch_disabled_off", ToggleState::Unselected).disabled(true), - ), - single_example( - "On", - Switch::new("switch_disabled_on", ToggleState::Selected).disabled(true), - ), - ], - ), - example_group_with_title( - "Label Permutations", - vec![ - single_example( - "Label", - Switch::new("switch_with_label", ToggleState::Selected) - .label("Always save on quit"), - ), - single_example( - "Keybinding", - Switch::new("switch_with_label", ToggleState::Selected) - .key_binding(theme_preview_keybinding("cmd-shift-e")), + CheckboxWithLabel::new( + "checkbox_with_label_selected", + Label::new("Always save on quit"), + ToggleState::Selected, + |_, _, _| {}, + ) + .into_any_element(), ), ], - ), - ] - } -} - -impl ComponentPreview for CheckboxWithLabel { - fn description() -> impl Into> { - "A checkbox with an associated label, allowing users to select an option while providing a descriptive text." - } - - fn examples(_window: &mut Window, _: &mut App) -> Vec> { - vec![example_group(vec![ - single_example( - "Unselected", - CheckboxWithLabel::new( - "checkbox_with_label_unselected", - Label::new("Always save on quit"), - ToggleState::Unselected, - |_, _, _| {}, - ), - ), - single_example( - "Indeterminate", - CheckboxWithLabel::new( - "checkbox_with_label_indeterminate", - Label::new("Always save on quit"), - ToggleState::Indeterminate, - |_, _, _| {}, - ), - ), - single_example( - "Selected", - CheckboxWithLabel::new( - "checkbox_with_label_selected", - Label::new("Always save on quit"), - ToggleState::Selected, - |_, _, _| {}, - ), - ), - ])] + )]) + .into_any_element() } } diff --git a/crates/ui/src/components/tooltip.rs b/crates/ui/src/components/tooltip.rs index 640bb8dc90389442821aa9c23c2b158f43a60757..c2a3ae69eb0d8e0ba9dce4b29827e5030d4d321e 100644 --- a/crates/ui/src/components/tooltip.rs +++ b/crates/ui/src/components/tooltip.rs @@ -1,12 +1,13 @@ #![allow(missing_docs)] -use gpui::{Action, AnyView, AppContext as _, FocusHandle, IntoElement, Render}; +use gpui::{Action, AnyElement, AnyView, AppContext as _, FocusHandle, IntoElement, Render}; use settings::Settings; use theme::ThemeSettings; use crate::prelude::*; use crate::{h_flex, v_flex, Color, KeyBinding, Label, LabelSize, StyledExt}; +#[derive(IntoComponent)] pub struct Tooltip { title: SharedString, meta: Option, @@ -204,3 +205,15 @@ impl Render for LinkPreview { }) } } + +impl ComponentPreview for Tooltip { + fn preview(_window: &mut Window, _cx: &App) -> AnyElement { + example_group(vec![single_example( + "Text only", + Button::new("delete-example", "Delete") + .tooltip(Tooltip::text("This is a tooltip!")) + .into_any_element(), + )]) + .into_any_element() + } +} diff --git a/crates/ui/src/prelude.rs b/crates/ui/src/prelude.rs index 6bb9d2cb400122a129cc54b694f56f4dcdf2e415..ba02dd5aeaccf264c739fb3ac517a75294a7f519 100644 --- a/crates/ui/src/prelude.rs +++ b/crates/ui/src/prelude.rs @@ -6,9 +6,11 @@ pub use gpui::{ InteractiveElement, ParentElement, Pixels, Rems, RenderOnce, SharedString, Styled, Window, }; +pub use component::{example_group, example_group_with_title, single_example, ComponentPreview}; +pub use ui_macros::IntoComponent; + pub use crate::styles::{rems_from_px, vh, vw, PlatformStyle, StyledTypography, TextSize}; pub use crate::traits::clickable::*; -pub use crate::traits::component_preview::*; pub use crate::traits::disableable::*; pub use crate::traits::fixed::*; pub use crate::traits::styled_ext::*; diff --git a/crates/ui/src/styles/typography.rs b/crates/ui/src/styles/typography.rs index 1f6c2e91127db79809cb3aedaadbfa234841c7e3..ec9c92cef9d2c467bc6e2c01caba28e3011e415e 100644 --- a/crates/ui/src/styles/typography.rs +++ b/crates/ui/src/styles/typography.rs @@ -1,5 +1,7 @@ +use crate::prelude::*; use gpui::{ - div, rems, App, IntoElement, ParentElement, Rems, RenderOnce, SharedString, Styled, Window, + div, rems, AnyElement, App, IntoElement, ParentElement, Rems, RenderOnce, SharedString, Styled, + Window, }; use settings::Settings; use theme::{ActiveTheme, ThemeSettings}; @@ -188,7 +190,7 @@ impl HeadlineSize { /// A headline element, used to emphasize some text and /// create a visual hierarchy. -#[derive(IntoElement)] +#[derive(IntoElement, IntoComponent)] pub struct Headline { size: HeadlineSize, text: SharedString, @@ -230,3 +232,44 @@ impl Headline { self } } + +impl ComponentPreview for Headline { + fn preview(_window: &mut Window, _cx: &App) -> AnyElement { + v_flex() + .gap_6() + .children(vec![example_group_with_title( + "Headline Sizes", + vec![ + single_example( + "XLarge", + Headline::new("XLarge Headline") + .size(HeadlineSize::XLarge) + .into_any_element(), + ), + single_example( + "Large", + Headline::new("Large Headline") + .size(HeadlineSize::Large) + .into_any_element(), + ), + single_example( + "Medium (Default)", + Headline::new("Medium Headline").into_any_element(), + ), + single_example( + "Small", + Headline::new("Small Headline") + .size(HeadlineSize::Small) + .into_any_element(), + ), + single_example( + "XSmall", + Headline::new("XSmall Headline") + .size(HeadlineSize::XSmall) + .into_any_element(), + ), + ], + )]) + .into_any_element() + } +} diff --git a/crates/ui/src/traits.rs b/crates/ui/src/traits.rs index 1b4d76171100c3b72bb76564c8af34827995c3ac..628c76aaddecaa291b3cfad2e6d16ccd6478c767 100644 --- a/crates/ui/src/traits.rs +++ b/crates/ui/src/traits.rs @@ -1,5 +1,4 @@ pub mod clickable; -pub mod component_preview; pub mod disableable; pub mod fixed; pub mod styled_ext; diff --git a/crates/ui/src/traits/component_preview.rs b/crates/ui/src/traits/component_preview.rs deleted file mode 100644 index 42c6cf9e4cf5561a7a31e6cfc6bc7a80d98630a6..0000000000000000000000000000000000000000 --- a/crates/ui/src/traits/component_preview.rs +++ /dev/null @@ -1,205 +0,0 @@ -#![allow(missing_docs)] -use crate::{prelude::*, KeyBinding}; -use gpui::{AnyElement, SharedString}; - -/// Which side of the preview to show labels on -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] -pub enum ExampleLabelSide { - /// Left side - Left, - /// Right side - Right, - #[default] - /// Top side - Top, - /// Bottom side - Bottom, -} - -/// Implement this trait to enable rich UI previews with metadata in the Theme Preview tool. -pub trait ComponentPreview: IntoElement { - fn title() -> &'static str { - std::any::type_name::() - } - - fn description() -> impl Into> { - None - } - - fn example_label_side() -> ExampleLabelSide { - ExampleLabelSide::default() - } - - fn examples(_window: &mut Window, _cx: &mut App) -> Vec>; - - fn custom_example(_window: &mut Window, _cx: &mut App) -> impl Into> { - None:: - } - - fn component_previews(window: &mut Window, cx: &mut App) -> Vec { - Self::examples(window, cx) - .into_iter() - .map(|example| Self::render_example_group(example)) - .collect() - } - - fn render_component_previews(window: &mut Window, cx: &mut App) -> AnyElement { - let title = Self::title(); - let (source, title) = title - .rsplit_once("::") - .map_or((None, title), |(s, t)| (Some(s), t)); - let description = Self::description().into(); - - v_flex() - .w_full() - .gap_6() - .p_4() - .border_1() - .border_color(cx.theme().colors().border) - .rounded_md() - .child( - v_flex() - .gap_1() - .child( - h_flex() - .gap_1() - .child(Headline::new(title).size(HeadlineSize::Small)) - .when_some(source, |this, source| { - this.child(Label::new(format!("({})", source)).color(Color::Muted)) - }), - ) - .when_some(description, |this, description| { - this.child( - div() - .text_ui_sm(cx) - .text_color(cx.theme().colors().text_muted) - .max_w(px(600.0)) - .child(description), - ) - }), - ) - .when_some( - Self::custom_example(window, cx).into(), - |this, custom_example| this.child(custom_example), - ) - .children(Self::component_previews(window, cx)) - .into_any_element() - } - - fn render_example_group(group: ComponentExampleGroup) -> AnyElement { - v_flex() - .gap_6() - .when(group.grow, |this| this.w_full().flex_1()) - .when_some(group.title, |this, title| { - this.child(Label::new(title).size(LabelSize::Small)) - }) - .child( - h_flex() - .w_full() - .gap_6() - .children(group.examples.into_iter().map(Self::render_example)) - .into_any_element(), - ) - .into_any_element() - } - - fn render_example(example: ComponentExample) -> AnyElement { - let base = div().flex(); - - let base = match Self::example_label_side() { - ExampleLabelSide::Right => base.flex_row(), - ExampleLabelSide::Left => base.flex_row_reverse(), - ExampleLabelSide::Bottom => base.flex_col(), - ExampleLabelSide::Top => base.flex_col_reverse(), - }; - - base.gap_1() - .when(example.grow, |this| this.flex_1()) - .child(example.element) - .child( - Label::new(example.variant_name) - .size(LabelSize::XSmall) - .color(Color::Muted), - ) - .into_any_element() - } -} - -/// A single example of a component. -pub struct ComponentExample { - variant_name: SharedString, - element: T, - grow: bool, -} - -impl ComponentExample { - /// Create a new example with the given variant name and example value. - pub fn new(variant_name: impl Into, example: T) -> Self { - Self { - variant_name: variant_name.into(), - element: example, - grow: false, - } - } - - /// Set the example to grow to fill the available horizontal space. - pub fn grow(mut self) -> Self { - self.grow = true; - self - } -} - -/// A group of component examples. -pub struct ComponentExampleGroup { - pub title: Option, - pub examples: Vec>, - pub grow: bool, -} - -impl ComponentExampleGroup { - /// Create a new group of examples with the given title. - pub fn new(examples: Vec>) -> Self { - Self { - title: None, - examples, - grow: false, - } - } - - /// Create a new group of examples with the given title. - pub fn with_title(title: impl Into, examples: Vec>) -> Self { - Self { - title: Some(title.into()), - examples, - grow: false, - } - } - - /// Set the group to grow to fill the available horizontal space. - pub fn grow(mut self) -> Self { - self.grow = true; - self - } -} - -/// Create a single example -pub fn single_example(variant_name: impl Into, example: T) -> ComponentExample { - ComponentExample::new(variant_name, example) -} - -/// Create a group of examples without a title -pub fn example_group(examples: Vec>) -> ComponentExampleGroup { - ComponentExampleGroup::new(examples) -} - -/// Create a group of examples with a title -pub fn example_group_with_title( - title: impl Into, - examples: Vec>, -) -> ComponentExampleGroup { - ComponentExampleGroup::with_title(title, examples) -} - -pub fn theme_preview_keybinding(keystrokes: &str) -> KeyBinding { - KeyBinding::new(gpui::KeyBinding::new(keystrokes, gpui::NoAction {}, None)) -} diff --git a/crates/ui_macros/Cargo.toml b/crates/ui_macros/Cargo.toml index 773c07d2383b62d62f948d986c275635fbfa2e08..cf9fef994f87ee89b9e70e6c6c3eee674f020846 100644 --- a/crates/ui_macros/Cargo.toml +++ b/crates/ui_macros/Cargo.toml @@ -13,7 +13,8 @@ path = "src/ui_macros.rs" proc-macro = true [dependencies] +convert_case.workspace = true +linkme.workspace = true proc-macro2.workspace = true quote.workspace = true syn.workspace = true -convert_case.workspace = true diff --git a/crates/ui_macros/src/derive_component.rs b/crates/ui_macros/src/derive_component.rs new file mode 100644 index 0000000000000000000000000000000000000000..5103d219c2ba5c7193c6ee8885ea8f71db772fb1 --- /dev/null +++ b/crates/ui_macros/src/derive_component.rs @@ -0,0 +1,97 @@ +use convert_case::{Case, Casing}; +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, DeriveInput, Lit, Meta, MetaList, MetaNameValue, NestedMeta}; + +pub fn derive_into_component(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let mut scope_val = None; + let mut description_val = None; + + for attr in &input.attrs { + if attr.path.is_ident("component") { + if let Ok(Meta::List(MetaList { nested, .. })) = attr.parse_meta() { + for item in nested { + if let NestedMeta::Meta(Meta::NameValue(MetaNameValue { + path, + lit: Lit::Str(s), + .. + })) = item + { + let ident = path.get_ident().map(|i| i.to_string()).unwrap_or_default(); + if ident == "scope" { + scope_val = Some(s.value()); + } else if ident == "description" { + description_val = Some(s.value()); + } + } + } + } + } + } + + let name = &input.ident; + + let scope_impl = if let Some(s) = scope_val { + quote! { + fn scope() -> Option<&'static str> { + Some(#s) + } + } + } else { + quote! { + fn scope() -> Option<&'static str> { + None + } + } + }; + + let description_impl = if let Some(desc) = description_val { + quote! { + fn description() -> Option<&'static str> { + Some(#desc) + } + } + } else { + quote! {} + }; + + let register_component_name = syn::Ident::new( + &format!( + "__register_component_{}", + Casing::to_case(&name.to_string(), Case::Snake) + ), + name.span(), + ); + let register_preview_name = syn::Ident::new( + &format!( + "__register_preview_{}", + Casing::to_case(&name.to_string(), Case::Snake) + ), + name.span(), + ); + + let expanded = quote! { + impl component::Component for #name { + #scope_impl + + fn name() -> &'static str { + stringify!(#name) + } + + #description_impl + } + + #[linkme::distributed_slice(component::__ALL_COMPONENTS)] + fn #register_component_name() { + component::register_component::<#name>(); + } + + #[linkme::distributed_slice(component::__ALL_PREVIEWS)] + fn #register_preview_name() { + component::register_preview::<#name>(); + } + }; + + expanded.into() +} diff --git a/crates/ui_macros/src/ui_macros.rs b/crates/ui_macros/src/ui_macros.rs index cd4b852766cb54f385fe4eda34c0b1c9ccc1c9b4..7898f226b037af6bb44b05b55dcb57e6f9584c9b 100644 --- a/crates/ui_macros/src/ui_macros.rs +++ b/crates/ui_macros/src/ui_macros.rs @@ -1,3 +1,4 @@ +mod derive_component; mod derive_path_str; mod dynamic_spacing; @@ -58,3 +59,27 @@ pub fn path_str(_args: TokenStream, input: TokenStream) -> TokenStream { pub fn derive_dynamic_spacing(input: TokenStream) -> TokenStream { dynamic_spacing::derive_spacing(input) } + +/// Derives the `Component` trait for a struct. +/// +/// This macro generates implementations for the `Component` trait and associated +/// registration functions for the component system. +/// +/// # Attributes +/// +/// - `#[component(scope = "...")]`: Required. Specifies the scope of the component. +/// - `#[component(description = "...")]`: Optional. Provides a description for the component. +/// +/// # Example +/// +/// ``` +/// use ui_macros::Component; +/// +/// #[derive(Component)] +/// #[component(scope = "toggle", description = "A element that can be toggled on and off")] +/// struct Checkbox; +/// ``` +#[proc_macro_derive(IntoComponent, attributes(component))] +pub fn derive_component(input: TokenStream) -> TokenStream { + derive_component::derive_into_component(input) +} diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 81bd40970e5871a393b3fbb8cca21f9ff22e3cdd..83ed9d5390a739d0775abe51d0926b4e369c8866 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -34,6 +34,7 @@ call.workspace = true client.workspace = true clock.workspace = true collections.workspace = true +component.workspace = true db.workspace = true derive_more.workspace = true fs.workspace = true diff --git a/crates/workspace/src/theme_preview.rs b/crates/workspace/src/theme_preview.rs index 656fb9a4aca2c8957290a10982068c398811c19b..da2d6b3ff196342865b951f535acfadf25deb75d 100644 --- a/crates/workspace/src/theme_preview.rs +++ b/crates/workspace/src/theme_preview.rs @@ -27,7 +27,6 @@ pub fn init(cx: &mut App) { enum ThemePreviewPage { Overview, Typography, - Components, } impl ThemePreviewPage { @@ -35,7 +34,6 @@ impl ThemePreviewPage { match self { Self::Overview => "Overview", Self::Typography => "Typography", - Self::Components => "Components", } } } @@ -64,9 +62,6 @@ impl ThemePreview { ThemePreviewPage::Typography => { self.render_typography_page(window, cx).into_any_element() } - ThemePreviewPage::Components => { - self.render_components_page(window, cx).into_any_element() - } } } } @@ -392,28 +387,6 @@ impl ThemePreview { ) } - fn render_components_page(&self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let layer = ElevationIndex::Surface; - - v_flex() - .id("theme-preview-components") - .overflow_scroll() - .size_full() - .gap_2() - .child(Button::render_component_previews(window, cx)) - .child(Checkbox::render_component_previews(window, cx)) - .child(CheckboxWithLabel::render_component_previews(window, cx)) - .child(ContentGroup::render_component_previews(window, cx)) - .child(DecoratedIcon::render_component_previews(window, cx)) - .child(Facepile::render_component_previews(window, cx)) - .child(Icon::render_component_previews(window, cx)) - .child(IconDecoration::render_component_previews(window, cx)) - .child(KeybindingHint::render_component_previews(window, cx)) - .child(Indicator::render_component_previews(window, cx)) - .child(Switch::render_component_previews(window, cx)) - .child(Table::render_component_previews(window, cx)) - } - fn render_page_nav(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { h_flex() .id("theme-preview-nav") diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index e4087fad4f50438e98bb89bf68b0e0087a10b707..78c950b535560bc63f5033d30f6940534694220d 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -148,6 +148,7 @@ actions!( Open, OpenFiles, OpenInTerminal, + OpenComponentPreview, ReloadActiveItem, SaveAs, SaveWithoutFormat, @@ -378,6 +379,7 @@ fn prompt_and_open_paths(app_state: Arc, options: PathPromptOptions, c pub fn init(app_state: Arc, cx: &mut App) { init_settings(cx); + component::init(); theme_preview::init(cx); cx.on_action(Workspace::close_global); diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 6106a382e17cc9c4a42a7a2a802090e5dd28705d..bf53db6d483870369bb94b81fb34b4618c8634eb 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -39,6 +39,7 @@ collab_ui.workspace = true collections.workspace = true command_palette.workspace = true command_palette_hooks.workspace = true +component_preview.workspace = true copilot.workspace = true db.workspace = true diagnostics.workspace = true @@ -54,8 +55,8 @@ file_icons.workspace = true fs.workspace = true futures.workspace = true git.workspace = true -git_ui.workspace = true git_hosting_providers.workspace = true +git_ui.workspace = true go_to_line.workspace = true gpui = { workspace = true, features = ["wayland", "x11", "font-kit"] } gpui_tokio.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 78cd4d19cdd8315c175bb4611b9bc0b0165952e0..cbd2519e602a362d9ba730e5a6785c022e0e5c70 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -490,6 +490,7 @@ fn main() { project_panel::init(Assets, cx); git_ui::git_panel::init(cx); outline_panel::init(Assets, cx); + component_preview::init(cx); tasks_ui::init(cx); snippets_ui::init(cx); channel::init(&app_state.client.clone(), app_state.user_store.clone(), cx); From cf74d653bdfda6d30e850849b5d5bb92e82b39df Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sun, 9 Feb 2025 23:29:29 +0200 Subject: [PATCH 29/42] Fix outline panel issues in a multi-worktree set-up (#24538) Closes https://github.com/zed-industries/zed/issues/22993 Properly calculates depth and maintains worktree order, when displaying multiple worktrees in the outline panel. Release Notes: - Fixed outline panel issues in a multi-worktree set-up --- crates/outline_panel/src/outline_panel.rs | 342 +++++++++++++++++++--- 1 file changed, 298 insertions(+), 44 deletions(-) diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 54e4a2bde0178f3c79b6edc4abecd3a58f6b53d4..b5b637ce04f88fe6ba75dd7d73645a3004c19e36 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -2,6 +2,7 @@ mod outline_panel_settings; use std::{ cmp, + collections::BTreeMap, hash::Hash, ops::Range, path::{Path, PathBuf, MAIN_SEPARATOR_STR}, @@ -2626,7 +2627,7 @@ impl OutlinePanel { .spawn(async move { let mut processed_external_buffers = HashSet::default(); let mut new_worktree_entries = - HashMap::>::default(); + BTreeMap::>::default(); let mut worktree_excerpts = HashMap::< WorktreeId, HashMap)>, @@ -3492,7 +3493,8 @@ impl OutlinePanel { .copied() .unwrap_or(0); while let Some(parent) = parent_dirs.last() { - if directory_entry.entry.path.starts_with(&parent.path) { + if !is_root && directory_entry.entry.path.starts_with(&parent.path) + { break; } parent_dirs.pop(); @@ -5156,6 +5158,7 @@ mod tests { use project::FakeFs; use search::project_search::{self, perform_project_search}; use serde_json::json; + use workspace::OpenVisible; use super::*; @@ -5212,7 +5215,7 @@ mod tests { }); }); - let all_matches = r#"/ + let all_matches = r#"/rust-analyzer/ crates/ ide/src/ inlay_hints/ @@ -5247,9 +5250,11 @@ mod tests { outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), select_first_in_all_matches( "search: match config.param_names_for_lifetime_elision_hints {" @@ -5261,9 +5266,11 @@ mod tests { outline_panel.select_parent(&SelectParent, window, cx); assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), select_first_in_all_matches("fn_lifetime_fn.rs") ); @@ -5277,12 +5284,14 @@ mod tests { outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), format!( - r#"/ + r#"/rust-analyzer/ crates/ ide/src/ inlay_hints/ @@ -5312,9 +5321,11 @@ mod tests { outline_panel.select_parent(&SelectParent, window, cx); assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), select_first_in_all_matches("inlay_hints/") ); @@ -5324,9 +5335,11 @@ mod tests { outline_panel.select_parent(&SelectParent, window, cx); assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), select_first_in_all_matches("ide/src/") ); @@ -5341,12 +5354,14 @@ mod tests { outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), format!( - r#"/ + r#"/rust-analyzer/ crates/ ide/src/{SELECTED_MARKER} rust-analyzer/src/ @@ -5367,9 +5382,11 @@ mod tests { outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), select_first_in_all_matches("ide/src/") ); @@ -5426,7 +5443,7 @@ mod tests { ); }); }); - let all_matches = r#"/ + let all_matches = r#"/rust-analyzer/ crates/ ide/src/ inlay_hints/ @@ -5453,9 +5470,11 @@ mod tests { outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, None, + cx, ), all_matches, ); @@ -5474,12 +5493,15 @@ mod tests { outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, None, + cx, ), all_matches .lines() + .skip(1) // `/rust-analyzer/` is a root entry with path `` and it will be filtered out .filter(|item| item.contains(filter_text)) .collect::>() .join("\n"), @@ -5497,9 +5519,11 @@ mod tests { outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, None, + cx, ), all_matches, ); @@ -5556,7 +5580,7 @@ mod tests { ); }); }); - let all_matches = r#"/ + let all_matches = r#"/rust-analyzer/ crates/ ide/src/ inlay_hints/ @@ -5598,9 +5622,11 @@ mod tests { outline_panel.update_in(cx, |outline_panel, window, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), + cx, ), select_first_in_all_matches(initial_outline_selection) ); @@ -5619,9 +5645,11 @@ mod tests { outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), + cx, ), select_first_in_all_matches(navigated_outline_selection) ); @@ -5655,9 +5683,11 @@ mod tests { outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), + cx, ), select_first_in_all_matches(next_navigated_outline_selection) ); @@ -5690,9 +5720,11 @@ mod tests { ); assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), + cx, ), "fn_lifetime_fn.rs <==== selected" ); @@ -5704,6 +5736,176 @@ mod tests { }); } + #[gpui::test] + async fn test_multiple_workrees(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/root", + json!({ + "one": { + "a.txt": "aaa aaa" + }, + "two": { + "b.txt": "a aaa" + } + + }), + ) + .await; + let project = Project::test(fs.clone(), [Path::new("/root/one")], cx).await; + let workspace = add_outline_panel(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let outline_panel = outline_panel(&workspace, cx); + outline_panel.update_in(cx, |outline_panel, window, cx| { + outline_panel.set_active(true, window, cx) + }); + + let items = workspace + .update(cx, |workspace, window, cx| { + workspace.open_paths( + vec![PathBuf::from("/root/two")], + OpenVisible::OnlyDirectories, + None, + window, + cx, + ) + }) + .unwrap() + .await; + assert_eq!(items.len(), 1, "Were opening another worktree directory"); + assert!( + items[0].is_none(), + "Directory should be opened successfully" + ); + + workspace + .update(cx, |workspace, window, cx| { + ProjectSearchView::deploy_search( + workspace, + &workspace::DeploySearch::default(), + window, + cx, + ) + }) + .unwrap(); + let search_view = workspace + .update(cx, |workspace, _, cx| { + workspace + .active_pane() + .read(cx) + .items() + .find_map(|item| item.downcast::()) + .expect("Project search view expected to appear after new search event trigger") + }) + .unwrap(); + + let query = "aaa"; + perform_project_search(&search_view, query, cx); + search_view.update(cx, |search_view, cx| { + search_view + .results_editor() + .update(cx, |results_editor, cx| { + assert_eq!( + results_editor.display_text(cx).match_indices(query).count(), + 3 + ); + }); + }); + + cx.executor() + .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); + cx.run_until_parked(); + outline_panel.update(cx, |outline_panel, cx| { + assert_eq!( + display_entries( + &project, + &snapshot(&outline_panel, cx), + &outline_panel.cached_entries, + outline_panel.selected_entry(), + cx, + ), + r#"/root/one/ + a.txt + search: aaa aaa <==== selected + search: aaa aaa +/root/two/ + b.txt + search: a aaa"# + ); + }); + + outline_panel.update_in(cx, |outline_panel, window, cx| { + outline_panel.select_prev(&SelectPrev, window, cx); + outline_panel.open(&Open, window, cx); + }); + cx.executor() + .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); + cx.run_until_parked(); + outline_panel.update(cx, |outline_panel, cx| { + assert_eq!( + display_entries( + &project, + &snapshot(&outline_panel, cx), + &outline_panel.cached_entries, + outline_panel.selected_entry(), + cx, + ), + r#"/root/one/ + a.txt <==== selected +/root/two/ + b.txt + search: a aaa"# + ); + }); + + outline_panel.update_in(cx, |outline_panel, window, cx| { + outline_panel.select_next(&SelectNext, window, cx); + outline_panel.open(&Open, window, cx); + }); + cx.executor() + .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); + cx.run_until_parked(); + outline_panel.update(cx, |outline_panel, cx| { + assert_eq!( + display_entries( + &project, + &snapshot(&outline_panel, cx), + &outline_panel.cached_entries, + outline_panel.selected_entry(), + cx, + ), + r#"/root/one/ + a.txt +/root/two/ <==== selected"# + ); + }); + + outline_panel.update_in(cx, |outline_panel, window, cx| { + outline_panel.open(&Open, window, cx); + }); + cx.executor() + .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); + cx.run_until_parked(); + outline_panel.update(cx, |outline_panel, cx| { + assert_eq!( + display_entries( + &project, + &snapshot(&outline_panel, cx), + &outline_panel.cached_entries, + outline_panel.selected_entry(), + cx, + ), + r#"/root/one/ + a.txt +/root/two/ <==== selected + b.txt + search: a aaa"# + ); + }); + } + #[gpui::test] async fn test_navigating_in_singleton(cx: &mut TestAppContext) { init_test(cx); @@ -5769,9 +5971,11 @@ struct OutlineEntryExcerpt { outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), indoc!( " @@ -5794,9 +5998,11 @@ outline: struct OutlineEntryExcerpt outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), indoc!( " @@ -5819,9 +6025,11 @@ outline: struct OutlineEntryExcerpt <==== selected outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), indoc!( " @@ -5844,9 +6052,11 @@ outline: struct OutlineEntryExcerpt outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), indoc!( " @@ -5869,9 +6079,11 @@ outline: struct OutlineEntryExcerpt outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), indoc!( " @@ -5894,9 +6106,11 @@ outline: struct OutlineEntryExcerpt outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), indoc!( " @@ -5919,9 +6133,11 @@ outline: struct OutlineEntryExcerpt <==== selected outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), indoc!( " @@ -5944,9 +6160,11 @@ outline: struct OutlineEntryExcerpt outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), indoc!( " @@ -5969,9 +6187,11 @@ outline: struct OutlineEntryExcerpt outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), indoc!( " @@ -5994,9 +6214,11 @@ outline: struct OutlineEntryExcerpt outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), indoc!( " @@ -6019,9 +6241,11 @@ outline: struct OutlineEntryExcerpt <==== selected outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), indoc!( " @@ -6123,11 +6347,13 @@ outline: struct OutlineEntryExcerpt outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), - r#"/ + r#"/frontend-project/ public/lottie/ syntax-tree.json search: { "something": "static" } <==== selected @@ -6158,11 +6384,13 @@ outline: struct OutlineEntryExcerpt outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), - r#"/ + r#"/frontend-project/ public/lottie/ syntax-tree.json search: { "something": "static" } @@ -6184,11 +6412,13 @@ outline: struct OutlineEntryExcerpt outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), - r#"/ + r#"/frontend-project/ public/lottie/ syntax-tree.json search: { "something": "static" } @@ -6214,11 +6444,13 @@ outline: struct OutlineEntryExcerpt outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), - r#"/ + r#"/frontend-project/ public/lottie/ syntax-tree.json search: { "something": "static" } @@ -6243,11 +6475,13 @@ outline: struct OutlineEntryExcerpt outline_panel.update(cx, |outline_panel, cx| { assert_eq!( display_entries( + &project, &snapshot(&outline_panel, cx), &outline_panel.cached_entries, - outline_panel.selected_entry() + outline_panel.selected_entry(), + cx, ), - r#"/ + r#"/frontend-project/ public/lottie/ syntax-tree.json search: { "something": "static" } @@ -6294,9 +6528,11 @@ outline: struct OutlineEntryExcerpt } fn display_entries( + project: &Entity, multi_buffer_snapshot: &MultiBufferSnapshot, cached_entries: &[CachedEntry], selected_entry: Option<&PanelEntry>, + cx: &mut App, ) -> String { let mut display_string = String::new(); for entry in cached_entries { @@ -6311,15 +6547,33 @@ outline: struct OutlineEntryExcerpt FsEntry::ExternalFile(_) => { panic!("Did not cover external files with tests") } - FsEntry::Directory(directory) => format!( - "{}/", - directory - .entry - .path - .file_name() - .map(|name| name.to_string_lossy().to_string()) - .unwrap_or_default() - ), + FsEntry::Directory(directory) => { + match project + .read(cx) + .worktree_for_id(directory.worktree_id, cx) + .and_then(|worktree| { + if worktree.read(cx).root_entry() == Some(&directory.entry.entry) { + Some(worktree.read(cx).abs_path()) + } else { + None + } + }) { + Some(root_path) => format!( + "{}/{}", + root_path.display(), + directory.entry.path.display(), + ), + None => format!( + "{}/", + directory + .entry + .path + .file_name() + .unwrap_or_default() + .to_string_lossy() + ), + } + } FsEntry::File(file) => file .entry .path From 1a133ab9d8cbfdafa8a4228025dbf5ff3e175a8b Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Sun, 9 Feb 2025 16:51:37 -0700 Subject: [PATCH 30/42] Settings/keymap backup path next to files + update notification messages (#24517) Before: ![image](https://github.com/user-attachments/assets/5b7d8677-b0db-4a66-ac30-e4751ba4182d) After: ![image](https://github.com/user-attachments/assets/94743bc2-2902-43a3-8d6e-e0e0e6e469ec) Release Notes: - N/A --- crates/paths/src/paths.rs | 12 +++ crates/settings/src/keymap_file.rs | 18 ++--- crates/settings/src/settings_store.rs | 27 ++++--- crates/workspace/src/notifications.rs | 16 ++++ crates/zed/src/zed.rs | 105 ++++++++++++++++---------- 5 files changed, 116 insertions(+), 62 deletions(-) diff --git a/crates/paths/src/paths.rs b/crates/paths/src/paths.rs index acb541aceabdab6d102158b34898b3d4e0d83502..fa6217ce690a9d471d3d1ef761f4e46e9f891e85 100644 --- a/crates/paths/src/paths.rs +++ b/crates/paths/src/paths.rs @@ -145,12 +145,24 @@ pub fn settings_file() -> &'static PathBuf { SETTINGS_FILE.get_or_init(|| config_dir().join("settings.json")) } +/// Returns the path to the `settings_backup.json` file. +pub fn settings_backup_file() -> &'static PathBuf { + static SETTINGS_FILE: OnceLock = OnceLock::new(); + SETTINGS_FILE.get_or_init(|| config_dir().join("settings_backup.json")) +} + /// Returns the path to the `keymap.json` file. pub fn keymap_file() -> &'static PathBuf { static KEYMAP_FILE: OnceLock = OnceLock::new(); KEYMAP_FILE.get_or_init(|| config_dir().join("keymap.json")) } +/// Returns the path to the `keymap_backup.json` file. +pub fn keymap_backup_file() -> &'static PathBuf { + static KEYMAP_FILE: OnceLock = OnceLock::new(); + KEYMAP_FILE.get_or_init(|| config_dir().join("keymap_backup.json")) +} + /// Returns the path to the `tasks.json` file. pub fn tasks_file() -> &'static PathBuf { static TASKS_FILE: OnceLock = OnceLock::new(); diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index 58c7915b915d6971fd0567502a64d7d26b58dd1b..fb967bde3f57b5d730108d72c2e84190b4777363 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -588,24 +588,24 @@ impl KeymapFile { let Some(new_text) = migrate_keymap(&old_text) else { return Ok(()); }; - let initial_path = paths::keymap_file().as_path(); - if fs.is_file(initial_path).await { - let backup_path = paths::home_dir().join(".zed_keymap_backup"); - fs.atomic_write(backup_path, old_text) + let keymap_path = paths::keymap_file().as_path(); + if fs.is_file(keymap_path).await { + fs.atomic_write(paths::keymap_backup_file().to_path_buf(), old_text) .await .with_context(|| { "Failed to create settings backup in home directory".to_string() })?; - let resolved_path = fs.canonicalize(initial_path).await.with_context(|| { - format!("Failed to canonicalize keymap path {:?}", initial_path) - })?; + let resolved_path = fs + .canonicalize(keymap_path) + .await + .with_context(|| format!("Failed to canonicalize keymap path {:?}", keymap_path))?; fs.atomic_write(resolved_path.clone(), new_text) .await .with_context(|| format!("Failed to write keymap to file {:?}", resolved_path))?; } else { - fs.atomic_write(initial_path.to_path_buf(), new_text) + fs.atomic_write(keymap_path.to_path_buf(), new_text) .await - .with_context(|| format!("Failed to write keymap to file {:?}", initial_path))?; + .with_context(|| format!("Failed to write keymap to file {:?}", keymap_path))?; } Ok(()) diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 2337f7fef3d0b1190231bcd020466d9224364391..6f69909b934432155f28d2ad696aa7464b87d516 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -415,11 +415,11 @@ impl SettingsStore { let new_text = cx.read_global(|store: &SettingsStore, cx| { store.new_text_for_update::(old_text, |content| update(content, cx)) })?; - let initial_path = paths::settings_file().as_path(); - if fs.is_file(initial_path).await { + let settings_path = paths::settings_file().as_path(); + if fs.is_file(settings_path).await { let resolved_path = - fs.canonicalize(initial_path).await.with_context(|| { - format!("Failed to canonicalize settings path {:?}", initial_path) + fs.canonicalize(settings_path).await.with_context(|| { + format!("Failed to canonicalize settings path {:?}", settings_path) })?; fs.atomic_write(resolved_path.clone(), new_text) @@ -428,10 +428,10 @@ impl SettingsStore { format!("Failed to write settings to file {:?}", resolved_path) })?; } else { - fs.atomic_write(initial_path.to_path_buf(), new_text) + fs.atomic_write(settings_path.to_path_buf(), new_text) .await .with_context(|| { - format!("Failed to write settings to file {:?}", initial_path) + format!("Failed to write settings to file {:?}", settings_path) })?; } @@ -1011,17 +1011,16 @@ impl SettingsStore { let Some(new_text) = migrate_settings(&old_text) else { return anyhow::Ok(()); }; - let initial_path = paths::settings_file().as_path(); - if fs.is_file(initial_path).await { - let backup_path = paths::home_dir().join(".zed_settings_backup"); - fs.atomic_write(backup_path, old_text) + let settings_path = paths::settings_file().as_path(); + if fs.is_file(settings_path).await { + fs.atomic_write(paths::settings_backup_file().to_path_buf(), old_text) .await .with_context(|| { "Failed to create settings backup in home directory".to_string() })?; let resolved_path = - fs.canonicalize(initial_path).await.with_context(|| { - format!("Failed to canonicalize settings path {:?}", initial_path) + fs.canonicalize(settings_path).await.with_context(|| { + format!("Failed to canonicalize settings path {:?}", settings_path) })?; fs.atomic_write(resolved_path.clone(), new_text) .await @@ -1029,10 +1028,10 @@ impl SettingsStore { format!("Failed to write settings to file {:?}", resolved_path) })?; } else { - fs.atomic_write(initial_path.to_path_buf(), new_text) + fs.atomic_write(settings_path.to_path_buf(), new_text) .await .with_context(|| { - format!("Failed to write settings to file {:?}", initial_path) + format!("Failed to write settings to file {:?}", settings_path) })?; } anyhow::Ok(()) diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index ad491910d06fc0e4fc5cf2cddd46ca2739b16c2e..797c9754edd28ea9a96f33b9663d1cfa5aca3760 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -448,6 +448,14 @@ pub mod simple_message_notification { self } + pub fn primary_on_click_arc(mut self, on_click: Arc) -> Self + where + F: 'static + Fn(&mut Window, &mut Context), + { + self.primary_on_click = Some(on_click); + self + } + pub fn secondary_message(mut self, message: S) -> Self where S: Into, @@ -474,6 +482,14 @@ pub mod simple_message_notification { self } + pub fn secondary_on_click_arc(mut self, on_click: Arc) -> Self + where + F: 'static + Fn(&mut Window, &mut Context), + { + self.secondary_on_click = Some(on_click); + self + } + pub fn more_info_message(mut self, message: S) -> Self where S: Into, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 9d4cb83e08bddcdb6c686e867baf8bce46bf7efa..7fe30db8fec7250d6f6c09c4c683d081c559f0de 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1217,25 +1217,29 @@ fn show_keymap_migration_notification_if_needed( if !KeymapFile::should_migrate_keymap(keymap_file) { return false; } - show_app_notification(notification_id, cx, move |cx| { - cx.new(move |_cx| { - let message = "A newer version of Zed has simplified several keymaps. Your existing keymaps may be deprecated. You can migrate them by clicking below. A backup will be created in your home directory."; - let button_text = "Backup and Migrate Keymap"; - MessageNotification::new_from_builder(move |_, _| { - gpui::div().text_xs().child(message).into_any() - }) - .primary_message(button_text) - .primary_on_click(move |_, cx| { - let fs = ::global(cx); - cx.spawn(move |weak_notification, mut cx| async move { - KeymapFile::migrate_keymap(fs).await.ok(); - weak_notification.update(&mut cx, |_, cx| { + let message = MarkdownString(format!( + "Keymap migration needed, as the format for some actions has changed. \ + You can migrate your keymap by clicking below. A backup will be created at {}.", + MarkdownString::inline_code(&paths::keymap_backup_file().to_string_lossy()) + )); + show_markdown_app_notification( + notification_id, + message, + "Backup and Migrate Keymap".into(), + move |_, cx| { + let fs = ::global(cx); + cx.spawn(move |weak_notification, mut cx| async move { + KeymapFile::migrate_keymap(fs).await.ok(); + weak_notification + .update(&mut cx, |_, cx| { cx.emit(DismissEvent); - }).ok(); - }).detach(); + }) + .ok(); }) - }) - }); + .detach(); + }, + cx, + ); return true; } @@ -1247,33 +1251,55 @@ fn show_settings_migration_notification_if_needed( if !SettingsStore::should_migrate_settings(&settings) { return; } - show_app_notification(notification_id, cx, move |cx| { - cx.new(move |_cx| { - let message = "A newer version of Zed has updated some settings. Your existing settings may be deprecated. You can migrate them by clicking below. A backup will be created in your home directory."; - let button_text = "Backup and Migrate Settings"; - MessageNotification::new_from_builder(move |_, _| { - gpui::div().text_xs().child(message).into_any() - }) - .primary_message(button_text) - .primary_on_click(move |_, cx| { - let fs = ::global(cx); - cx.update_global(|store: &mut SettingsStore, _| store.migrate_settings(fs)); - cx.emit(DismissEvent); - }) - }) - }); + let message = MarkdownString(format!( + "Settings migration needed, as the format for some settings has changed. \ + You can migrate your settings by clicking below. A backup will be created at {}.", + MarkdownString::inline_code(&paths::settings_backup_file().to_string_lossy()) + )); + show_markdown_app_notification( + notification_id, + message, + "Backup and Migrate Settings".into(), + move |_, cx| { + let fs = ::global(cx); + cx.update_global(|store: &mut SettingsStore, _| store.migrate_settings(fs)); + cx.emit(DismissEvent); + }, + cx, + ); } fn show_keymap_file_load_error( notification_id: NotificationId, - markdown_error_message: MarkdownString, + error_message: MarkdownString, cx: &mut App, ) { + show_markdown_app_notification( + notification_id.clone(), + error_message, + "Open Keymap File".into(), + |window, cx| { + window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx); + cx.emit(DismissEvent); + }, + cx, + ) +} + +fn show_markdown_app_notification( + notification_id: NotificationId, + message: MarkdownString, + primary_button_message: SharedString, + primary_button_on_click: F, + cx: &mut App, +) where + F: 'static + Send + Sync + Fn(&mut Window, &mut Context), +{ let parsed_markdown = cx.background_executor().spawn(async move { let file_location_directory = None; let language_registry = None; markdown_preview::markdown_parser::parse_markdown( - &markdown_error_message.0, + &message.0, file_location_directory, language_registry, ) @@ -1282,10 +1308,14 @@ fn show_keymap_file_load_error( cx.spawn(move |cx| async move { let parsed_markdown = Arc::new(parsed_markdown.await); + let primary_button_message = primary_button_message.clone(); + let primary_button_on_click = Arc::new(primary_button_on_click); cx.update(|cx| { show_app_notification(notification_id, cx, move |cx| { let workspace_handle = cx.entity().downgrade(); let parsed_markdown = parsed_markdown.clone(); + let primary_button_message = primary_button_message.clone(); + let primary_button_on_click = primary_button_on_click.clone(); cx.new(move |_cx| { MessageNotification::new_from_builder(move |window, cx| { gpui::div() @@ -1298,11 +1328,8 @@ fn show_keymap_file_load_error( )) .into_any() }) - .primary_message("Open Keymap File") - .primary_on_click(|window, cx| { - window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx); - cx.emit(DismissEvent); - }) + .primary_message(primary_button_message) + .primary_on_click_arc(primary_button_on_click) }) }) }) From 6f7f0f30e280b0d896668b27b8be62b4c212f9c7 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 10 Feb 2025 02:16:12 +0200 Subject: [PATCH 31/42] Fix hover tooltips appearing after related element is pressed (#24540) Closes https://github.com/zed-industries/zed/issues/23894 Reworks all trigger declarations from `.trigger(element.tooltip(tooltip))` into `.trigger_with_tooltip(element, tooltip)` , with new API disallowing simultaneous trigger and tooltip display. All existing `.trigger(` calls were replaced, except 2 not applicable (in dock.rs and pane.rs), 15 left as ones without tooltips, and 2 unchanged places in `inline_completion_button.rs`, where https://github.com/zed-industries/zed/blob/0f7bb2e9fd6dc1fe3f0127de19df372f75ad0c4f/crates/inline_completion_button/src/inline_completion_button.rs#L311-L319 `with_animation` does not allow us to simply use the same approach. Release Notes: - Fixed hover tooltips appearing after related element is pressed --------- Co-authored-by: Danilo Leal --- crates/assistant/src/assistant_panel.rs | 6 +-- crates/assistant/src/inline_assistant.rs | 32 +++++++-------- .../src/terminal_inline_assistant.rs | 32 +++++++-------- .../src/assistant_model_selector.rs | 18 ++++----- crates/assistant2/src/assistant_panel.rs | 12 +++--- crates/assistant2/src/context_strip.rs | 28 ++++++------- .../src/context_editor.rs | 12 +++--- .../src/slash_command_picker.rs | 25 +++++++++--- crates/editor/src/hunk_diff.rs | 15 +------ crates/git_ui/src/git_panel.rs | 1 + crates/git_ui/src/repository_selector.rs | 27 +++++++++---- .../src/inline_completion_button.rs | 39 +++++-------------- .../src/language_model_selector.rs | 29 ++++++++++---- crates/repl/src/components/kernel_options.rs | 25 +++++++++--- crates/terminal_view/src/terminal_panel.rs | 13 +++---- crates/title_bar/src/application_menu.rs | 8 ++-- crates/title_bar/src/title_bar.rs | 13 +++---- crates/ui/src/components/popover_menu.rs | 26 ++++++++++++- crates/workspace/src/pane.rs | 13 +++---- crates/zed/src/zed/quick_action_bar.rs | 16 +++----- .../zed/src/zed/quick_action_bar/repl_menu.rs | 8 ++-- 21 files changed, 218 insertions(+), 180 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index b3b11fa9c737efab43428e99e613ce94ed79824a..d5e164358974cda9754d590873a6de098826e1a8 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -250,10 +250,10 @@ impl AssistantPanel { ) .child( PopoverMenu::new("assistant-panel-popover-menu") - .trigger( + .trigger_with_tooltip( IconButton::new("menu", IconName::EllipsisVertical) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text("Toggle Assistant Menu")), + .icon_size(IconSize::Small), + Tooltip::text("Toggle Assistant Menu"), ) .menu(move |window, cx| { let zoom_label = if _pane.read(cx).is_zoomed() { diff --git a/crates/assistant/src/inline_assistant.rs b/crates/assistant/src/inline_assistant.rs index 53f142c029fb08361cf69c9471a41bbb7e3fb2cb..286440f9896a7f22e2cafed614861f09ee64e921 100644 --- a/crates/assistant/src/inline_assistant.rs +++ b/crates/assistant/src/inline_assistant.rs @@ -1595,22 +1595,22 @@ impl Render for PromptEditor { IconButton::new("context", IconName::SettingsAlt) .shape(IconButtonShape::Square) .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .tooltip(move |window, cx| { - Tooltip::with_meta( - format!( - "Using {}", - LanguageModelRegistry::read_global(cx) - .active_model() - .map(|model| model.name().0) - .unwrap_or_else(|| "No model selected".into()), - ), - None, - "Change Model", - window, - cx, - ) - }), + .icon_color(Color::Muted), + move |window, cx| { + Tooltip::with_meta( + format!( + "Using {}", + LanguageModelRegistry::read_global(cx) + .active_model() + .map(|model| model.name().0) + .unwrap_or_else(|| "No model selected".into()), + ), + None, + "Change Model", + window, + cx, + ) + }, )) .map(|el| { let CodegenStatus::Error(error) = self.codegen.read(cx).status(cx) else { diff --git a/crates/assistant/src/terminal_inline_assistant.rs b/crates/assistant/src/terminal_inline_assistant.rs index 4547ea8e679e444b6503f0edada306c8a2ea6597..a7f1d1966769306152c0eb7971535edbf68b42bb 100644 --- a/crates/assistant/src/terminal_inline_assistant.rs +++ b/crates/assistant/src/terminal_inline_assistant.rs @@ -646,22 +646,22 @@ impl Render for PromptEditor { IconButton::new("context", IconName::SettingsAlt) .shape(IconButtonShape::Square) .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .tooltip(move |window, cx| { - Tooltip::with_meta( - format!( - "Using {}", - LanguageModelRegistry::read_global(cx) - .active_model() - .map(|model| model.name().0) - .unwrap_or_else(|| "No model selected".into()), - ), - None, - "Change Model", - window, - cx, - ) - }), + .icon_color(Color::Muted), + move |window, cx| { + Tooltip::with_meta( + format!( + "Using {}", + LanguageModelRegistry::read_global(cx) + .active_model() + .map(|model| model.name().0) + .unwrap_or_else(|| "No model selected".into()), + ), + None, + "Change Model", + window, + cx, + ) + }, )) .children( if let CodegenStatus::Error(error) = &self.codegen.read(cx).status { diff --git a/crates/assistant2/src/assistant_model_selector.rs b/crates/assistant2/src/assistant_model_selector.rs index cca0454bf17ad74c9f87887d995b1ceeccf8ac1b..0308757e59298d9a86f27e1efdd30fdf7d654ae8 100644 --- a/crates/assistant2/src/assistant_model_selector.rs +++ b/crates/assistant2/src/assistant_model_selector.rs @@ -74,16 +74,16 @@ impl Render for AssistantModelSelector { .color(Color::Muted) .size(IconSize::XSmall), ), + ), + move |window, cx| { + Tooltip::for_action_in( + "Change Model", + &ToggleModelSelector, + &focus_handle, + window, + cx, ) - .tooltip(move |window, cx| { - Tooltip::for_action_in( - "Change Model", - &ToggleModelSelector, - &focus_handle, - window, - cx, - ) - }), + }, ) .with_handle(self.menu_handle.clone()) } diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index 45f3d15a81484b6aa7300257cde9e01d7051fa26..bfdeeb545bb939bc93f73cc5891e86c9208b6e45 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -660,11 +660,11 @@ impl AssistantPanel { .gap(DynamicSpacing::Base02.rems(cx)) .child( PopoverMenu::new("assistant-toolbar-new-popover-menu") - .trigger( + .trigger_with_tooltip( IconButton::new("new", IconName::Plus) .icon_size(IconSize::Small) - .style(ButtonStyle::Subtle) - .tooltip(Tooltip::text("New…")), + .style(ButtonStyle::Subtle), + Tooltip::text("New…"), ) .anchor(Corner::TopRight) .with_handle(self.new_item_context_menu_handle.clone()) @@ -677,11 +677,11 @@ impl AssistantPanel { ) .child( PopoverMenu::new("assistant-toolbar-history-popover-menu") - .trigger( + .trigger_with_tooltip( IconButton::new("open-history", IconName::HistoryRerun) .icon_size(IconSize::Small) - .style(ButtonStyle::Subtle) - .tooltip(Tooltip::text("History…")), + .style(ButtonStyle::Subtle), + Tooltip::text("History…"), ) .anchor(Corner::TopRight) .with_handle(self.open_history_context_menu_handle.clone()) diff --git a/crates/assistant2/src/context_strip.rs b/crates/assistant2/src/context_strip.rs index d7b1503713a9e63b21d9b90d75e65bf39e0360b0..317eaad8a1547d0b0de2a2c0b0b833966ceebb39 100644 --- a/crates/assistant2/src/context_strip.rs +++ b/crates/assistant2/src/context_strip.rs @@ -411,22 +411,22 @@ impl Render for ContextStrip { Some(context_picker.clone()) }) - .trigger( + .trigger_with_tooltip( IconButton::new("add-context", IconName::Plus) .icon_size(IconSize::Small) - .style(ui::ButtonStyle::Filled) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Add Context", - &ToggleContextPicker, - &focus_handle, - window, - cx, - ) - } - }), + .style(ui::ButtonStyle::Filled), + { + let focus_handle = focus_handle.clone(); + move |window, cx| { + Tooltip::for_action_in( + "Add Context", + &ToggleContextPicker, + &focus_handle, + window, + cx, + ) + } + }, ) .attach(gpui::Corner::TopLeft) .anchor(gpui::Corner::BottomLeft) diff --git a/crates/assistant_context_editor/src/context_editor.rs b/crates/assistant_context_editor/src/context_editor.rs index 290cff13fae047039d32ab6987f2d4e2ab69e002..fcfce741c1e8125fd241b99c5791565e3262c943 100644 --- a/crates/assistant_context_editor/src/context_editor.rs +++ b/crates/assistant_context_editor/src/context_editor.rs @@ -2359,8 +2359,8 @@ impl ContextEditor { .icon(IconName::Plus) .icon_size(IconSize::Small) .icon_color(Color::Muted) - .icon_position(IconPosition::Start) - .tooltip(Tooltip::text("Type / to insert via keyboard")), + .icon_position(IconPosition::Start), + Tooltip::text("Type / to insert via keyboard"), ) } @@ -3323,10 +3323,10 @@ impl Render for ContextEditorToolbarItem { .color(Color::Muted) .size(IconSize::XSmall), ), - ) - .tooltip(move |window, cx| { - Tooltip::for_action("Change Model", &ToggleModelSelector, window, cx) - }), + ), + move |window, cx| { + Tooltip::for_action("Change Model", &ToggleModelSelector, window, cx) + }, ) .with_handle(self.language_model_selector_menu_handle.clone()), ) diff --git a/crates/assistant_context_editor/src/slash_command_picker.rs b/crates/assistant_context_editor/src/slash_command_picker.rs index 373e5f09ddcb7f57b21d1d1d03a0277d84ae24a0..3bdc3160300eca2ec71f349c9905145bb9c5f49f 100644 --- a/crates/assistant_context_editor/src/slash_command_picker.rs +++ b/crates/assistant_context_editor/src/slash_command_picker.rs @@ -1,17 +1,22 @@ use std::sync::Arc; use assistant_slash_command::SlashCommandWorkingSet; -use gpui::{AnyElement, DismissEvent, SharedString, Task, WeakEntity}; +use gpui::{AnyElement, AnyView, DismissEvent, SharedString, Task, WeakEntity}; use picker::{Picker, PickerDelegate, PickerEditorPosition}; use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger, Tooltip}; use crate::context_editor::ContextEditor; #[derive(IntoElement)] -pub(super) struct SlashCommandSelector { +pub(super) struct SlashCommandSelector +where + T: PopoverTrigger + ButtonCommon, + TT: Fn(&mut Window, &mut App) -> AnyView + 'static, +{ working_set: Arc, active_context_editor: WeakEntity, trigger: T, + tooltip: TT, } #[derive(Clone)] @@ -48,16 +53,22 @@ pub(crate) struct SlashCommandDelegate { selected_index: usize, } -impl SlashCommandSelector { +impl SlashCommandSelector +where + T: PopoverTrigger + ButtonCommon, + TT: Fn(&mut Window, &mut App) -> AnyView + 'static, +{ pub(crate) fn new( working_set: Arc, active_context_editor: WeakEntity, trigger: T, + tooltip: TT, ) -> Self { SlashCommandSelector { working_set, active_context_editor, trigger, + tooltip, } } } @@ -241,7 +252,11 @@ impl PickerDelegate for SlashCommandDelegate { } } -impl RenderOnce for SlashCommandSelector { +impl RenderOnce for SlashCommandSelector +where + T: PopoverTrigger + ButtonCommon, + TT: Fn(&mut Window, &mut App) -> AnyView + 'static, +{ fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let all_models = self .working_set @@ -322,7 +337,7 @@ impl RenderOnce for SlashCommandSelector { .ok(); PopoverMenu::new("model-switcher") .menu(move |_window, _cx| Some(picker_view.clone())) - .trigger(self.trigger) + .trigger_with_tooltip(self.trigger, self.tooltip) .attach(gpui::Corner::TopLeft) .anchor(gpui::Corner::BottomLeft) .offset(gpui::Point { diff --git a/crates/editor/src/hunk_diff.rs b/crates/editor/src/hunk_diff.rs index 8bed3e2ccb8054427e35db0ba264c23152cafcef..03dac81d653be336694e04e387d5a6c7cbadbd61 100644 --- a/crates/editor/src/hunk_diff.rs +++ b/crates/editor/src/hunk_diff.rs @@ -763,7 +763,7 @@ impl Editor { this.child({ let focus = editor.focus_handle(cx); PopoverMenu::new("hunk-controls-dropdown") - .trigger( + .trigger_with_tooltip( IconButton::new( "toggle_editor_selections_icon", IconName::EllipsisVertical, @@ -774,19 +774,8 @@ impl Editor { .toggle_state( hunk_controls_menu_handle .is_deployed(), - ) - .when( - !hunk_controls_menu_handle - .is_deployed(), - |this| { - this.tooltip(|_, cx| { - Tooltip::simple( - "Hunk Controls", - cx, - ) - }) - }, ), + Tooltip::simple("Hunk Controls", cx), ) .anchor(Corner::TopRight) .with_handle(hunk_controls_menu_handle) diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index d7aa40f8d9d6eb2ed0130333292878d518089bb9..c92eb56e522e433ccb0ae3ec1423d6128fc7309a 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -1161,6 +1161,7 @@ impl GitPanel { ButtonLike::new("active-repository") .style(ButtonStyle::Subtle) .child(Label::new(repository_display_name).size(LabelSize::Small)), + Tooltip::text("Select a repository"), ) } diff --git a/crates/git_ui/src/repository_selector.rs b/crates/git_ui/src/repository_selector.rs index ff8cfa406eb94360259eeefd866d439d49ed8ad7..e5d9c1839a90bfe94e4c0440ac2d5bcfc001f496 100644 --- a/crates/git_ui/src/repository_selector.rs +++ b/crates/git_ui/src/repository_selector.rs @@ -1,6 +1,6 @@ use gpui::{ - AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription, - Task, WeakEntity, + AnyElement, AnyView, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, + Subscription, Task, WeakEntity, }; use picker::{Picker, PickerDelegate}; use project::{ @@ -79,20 +79,27 @@ impl Render for RepositorySelector { } #[derive(IntoElement)] -pub struct RepositorySelectorPopoverMenu +pub struct RepositorySelectorPopoverMenu where - T: PopoverTrigger, + T: PopoverTrigger + ButtonCommon, + TT: Fn(&mut Window, &mut App) -> AnyView + 'static, { repository_selector: Entity, trigger: T, + tooltip: TT, handle: Option>, } -impl RepositorySelectorPopoverMenu { - pub fn new(repository_selector: Entity, trigger: T) -> Self { +impl RepositorySelectorPopoverMenu +where + T: PopoverTrigger + ButtonCommon, + TT: Fn(&mut Window, &mut App) -> AnyView + 'static, +{ + pub fn new(repository_selector: Entity, trigger: T, tooltip: TT) -> Self { Self { repository_selector, trigger, + tooltip, handle: None, } } @@ -103,13 +110,17 @@ impl RepositorySelectorPopoverMenu { } } -impl RenderOnce for RepositorySelectorPopoverMenu { +impl RenderOnce for RepositorySelectorPopoverMenu +where + T: PopoverTrigger + ButtonCommon, + TT: Fn(&mut Window, &mut App) -> AnyView + 'static, +{ fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { let repository_selector = self.repository_selector.clone(); PopoverMenu::new("repository-switcher") .menu(move |_window, _cx| Some(repository_selector.clone())) - .trigger(self.trigger) + .trigger_with_tooltip(self.trigger, self.tooltip) .attach(gpui::Corner::BottomLeft) .when_some(self.handle.clone(), |menu, handle| menu.with_handle(handle)) } diff --git a/crates/inline_completion_button/src/inline_completion_button.rs b/crates/inline_completion_button/src/inline_completion_button.rs index ce1b7bcd839db698da6aa4a3dc2ceb0736bc5f06..c292b75f127ca38f22bb9735bf633dc30d358351 100644 --- a/crates/inline_completion_button/src/inline_completion_button.rs +++ b/crates/inline_completion_button/src/inline_completion_button.rs @@ -142,9 +142,12 @@ impl Render for InlineCompletionButton { }) }) .anchor(Corner::BottomRight) - .trigger(IconButton::new("copilot-icon", icon).tooltip(|window, cx| { - Tooltip::for_action("GitHub Copilot", &ToggleMenu, window, cx) - })) + .trigger_with_tooltip( + IconButton::new("copilot-icon", icon), + |window, cx| { + Tooltip::for_action("GitHub Copilot", &ToggleMenu, window, cx) + }, + ) .with_handle(self.popover_menu_handle.clone()), ) } @@ -211,7 +214,8 @@ impl Render for InlineCompletionButton { _ => None, }) .anchor(Corner::BottomRight) - .trigger(IconButton::new("supermaven-icon", icon).tooltip( + .trigger_with_tooltip( + IconButton::new("supermaven-icon", icon), move |window, cx| { if has_menu { Tooltip::for_action( @@ -224,7 +228,7 @@ impl Render for InlineCompletionButton { Tooltip::text(tooltip_text.clone())(window, cx) } }, - )) + ) .with_handle(self.popover_menu_handle.clone()), ); } @@ -287,31 +291,6 @@ impl Render for InlineCompletionButton { .when(enabled && !show_editor_predictions, |this| { this.indicator(Indicator::dot().color(Color::Muted)) .indicator_border_color(Some(cx.theme().colors().status_bar_background)) - }) - .when(!self.popover_menu_handle.is_deployed(), |element| { - element.tooltip(move |window, cx| { - if enabled { - if show_editor_predictions { - Tooltip::for_action("Edit Prediction", &ToggleMenu, window, cx) - } else { - Tooltip::with_meta( - "Edit Prediction", - Some(&ToggleMenu), - "Hidden For This File", - window, - cx, - ) - } - } else { - Tooltip::with_meta( - "Edit Prediction", - Some(&ToggleMenu), - "Disabled For This File", - window, - cx, - ) - } - }) }); let this = cx.entity().clone(); diff --git a/crates/language_model_selector/src/language_model_selector.rs b/crates/language_model_selector/src/language_model_selector.rs index 10e8b57d684bd6d9b446fd62a120eb568153e900..78e5ed29b44592fd4333f80a81e383fa66f2a2c0 100644 --- a/crates/language_model_selector/src/language_model_selector.rs +++ b/crates/language_model_selector/src/language_model_selector.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use feature_flags::ZedPro; use gpui::{ - Action, AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, + Action, AnyElement, AnyView, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, }; use language_model::{LanguageModel, LanguageModelAvailability, LanguageModelRegistry}; @@ -115,20 +115,31 @@ impl Render for LanguageModelSelector { } #[derive(IntoElement)] -pub struct LanguageModelSelectorPopoverMenu +pub struct LanguageModelSelectorPopoverMenu where - T: PopoverTrigger, + T: PopoverTrigger + ButtonCommon, + TT: Fn(&mut Window, &mut App) -> AnyView + 'static, { language_model_selector: Entity, trigger: T, + tooltip: TT, handle: Option>, } -impl LanguageModelSelectorPopoverMenu { - pub fn new(language_model_selector: Entity, trigger: T) -> Self { +impl LanguageModelSelectorPopoverMenu +where + T: PopoverTrigger + ButtonCommon, + TT: Fn(&mut Window, &mut App) -> AnyView + 'static, +{ + pub fn new( + language_model_selector: Entity, + trigger: T, + tooltip: TT, + ) -> Self { Self { language_model_selector, trigger, + tooltip, handle: None, } } @@ -139,13 +150,17 @@ impl LanguageModelSelectorPopoverMenu { } } -impl RenderOnce for LanguageModelSelectorPopoverMenu { +impl RenderOnce for LanguageModelSelectorPopoverMenu +where + T: PopoverTrigger + ButtonCommon, + TT: Fn(&mut Window, &mut App) -> AnyView + 'static, +{ fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { let language_model_selector = self.language_model_selector.clone(); PopoverMenu::new("model-switcher") .menu(move |_window, _cx| Some(language_model_selector.clone())) - .trigger(self.trigger) + .trigger_with_tooltip(self.trigger, self.tooltip) .anchor(gpui::Corner::BottomRight) .when_some(self.handle.clone(), |menu, handle| menu.with_handle(handle)) .offset(gpui::Point { diff --git a/crates/repl/src/components/kernel_options.rs b/crates/repl/src/components/kernel_options.rs index 4c00977cf65c7960152c13d7fd53a971beef2739..57ee4cdcefae927802cd35213f8247cbae00cde2 100644 --- a/crates/repl/src/components/kernel_options.rs +++ b/crates/repl/src/components/kernel_options.rs @@ -2,6 +2,7 @@ use crate::kernels::KernelSpecification; use crate::repl_store::ReplStore; use crate::KERNEL_DOCS_URL; +use gpui::AnyView; use gpui::DismissEvent; use gpui::FontWeight; @@ -19,10 +20,15 @@ use ui::{prelude::*, ListItem, PopoverMenu, PopoverMenuHandle, PopoverTrigger}; type OnSelect = Box; #[derive(IntoElement)] -pub struct KernelSelector { +pub struct KernelSelector +where + T: PopoverTrigger + ButtonCommon, + TT: Fn(&mut Window, &mut App) -> AnyView + 'static, +{ handle: Option>>, on_select: OnSelect, trigger: T, + tooltip: TT, info_text: Option, worktree_id: WorktreeId, } @@ -44,12 +50,17 @@ fn truncate_path(path: &SharedString, max_length: usize) -> SharedString { } } -impl KernelSelector { - pub fn new(on_select: OnSelect, worktree_id: WorktreeId, trigger: T) -> Self { +impl KernelSelector +where + T: PopoverTrigger + ButtonCommon, + TT: Fn(&mut Window, &mut App) -> AnyView + 'static, +{ + pub fn new(on_select: OnSelect, worktree_id: WorktreeId, trigger: T, tooltip: TT) -> Self { KernelSelector { on_select, handle: None, trigger, + tooltip, info_text: None, worktree_id, } @@ -235,7 +246,11 @@ impl PickerDelegate for KernelPickerDelegate { } } -impl RenderOnce for KernelSelector { +impl RenderOnce for KernelSelector +where + T: PopoverTrigger + ButtonCommon, + TT: Fn(&mut Window, &mut App) -> AnyView + 'static, +{ fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let store = ReplStore::global(cx).read(cx); @@ -262,7 +277,7 @@ impl RenderOnce for KernelSelector { PopoverMenu::new("kernel-switcher") .menu(move |_window, _cx| Some(picker_view.clone())) - .trigger(self.trigger) + .trigger_with_tooltip(self.trigger, self.tooltip) .attach(gpui::Corner::BottomLeft) .when_some(self.handle, |menu, handle| menu.with_handle(handle)) } diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 14a3e111b304a8911f5dad0a3b47b0c028a1dc85..af19555fe907fa2b894818b1af1b5a481f948f10 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -139,10 +139,9 @@ impl TerminalPanel { .gap(DynamicSpacing::Base02.rems(cx)) .child( PopoverMenu::new("terminal-tab-bar-popover-menu") - .trigger( - IconButton::new("plus", IconName::Plus) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text("New…")), + .trigger_with_tooltip( + IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small), + Tooltip::text("New…"), ) .anchor(Corner::TopRight) .with_handle(pane.new_item_context_menu_handle.clone()) @@ -169,10 +168,10 @@ impl TerminalPanel { .children(assistant_tab_bar_button.clone()) .child( PopoverMenu::new("terminal-pane-tab-bar-split") - .trigger( + .trigger_with_tooltip( IconButton::new("terminal-pane-split", IconName::Split) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text("Split Pane")), + .icon_size(IconSize::Small), + Tooltip::text("Split Pane"), ) .anchor(Corner::TopRight) .with_handle(pane.split_item_context_menu_handle.clone()) diff --git a/crates/title_bar/src/application_menu.rs b/crates/title_bar/src/application_menu.rs index 955550596d69d31b1f50f17bc63bccd3d75147f1..dec281b47224bff1a9d98f3e0ac6ac905b2778a2 100644 --- a/crates/title_bar/src/application_menu.rs +++ b/crates/title_bar/src/application_menu.rs @@ -133,16 +133,14 @@ impl ApplicationMenu { .menu(move |window, cx| { Self::build_menu_from_items(entry.clone(), window, cx).into() }) - .trigger( + .trigger_with_tooltip( IconButton::new( SharedString::from(format!("{}-menu-trigger", menu_name)), ui::IconName::Menu, ) .style(ButtonStyle::Subtle) - .icon_size(IconSize::Small) - .when(!handle.is_deployed(), |this| { - this.tooltip(Tooltip::text("Open Application Menu")) - }), + .icon_size(IconSize::Small), + Tooltip::text("Open Application Menu"), ) .with_handle(handle), ) diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index b396e770141f06570a7a338aa5074629be9c9482..9f430585c41656afcb319b4dfd216b8f3b02fafb 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -690,7 +690,7 @@ impl TitleBar { }) .into() }) - .trigger( + .trigger_with_tooltip( ButtonLike::new("user-menu") .child( h_flex() @@ -706,8 +706,8 @@ impl TitleBar { .color(Color::Muted), ), ) - .style(ButtonStyle::Subtle) - .tooltip(Tooltip::text("Toggle User Menu")), + .style(ButtonStyle::Subtle), + Tooltip::text("Toggle User Menu"), ) .anchor(gpui::Corner::TopRight) } else { @@ -736,10 +736,9 @@ impl TitleBar { }) .into() }) - .trigger( - IconButton::new("user-menu", IconName::ChevronDown) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text("Toggle User Menu")), + .trigger_with_tooltip( + IconButton::new("user-menu", IconName::ChevronDown).icon_size(IconSize::Small), + Tooltip::text("Toggle User Menu"), ) } } diff --git a/crates/ui/src/components/popover_menu.rs b/crates/ui/src/components/popover_menu.rs index af801ec97c6e0bbb0522c7ebfa45536b3dd7a562..095b05793a773afb792170fe9eb7ce4f9202b5ff 100644 --- a/crates/ui/src/components/popover_menu.rs +++ b/crates/ui/src/components/popover_menu.rs @@ -3,8 +3,8 @@ use std::{cell::RefCell, rc::Rc}; use gpui::{ - anchored, deferred, div, point, prelude::FluentBuilder, px, size, AnyElement, App, Bounds, - Corner, DismissEvent, DispatchPhase, Element, ElementId, Entity, Focusable as _, + anchored, deferred, div, point, prelude::FluentBuilder, px, size, AnyElement, AnyView, App, + Bounds, Corner, DismissEvent, DispatchPhase, Element, ElementId, Entity, Focusable as _, GlobalElementId, HitboxId, InteractiveElement, IntoElement, LayoutId, Length, ManagedView, MouseDownEvent, ParentElement, Pixels, Point, Style, Window, }; @@ -178,6 +178,28 @@ impl PopoverMenu { self } + pub fn trigger_with_tooltip( + mut self, + t: T, + tooltip_builder: impl Fn(&mut Window, &mut App) -> AnyView + 'static, + ) -> Self { + let on_open = self.on_open.clone(); + self.child_builder = Some(Box::new(move |menu, builder| { + let open = menu.borrow().is_some(); + t.toggle_state(open) + .when_some(builder, |el, builder| { + el.on_click(move |_, window, cx| { + show_menu(&builder, &menu, on_open.clone(), window, cx) + }) + .when(!open, |t| { + t.tooltip(move |window, cx| tooltip_builder(window, cx)) + }) + }) + .into_any_element() + })); + self + } + /// anchor defines which corner of the menu to anchor to the attachment point /// (by default the cursor position, but see attach) pub fn anchor(mut self, anchor: Corner) -> Self { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 7f8596112016f4a56ae99f477d26763311cddbb1..7e628accdd7f7314728f1b51fbce378f9af84e85 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -441,10 +441,9 @@ impl Pane { .gap(DynamicSpacing::Base04.rems(cx)) .child( PopoverMenu::new("pane-tab-bar-popover-menu") - .trigger( - IconButton::new("plus", IconName::Plus) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text("New...")), + .trigger_with_tooltip( + IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small), + Tooltip::text("New..."), ) .anchor(Corner::TopRight) .with_handle(pane.new_item_context_menu_handle.clone()) @@ -474,10 +473,10 @@ impl Pane { ) .child( PopoverMenu::new("pane-tab-bar-split") - .trigger( + .trigger_with_tooltip( IconButton::new("split", IconName::Split) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text("Split Pane")), + .icon_size(IconSize::Small), + Tooltip::text("Split Pane"), ) .anchor(Corner::TopRight) .with_handle(pane.split_item_context_menu_handle.clone()) diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs index bc523be4fdcd08743088cbf9fcfe3a8d2343b77f..5f2b98d444e3a8f6da2035e3db03498399bbf2b7 100644 --- a/crates/zed/src/zed/quick_action_bar.rs +++ b/crates/zed/src/zed/quick_action_bar.rs @@ -168,15 +168,13 @@ impl Render for QuickActionBar { let focus = editor.focus_handle(cx); PopoverMenu::new("editor-selections-dropdown") - .trigger( + .trigger_with_tooltip( IconButton::new("toggle_editor_selections_icon", IconName::CursorIBeam) .shape(IconButtonShape::Square) .icon_size(IconSize::Small) .style(ButtonStyle::Subtle) - .toggle_state(self.toggle_selections_handle.is_deployed()) - .when(!self.toggle_selections_handle.is_deployed(), |this| { - this.tooltip(Tooltip::text("Selection Controls")) - }), + .toggle_state(self.toggle_selections_handle.is_deployed()), + Tooltip::text("Selection Controls"), ) .with_handle(self.toggle_selections_handle.clone()) .anchor(Corner::TopRight) @@ -219,15 +217,13 @@ impl Render for QuickActionBar { let vim_mode_enabled = VimModeSetting::get_global(cx).0; PopoverMenu::new("editor-settings") - .trigger( + .trigger_with_tooltip( IconButton::new("toggle_editor_settings_icon", IconName::Sliders) .shape(IconButtonShape::Square) .icon_size(IconSize::Small) .style(ButtonStyle::Subtle) - .toggle_state(self.toggle_settings_handle.is_deployed()) - .when(!self.toggle_settings_handle.is_deployed(), |this| { - this.tooltip(Tooltip::text("Editor Controls")) - }), + .toggle_state(self.toggle_settings_handle.is_deployed()), + Tooltip::text("Editor Controls"), ) .anchor(Corner::TopRight) .with_handle(self.toggle_settings_handle.clone()) diff --git a/crates/zed/src/zed/quick_action_bar/repl_menu.rs b/crates/zed/src/zed/quick_action_bar/repl_menu.rs index 6e7f57ad947ddc3be79d0ff47d2fef85378cb8a0..51ed0af6b88c8b5b97e9fb6d360f6b159c07c6d9 100644 --- a/crates/zed/src/zed/quick_action_bar/repl_menu.rs +++ b/crates/zed/src/zed/quick_action_bar/repl_menu.rs @@ -209,16 +209,16 @@ impl QuickActionBar { }) .into() }) - .trigger( + .trigger_with_tooltip( ButtonLike::new_rounded_right(element_id("dropdown")) .child( Icon::new(IconName::ChevronDownSmall) .size(IconSize::XSmall) .color(Color::Muted), ) - .tooltip(Tooltip::text("REPL Menu")) .width(rems(1.).into()) .disabled(menu_state.popover_disabled), + Tooltip::text("REPL Menu"), ); let button = ButtonLike::new_rounded_left("toggle_repl_icon") @@ -343,8 +343,8 @@ impl QuickActionBar { .color(Color::Muted) .size(IconSize::XSmall), ), - ) - .tooltip(Tooltip::text("Select Kernel")), + ), + Tooltip::text("Select Kernel"), ) .with_handle(menu_handle.clone()) .into_any_element() From 994bea00032df173e03f0388b714f95838eee1e2 Mon Sep 17 00:00:00 2001 From: CharlesChen0823 Date: Mon, 10 Feb 2025 17:54:06 +0800 Subject: [PATCH 32/42] workspace: Fix pane focus transfer when closing another pane (#23175) Closes #23123 Only close current active_pane should move focus to other pane. Release Notes: - N/A --- crates/workspace/src/workspace.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 78c950b535560bc63f5033d30f6940534694220d..e4e3a7c783ec71aa29ce8518b85d122899aecf80 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -4442,10 +4442,12 @@ impl Workspace { if let Some(focus_on) = focus_on { focus_on.update(cx, |pane, cx| window.focus(&pane.focus_handle(cx))); } else { - self.panes - .last() - .unwrap() - .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx))); + if self.active_pane() == pane { + self.panes + .last() + .unwrap() + .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx))); + } } if self.last_active_center_pane == Some(pane.downgrade()) { self.last_active_center_pane = None; From d0c4c664b0fac4e6ef020ec6b14e15b2ef2f49b7 Mon Sep 17 00:00:00 2001 From: Libon Date: Mon, 10 Feb 2025 19:03:47 +0800 Subject: [PATCH 33/42] Brighten yellow and black terminal colors in One themes (#24420) Closes https://github.com/zed-industries/zed/issues/24419 I made some fine adjustments to the color of the theme with reference to' Window Terminal' to make it look good. If there is anything inappropriate in this revision, please also point it out. :) ![window-terminal-one-light](https://github.com/user-attachments/assets/86c4002f-a5a7-4ab1-81de-e6ed0529fe06) ![window-terminal-one-dark](https://github.com/user-attachments/assets/c57095d7-0131-4978-ae6d-7105639110b5) Release Notes: - N/A --- assets/themes/one/one.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/themes/one/one.json b/assets/themes/one/one.json index 4e26d646ddd8101e5785ca796550f09fdfd72ed3..1cac0db14b9336deb2a19c0e2dc33f162d075009 100644 --- a/assets/themes/one/one.json +++ b/assets/themes/one/one.json @@ -81,7 +81,7 @@ "terminal.ansi.bright_green": "#4d6140ff", "terminal.ansi.dim_green": "#d1e0bfff", "terminal.ansi.yellow": "#dec184ff", - "terminal.ansi.bright_yellow": "#786441ff", + "terminal.ansi.bright_yellow": "#e5c07bff", "terminal.ansi.dim_yellow": "#f1dfc1ff", "terminal.ansi.blue": "#74ade8ff", "terminal.ansi.bright_blue": "#385378ff", @@ -457,7 +457,7 @@ "terminal.ansi.bright_green": "#b2cfa9ff", "terminal.ansi.dim_green": "#354d2eff", "terminal.ansi.yellow": "#dec184ff", - "terminal.ansi.bright_yellow": "#f1dfc1ff", + "terminal.ansi.bright_yellow": "#826221ff", "terminal.ansi.dim_yellow": "#786441ff", "terminal.ansi.blue": "#5c78e2ff", "terminal.ansi.bright_blue": "#b5baf2ff", From e72f7b4e2290c41c8f276b79508e6b470db52b18 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 10 Feb 2025 08:19:46 -0300 Subject: [PATCH 34/42] edit predictions: Put back status bar button tooltips (#24548) These were wrongly removed in https://github.com/zed-industries/zed/pull/24540; putting them back. cc @SomeoneToIgnore Release Notes: - N/A --- .../src/inline_completion_button.rs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/crates/inline_completion_button/src/inline_completion_button.rs b/crates/inline_completion_button/src/inline_completion_button.rs index c292b75f127ca38f22bb9735bf633dc30d358351..1b9e7309692bbb6fc8a5a89872aa9fa3e0cabc48 100644 --- a/crates/inline_completion_button/src/inline_completion_button.rs +++ b/crates/inline_completion_button/src/inline_completion_button.rs @@ -291,6 +291,31 @@ impl Render for InlineCompletionButton { .when(enabled && !show_editor_predictions, |this| { this.indicator(Indicator::dot().color(Color::Muted)) .indicator_border_color(Some(cx.theme().colors().status_bar_background)) + }) + .when(!self.popover_menu_handle.is_deployed(), |element| { + element.tooltip(move |window, cx| { + if enabled { + if show_editor_predictions { + Tooltip::for_action("Edit Prediction", &ToggleMenu, window, cx) + } else { + Tooltip::with_meta( + "Edit Prediction", + Some(&ToggleMenu), + "Hidden For This File", + window, + cx, + ) + } + } else { + Tooltip::with_meta( + "Edit Prediction", + Some(&ToggleMenu), + "Disabled For This File", + window, + cx, + ) + } + }) }); let this = cx.entity().clone(); From d15a61a1aa81317ae2414d8002a6e00475407cc3 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 10 Feb 2025 08:19:58 -0300 Subject: [PATCH 35/42] context menu: Adjust toggleable entry label alignment (#24549) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, we were passing an `IconSize` that had a default size. Given the check icon is small by default, when the entry is not toggled, that caused a slight misalignment between the toggled and not-toggled items. I'm passing now the same icon element but inside an opacity 0 div. Open to other suggestions if this feels clunky. | Before | After | |--------|--------| | Screenshot 2025-02-10 at 7 58 28 AM | Screenshot 2025-02-10 at 7 58 37 AM | Release Notes: - N/A --- crates/ui/src/components/context_menu.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index fe00c733f054ec2fab8a9cd0c2ff2630d1c0710b..6d8e0a86784cea80d9e93fcb88637ecb63553215 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -672,15 +672,16 @@ impl Render for ContextMenu { *toggle, |list_item, (position, toggled)| { let contents = if toggled { - v_flex().flex_none().child( + div().flex_none().child( Icon::new(IconName::Check) .color(Color::Accent) .size(*icon_size) ) } else { - v_flex().flex_none().size( - IconSize::default().rems(), - ) + div().flex_none().child( + Icon::new(IconName::Check) + .size(*icon_size) + ).opacity(0.) }; match position { IconPosition::Start => { From 3f0288e52a88d0e267904a199097107340d6671b Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 10 Feb 2025 09:43:51 -0300 Subject: [PATCH 36/42] docs: Add a light border to h2s (#24554) I was finding hard to navigate the "Configuring Zed" page with just white space creating a boundary between the different chunks of content. I think a slight border below the h2 heading helps a lot with that! Release Notes: - N/A --- docs/theme/css/general.css | 6 ++++++ docs/theme/css/variables.css | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/theme/css/general.css b/docs/theme/css/general.css index d1b8e9b92653e7ffc0dce7245b5ebd3da378ef17..b4ef43518c88f768124fd96f2df1299a93dd02dc 100644 --- a/docs/theme/css/general.css +++ b/docs/theme/css/general.css @@ -79,6 +79,12 @@ h6 code { display: none !important; } +h2 { + padding-bottom: 1rem; + border-bottom: 1px solid; + border-color: var(--border-light); +} + h2, h3 { margin-block-start: 1.5em; diff --git a/docs/theme/css/variables.css b/docs/theme/css/variables.css index 6604545c45616b012fd569517ab41ca27e834800..bd3b42522e0f59a6c632839f86d24461b1e3274f 100644 --- a/docs/theme/css/variables.css +++ b/docs/theme/css/variables.css @@ -98,7 +98,7 @@ --title-color: hsl(220, 92%, 80%); --border: hsl(220, 13%, 20%); - --border-light: hsl(220, 13%, 90%); + --border-light: hsl(220, 13%, 15%); --border-hover: hsl(220, 13%, 40%); --media-bg: hsl(220, 13%, 8%); From d42322ab0667e474f16d5e64d070802a6582cea3 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Mon, 10 Feb 2025 15:42:16 +0100 Subject: [PATCH 37/42] php: Update `brackets.scm` (#24558) Closes #24550 Adds some missing brackets to the PHP language extension. --- extensions/php/languages/php/brackets.scm | 3 +++ 1 file changed, 3 insertions(+) diff --git a/extensions/php/languages/php/brackets.scm b/extensions/php/languages/php/brackets.scm index e3f280b71f85147b50a9b0339f3e616b80d132a1..988602aa8d715a2a771b2298a022d661c7683465 100644 --- a/extensions/php/languages/php/brackets.scm +++ b/extensions/php/languages/php/brackets.scm @@ -1 +1,4 @@ ("{" @open "}" @close) +("(" @open ")" @close) +("[" @open "]" @close) +("\"" @open "\"" @close) From ca4378cbaa0413ee85d3198ccfdf55e69324b3de Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 10 Feb 2025 16:10:35 +0100 Subject: [PATCH 38/42] ui: Use `cursor: pointer` for `Toggle`s (#24563) Closes #ISSUE Release Notes: - N/A --- crates/ui/src/components/toggle.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/ui/src/components/toggle.rs b/crates/ui/src/components/toggle.rs index c287f2f8466c4812c82dd1d1718c18d122e1ab19..13811883bcc7275a1aedb53b13a26199bf4f01e2 100644 --- a/crates/ui/src/components/toggle.rs +++ b/crates/ui/src/components/toggle.rs @@ -433,6 +433,7 @@ impl RenderOnce for Switch { h_flex() .id(self.id) .gap(DynamicSpacing::Base06.rems(cx)) + .cursor_pointer() .child(switch) .when_some( self.on_click.filter(|_| !self.disabled), From d292b7c96dbc5b1294b2c65dee40b64d78e3e034 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 10 Feb 2025 12:16:33 -0300 Subject: [PATCH 39/42] context menu: Use `invisible()` to hide the check icon (#24562) Follow up to: https://github.com/zed-industries/zed/pull/24549 Release Notes: - N/A --- crates/ui/src/components/context_menu.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index 6d8e0a86784cea80d9e93fcb88637ecb63553215..b827a576673f7ffefc4421c7791237fcce13679f 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -681,7 +681,7 @@ impl Render for ContextMenu { div().flex_none().child( Icon::new(IconName::Check) .size(*icon_size) - ).opacity(0.) + ).invisible() }; match position { IconPosition::Start => { From 69d415c8d01769dd89d3136d4427cbf068eda645 Mon Sep 17 00:00:00 2001 From: 5brian Date: Mon, 10 Feb 2025 10:45:06 -0500 Subject: [PATCH 40/42] vim: Multiline operation improvements (#24518) Closes #15711 Discussed changes to match neovim in https://github.com/zed-industries/zed/pull/24481#issuecomment-2644504695 -- `vi{` matches neovim with treesitter instead of vanilla neovim. Change and delete matches standard neovim. Not sure if this is the best way to do it, implemented post processing to change and delete objects. I think another way would be adjust the range to trim the trailing newline char on change and delete operations, instead of having to add it back. ||Before|After| |---|---|---| |initial|![image](https://github.com/user-attachments/assets/0bab37b7-c0ac-4992-a365-b7ec304a6800)|| | `vi{` | ![image](https://github.com/user-attachments/assets/4c802fcd-fa7e-45ba-b7d4-3283ed538e10) | ![image](https://github.com/user-attachments/assets/4394bb6e-418b-4463-9737-f9bdfc6d31c2) | | `ci{` | ![image](https://github.com/user-attachments/assets/b5eabb58-4a93-4c98-80b6-f34a6525b1fb) | ![image](https://github.com/user-attachments/assets/79af57e4-260c-4432-af66-eba5285d97a0) | | `di{` | ![image](https://github.com/user-attachments/assets/190a70e7-71fd-47fe-9d6c-2082f2034d0f) | ![image](https://github.com/user-attachments/assets/775b86a9-68c1-4397-a44b-c645a772de63) | Release Notes: - vim: Improved multi-line operations --- crates/vim/src/object.rs | 150 +++++++++++++++++++++++++-------------- 1 file changed, 97 insertions(+), 53 deletions(-) diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index eb2999cb7266065c7b6be5dbca30ddc5a4ffd948..ed5e3c21bf2fa59bcced32222c6795ec89cad6a5 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -407,6 +407,9 @@ impl Object { if let Some(range) = self.range(map, selection.clone(), around) { selection.start = range.start; selection.end = range.end; + if !around && self.is_multiline() { + preserve_indented_newline(map, selection); + } true } else { false @@ -414,6 +417,49 @@ impl Object { } } +/// Returns a range without the final newline char. +/// +/// If the selection spans multiple lines and is preceded by an opening brace (`{`), +/// this function will trim the selection to exclude the final newline +/// in order to preserve a properly indented line. +fn preserve_indented_newline(map: &DisplaySnapshot, selection: &mut Selection) { + let (start_point, end_point) = (selection.start.to_point(map), selection.end.to_point(map)); + + if start_point.row == end_point.row { + return; + } + + let start_offset = selection.start.to_offset(map, Bias::Left); + let mut pos = start_offset; + + while pos > 0 { + pos -= 1; + let current_char = map.buffer_chars_at(pos).next().map(|(ch, _)| ch); + + match current_char { + Some(ch) if !ch.is_whitespace() => break, + Some('\n') if pos > 0 => { + let prev_char = map.buffer_chars_at(pos - 1).next().map(|(ch, _)| ch); + if prev_char == Some('{') { + let end_pos = selection.end.to_offset(map, Bias::Left); + for (ch, offset) in map.reverse_buffer_chars_at(end_pos) { + match ch { + '\n' => { + selection.end = offset.to_display_point(map); + break; + } + ch if !ch.is_whitespace() => break, + _ => continue, + } + } + } + break; + } + _ => continue, + } + } +} + /// Returns a range that surrounds the word `relative_to` is in. /// /// If `relative_to` is at the start of a word, return the word. @@ -1333,12 +1379,24 @@ fn surrounding_markers( } if !around && search_across_lines { + // Handle trailing newline after opening if let Some((ch, range)) = movement::chars_after(map, opening.end).next() { if ch == '\n' { - opening.end = range.end + opening.end = range.end; + + // After newline, skip leading whitespace + let mut chars = movement::chars_after(map, opening.end).peekable(); + while let Some((ch, range)) = chars.peek() { + if !ch.is_whitespace() { + break; + } + opening.end = range.end; + chars.next(); + } } } + // Handle leading whitespace before closing let mut last_newline_end = None; for (ch, range) in movement::chars_before(map, closing.start) { if !ch.is_whitespace() { @@ -1687,60 +1745,46 @@ mod test { #[gpui::test] async fn test_multiline_surrounding_character_objects(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; + let mut cx = VimTestContext::new(cx, true).await; - cx.set_shared_state(indoc! { - "func empty(a string) bool { - if a == \"\" { - return true - } - ˇreturn false - }" - }) - .await; - cx.simulate_shared_keystrokes("v i {").await; - cx.shared_state().await.assert_eq(indoc! {" - func empty(a string) bool { - « if a == \"\" { - return true - } - return false - ˇ»}"}); - cx.set_shared_state(indoc! { - "func empty(a string) bool { - if a == \"\" { - ˇreturn true - } - return false - }" - }) - .await; - cx.simulate_shared_keystrokes("v i {").await; - cx.shared_state().await.assert_eq(indoc! {" - func empty(a string) bool { - if a == \"\" { - « return true - ˇ» } - return false - }"}); + cx.set_state( + indoc! { + "func empty(a string) bool { + if a == \"\" { + return true + } + ˇreturn false + }" + }, + Mode::Normal, + ); + cx.simulate_keystrokes("v i {"); - cx.set_shared_state(indoc! { - "func empty(a string) bool { - if a == \"\" ˇ{ - return true - } - return false - }" - }) - .await; - cx.simulate_shared_keystrokes("v i {").await; - cx.shared_state().await.assert_eq(indoc! {" - func empty(a string) bool { - if a == \"\" { - « return true - ˇ» } - return false - }"}); + cx.set_state( + indoc! { + "func empty(a string) bool { + if a == \"\" { + ˇreturn true + } + return false + }" + }, + Mode::Normal, + ); + cx.simulate_keystrokes("v i {"); + + cx.set_state( + indoc! { + "func empty(a string) bool { + if a == \"\" ˇ{ + return true + } + return false + }" + }, + Mode::Normal, + ); + cx.simulate_keystrokes("v i {"); } #[gpui::test] From de8d4d00cefdc40e5ee811c31680eecb8588749d Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 10 Feb 2025 10:52:09 -0500 Subject: [PATCH 41/42] git_ui: Update git panel commit editor, start on quick commit - Fixes commit editor issues & updates style - Starts on quick commit (not hooked up to anything) - Updates some panel styles - Adds SwitchWithLabel - Release Notes: - N/A --- Cargo.lock | 3 + crates/git_ui/src/git_panel.rs | 239 ++++++++------ crates/git_ui/src/git_ui.rs | 2 + crates/git_ui/src/quick_commit.rs | 307 ++++++++++++++++++ crates/panel/Cargo.toml | 3 + crates/panel/src/panel.rs | 65 +++- crates/ui/src/components/button/button.rs | 8 +- .../ui/src/components/button/button_like.rs | 4 +- .../ui/src/components/button/icon_button.rs | 9 + crates/ui/src/components/toggle.rs | 58 ++++ 10 files changed, 582 insertions(+), 116 deletions(-) create mode 100644 crates/git_ui/src/quick_commit.rs diff --git a/Cargo.lock b/Cargo.lock index 3fb5ee2f9f575739e81aa7ba0e6276abdb260315..40f302224a5bbfbb11dfbf121532be066f7c45fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9044,7 +9044,10 @@ dependencies = [ name = "panel" version = "0.1.0" dependencies = [ + "editor", "gpui", + "settings", + "theme", "ui", "workspace", ] diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index c92eb56e522e433ccb0ae3ec1423d6128fc7309a..d8a676313c8878bcd79df25f57ae935d94397a39 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -6,33 +6,32 @@ use crate::{ }; use collections::HashMap; use db::kvp::KEY_VALUE_STORE; -use editor::actions::MoveToEnd; -use editor::scroll::ScrollbarAutoHide; -use editor::{Editor, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar}; -use git::repository::RepoPath; -use git::status::FileStatus; -use git::{Commit, ToggleStaged}; +use editor::{ + actions::MoveToEnd, scroll::ScrollbarAutoHide, Editor, EditorElement, EditorMode, + EditorSettings, MultiBuffer, ShowScrollbar, +}; +use git::{repository::RepoPath, status::FileStatus, Commit, ToggleStaged}; use gpui::*; use language::{Buffer, File}; use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev}; use multi_buffer::ExcerptInfo; -use panel::PanelHeader; -use project::git::{GitEvent, Repository}; -use project::{Fs, Project, ProjectPath}; +use panel::{panel_editor_container, panel_editor_style, panel_filled_button, PanelHeader}; +use project::{ + git::{GitEvent, Repository}, + Fs, Project, ProjectPath, +}; use serde::{Deserialize, Serialize}; use settings::Settings as _; use std::{collections::HashSet, path::PathBuf, sync::Arc, time::Duration, usize}; -use theme::ThemeSettings; use ui::{ - prelude::*, ButtonLike, Checkbox, Divider, DividerColor, ElevationIndex, IndentGuideColors, - ListItem, ListItemSpacing, Scrollbar, ScrollbarState, Tooltip, + prelude::*, ButtonLike, Checkbox, CheckboxWithLabel, Divider, DividerColor, ElevationIndex, + IndentGuideColors, ListItem, ListItemSpacing, Scrollbar, ScrollbarState, Tooltip, }; use util::{maybe, ResultExt, TryFutureExt}; -use workspace::notifications::{DetachAndPromptErr, NotificationId}; -use workspace::Toast; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, - Workspace, + notifications::{DetachAndPromptErr, NotificationId}, + Toast, Workspace, }; actions!( @@ -147,33 +146,33 @@ struct PendingOperation { } pub struct GitPanel { + active_repository: Option>, + commit_editor: Entity, + conflicted_count: usize, + conflicted_staged_count: usize, current_modifiers: Modifiers, + enable_auto_coauthors: bool, + entries: Vec, + entries_by_path: collections::HashMap, focus_handle: FocusHandle, fs: Arc, hide_scrollbar_task: Option>, + new_count: usize, + new_staged_count: usize, + pending: Vec, + pending_commit: Option>, pending_serialization: Task>, - workspace: WeakEntity, project: Entity, - active_repository: Option>, + repository_selector: Entity, scroll_handle: UniformListScrollHandle, scrollbar_state: ScrollbarState, selected_entry: Option, show_scrollbar: bool, + tracked_count: usize, + tracked_staged_count: usize, update_visible_entries_task: Task<()>, - repository_selector: Entity, - commit_editor: Entity, - entries: Vec, - entries_by_path: collections::HashMap, width: Option, - pending: Vec, - pending_commit: Option>, - - conflicted_staged_count: usize, - conflicted_count: usize, - tracked_staged_count: usize, - tracked_count: usize, - new_staged_count: usize, - new_count: usize, + workspace: WeakEntity, } fn commit_message_editor( @@ -181,23 +180,10 @@ fn commit_message_editor( window: &mut Window, cx: &mut Context<'_, Editor>, ) -> Editor { - let theme = ThemeSettings::get_global(cx); - - let mut text_style = window.text_style(); - let refinement = TextStyleRefinement { - font_family: Some(theme.buffer_font.family.clone()), - font_features: Some(FontFeatures::disable_ligatures()), - font_size: Some(px(12.).into()), - color: Some(cx.theme().colors().editor_foreground), - background_color: Some(gpui::transparent_black()), - ..Default::default() - }; - text_style.refine(&refinement); - let mut commit_editor = if let Some(commit_message_buffer) = commit_message_buffer { let buffer = cx.new(|cx| MultiBuffer::singleton(commit_message_buffer, cx)); Editor::new( - EditorMode::AutoHeight { max_lines: 10 }, + EditorMode::AutoHeight { max_lines: 6 }, buffer, None, false, @@ -205,13 +191,12 @@ fn commit_message_editor( cx, ) } else { - Editor::auto_height(10, window, cx) + Editor::auto_height(6, window, cx) }; commit_editor.set_use_autoclose(false); commit_editor.set_show_gutter(false, cx); commit_editor.set_show_wrap_guides(false, cx); commit_editor.set_show_indent_guides(false, cx); - commit_editor.set_text_style_refinement(refinement); commit_editor.set_placeholder_text("Enter commit message", cx); commit_editor } @@ -260,37 +245,40 @@ impl GitPanel { ) .detach(); + let scrollbar_state = + ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()); + let repository_selector = cx.new(|cx| RepositorySelector::new(project.clone(), window, cx)); let mut git_panel = Self { - focus_handle: cx.focus_handle(), - pending_serialization: Task::ready(None), + active_repository, + commit_editor, + conflicted_count: 0, + conflicted_staged_count: 0, + current_modifiers: window.modifiers(), + enable_auto_coauthors: true, entries: Vec::new(), entries_by_path: HashMap::default(), + focus_handle: cx.focus_handle(), + fs, + hide_scrollbar_task: None, + new_count: 0, + new_staged_count: 0, pending: Vec::new(), - current_modifiers: window.modifiers(), - width: Some(px(360.)), - scrollbar_state: ScrollbarState::new(scroll_handle.clone()) - .parent_entity(&cx.entity()), + pending_commit: None, + pending_serialization: Task::ready(None), + project, repository_selector, + scroll_handle, + scrollbar_state, selected_entry: None, show_scrollbar: false, - hide_scrollbar_task: None, + tracked_count: 0, + tracked_staged_count: 0, update_visible_entries_task: Task::ready(()), - pending_commit: None, - active_repository, - scroll_handle, - fs, - commit_editor, - project, + width: Some(px(360.)), workspace, - conflicted_count: 0, - conflicted_staged_count: 0, - tracked_staged_count: 0, - tracked_count: 0, - new_staged_count: 0, - new_count: 0, }; git_panel.schedule_update(false, window, cx); git_panel.show_scrollbar = git_panel.should_show_scrollbar(cx); @@ -990,6 +978,26 @@ impl GitPanel { cx.notify(); } + fn toggle_auto_coauthors(&mut self, cx: &mut Context) { + self.enable_auto_coauthors = !self.enable_auto_coauthors; + cx.notify(); + } + + fn header_state(&self, header_type: Section) -> ToggleState { + let (staged_count, count) = match header_type { + Section::New => (self.new_staged_count, self.new_count), + Section::Tracked => (self.tracked_staged_count, self.tracked_count), + Section::Conflict => (self.conflicted_staged_count, self.conflicted_count), + }; + if staged_count == 0 { + ToggleState::Unselected + } else if count == staged_count { + ToggleState::Selected + } else { + ToggleState::Indeterminate + } + } + fn update_counts(&mut self, repo: &Repository) { self.conflicted_count = 0; self.conflicted_staged_count = 0; @@ -1043,21 +1051,6 @@ impl GitPanel { self.conflicted_count > 0 && self.conflicted_count != self.conflicted_staged_count } - fn header_state(&self, header_type: Section) -> ToggleState { - let (staged_count, count) = match header_type { - Section::New => (self.new_staged_count, self.new_count), - Section::Tracked => (self.tracked_staged_count, self.tracked_count), - Section::Conflict => (self.conflicted_staged_count, self.conflicted_count), - }; - if staged_count == 0 { - ToggleState::Unselected - } else if count == staged_count { - ToggleState::Selected - } else { - ToggleState::Indeterminate - } - } - fn show_err_toast(&self, e: anyhow::Error, cx: &mut App) { let Some(workspace) = self.workspace.upgrade() else { return; @@ -1165,13 +1158,21 @@ impl GitPanel { ) } - pub fn render_commit_editor(&self, cx: &Context) -> impl IntoElement { + pub fn render_commit_editor( + &self, + window: &mut Window, + cx: &mut Context, + ) -> impl IntoElement { let editor = self.commit_editor.clone(); let can_commit = (self.has_staged_changes() || self.has_tracked_changes()) && self.pending_commit.is_none() && !editor.read(cx).is_empty(cx) && !self.has_unstaged_conflicts() && self.has_write_access(cx); + // let can_commit_all = + // !self.commit_pending && self.can_commit_all && !editor.read(cx).is_empty(cx); + let panel_editor_style = panel_editor_style(true, window, cx); + let editor_focus_handle = editor.read(cx).focus_handle(cx).clone(); let focus_handle_1 = self.focus_handle(cx).clone(); @@ -1186,8 +1187,7 @@ impl GitPanel { "Commit All" }; - let commit_button = self - .panel_button("commit-changes", title) + let commit_button = panel_filled_button(title) .tooltip(move |window, cx| { let focus_handle = focus_handle_1.clone(); Tooltip::for_action_in(tooltip, &Commit, &focus_handle, window, cx) @@ -1197,28 +1197,50 @@ impl GitPanel { cx.listener(move |this, _: &ClickEvent, window, cx| this.commit_changes(window, cx)) }); - div().w_full().h(px(140.)).px_2().pt_1().pb_2().child( - v_flex() - .id("commit-editor-container") - .relative() - .h_full() - .py_2p5() - .px_3() - .bg(cx.theme().colors().editor_background) - .on_click(cx.listener(move |_, _: &ClickEvent, window, _cx| { - window.focus(&editor_focus_handle); - })) - .child(self.commit_editor.clone()) - .child( - h_flex() - .absolute() - .bottom_2p5() - .right_3() - .gap_1p5() - .child(div().gap_1().flex_grow()) - .child(commit_button), - ), - ) + let enable_coauthors = CheckboxWithLabel::new( + "enable-coauthors", + Label::new("Add Co-authors") + .color(Color::Disabled) + .size(LabelSize::XSmall), + self.enable_auto_coauthors.into(), + cx.listener(move |this, _, _, cx| this.toggle_auto_coauthors(cx)), + ); + + let footer_size = px(32.); + let gap = px(16.0); + + let max_height = window.line_height() * 6. + gap + footer_size; + + panel_editor_container(window, cx) + .id("commit-editor-container") + .relative() + .h(max_height) + .w_full() + .border_t_1() + .border_color(cx.theme().colors().border) + .bg(cx.theme().colors().editor_background) + .on_click(cx.listener(move |_, _: &ClickEvent, window, _cx| { + window.focus(&editor_focus_handle); + })) + .child(EditorElement::new(&self.commit_editor, panel_editor_style)) + .child( + h_flex() + .absolute() + .bottom_0() + .left_2() + .h(footer_size) + .flex_none() + .child(enable_coauthors), + ) + .child( + h_flex() + .absolute() + .bottom_0() + .right_2() + .h(footer_size) + .flex_none() + .child(commit_button), + ) } fn render_empty_state(&self, cx: &mut Context) -> impl IntoElement { @@ -1348,6 +1370,7 @@ impl GitPanel { v_flex() .size_full() + .flex_grow() .overflow_hidden() .child( uniform_list(cx.entity().clone(), "entries", entry_count, { @@ -1496,7 +1519,7 @@ impl GitPanel { .spacing(ListItemSpacing::Sparse) .start_slot(start_slot) .toggle_state(selected) - .focused(selected && self.focus_handle.is_focused(window)) + .focused(selected && self.focus_handle(cx).is_focused(window)) .disabled(!has_write_access) .on_click({ cx.listener(move |this, _, _, cx| { @@ -1599,7 +1622,7 @@ impl GitPanel { .spacing(ListItemSpacing::Sparse) .start_slot(start_slot) .toggle_state(selected) - .focused(selected && self.focus_handle.is_focused(window)) + .focused(selected && self.focus_handle(cx).is_focused(window)) .disabled(!has_write_access) .on_click({ cx.listener(move |this, _, window, cx| { @@ -1705,7 +1728,7 @@ impl Render for GitPanel { } else { self.render_empty_state(cx).into_any_element() }) - .child(self.render_commit_editor(cx)) + .child(self.render_commit_editor(window, cx)) } } diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index a8313aa9d5b3cf284f42cdc6ec6e8191c0f10082..300c589ecd910c51e29cef22587930310ae8b68d 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -9,12 +9,14 @@ pub mod branch_picker; pub mod git_panel; mod git_panel_settings; pub mod project_diff; +// mod quick_commit; pub mod repository_selector; pub fn init(cx: &mut App) { GitPanelSettings::register(cx); branch_picker::init(cx); cx.observe_new(ProjectDiff::register).detach(); + // quick_commit::init(cx); } // TODO: Add updated status colors to theme diff --git a/crates/git_ui/src/quick_commit.rs b/crates/git_ui/src/quick_commit.rs new file mode 100644 index 0000000000000000000000000000000000000000..be7f3fa84db40465fdbdbe411c1e9d0d567d2de9 --- /dev/null +++ b/crates/git_ui/src/quick_commit.rs @@ -0,0 +1,307 @@ +#![allow(unused, dead_code)] + +use crate::repository_selector::RepositorySelector; +use anyhow::Result; +use git::{CommitAllChanges, CommitChanges}; +use language::Buffer; +use panel::{panel_editor_container, panel_editor_style, panel_filled_button, panel_icon_button}; +use ui::{prelude::*, Tooltip}; + +use editor::{Editor, EditorElement, EditorMode, MultiBuffer}; +use gpui::*; +use project::git::Repository; +use project::{Fs, Project}; +use std::sync::Arc; +use workspace::{ModalView, Workspace}; + +actions!( + git, + [QuickCommitWithMessage, QuickCommitStaged, QuickCommitAll] +); + +pub fn init(cx: &mut App) { + cx.observe_new(|workspace: &mut Workspace, window, cx| { + let Some(window) = window else { + return; + }; + QuickCommitModal::register(workspace, window, cx) + }) + .detach(); +} + +fn commit_message_editor( + commit_message_buffer: Option>, + window: &mut Window, + cx: &mut Context<'_, Editor>, +) -> Editor { + let mut commit_editor = if let Some(commit_message_buffer) = commit_message_buffer { + let buffer = cx.new(|cx| MultiBuffer::singleton(commit_message_buffer, cx)); + Editor::new( + EditorMode::AutoHeight { max_lines: 10 }, + buffer, + None, + false, + window, + cx, + ) + } else { + Editor::auto_height(10, window, cx) + }; + commit_editor.set_use_autoclose(false); + commit_editor.set_show_gutter(false, cx); + commit_editor.set_show_wrap_guides(false, cx); + commit_editor.set_show_indent_guides(false, cx); + commit_editor.set_placeholder_text("Enter commit message", cx); + commit_editor +} + +pub struct QuickCommitModal { + focus_handle: FocusHandle, + fs: Arc, + project: Entity, + active_repository: Option>, + repository_selector: Entity, + commit_editor: Entity, + width: Option, + commit_task: Task>, + commit_pending: bool, + can_commit: bool, + can_commit_all: bool, + enable_auto_coauthors: bool, +} + +impl Focusable for QuickCommitModal { + fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { + self.focus_handle.clone() + } +} + +impl EventEmitter for QuickCommitModal {} +impl ModalView for QuickCommitModal {} + +impl QuickCommitModal { + pub fn register(workspace: &mut Workspace, _: &mut Window, cx: &mut Context) { + workspace.register_action(|workspace, _: &QuickCommitWithMessage, window, cx| { + let project = workspace.project().clone(); + let fs = workspace.app_state().fs.clone(); + + workspace.toggle_modal(window, cx, move |window, cx| { + QuickCommitModal::new(project, fs, window, None, cx) + }); + }); + } + + pub fn new( + project: Entity, + fs: Arc, + window: &mut Window, + commit_message_buffer: Option>, + cx: &mut Context, + ) -> Self { + let git_state = project.read(cx).git_state().clone(); + let active_repository = project.read(cx).active_repository(cx); + + let focus_handle = cx.focus_handle(); + + let commit_editor = cx.new(|cx| commit_message_editor(commit_message_buffer, window, cx)); + commit_editor.update(cx, |editor, cx| { + editor.clear(window, cx); + }); + + let repository_selector = cx.new(|cx| RepositorySelector::new(project.clone(), window, cx)); + + Self { + focus_handle, + fs, + project, + active_repository, + repository_selector, + commit_editor, + width: None, + commit_task: Task::ready(Ok(())), + commit_pending: false, + can_commit: false, + can_commit_all: false, + enable_auto_coauthors: true, + } + } + + pub fn render_header(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let all_repositories = self + .project + .read(cx) + .git_state() + .read(cx) + .all_repositories(); + let entry_count = self + .active_repository + .as_ref() + .map_or(0, |repo| repo.read(cx).entry_count()); + + let changes_string = match entry_count { + 0 => "No changes".to_string(), + 1 => "1 change".to_string(), + n => format!("{} changes", n), + }; + + div().absolute().top_0().right_0().child( + panel_icon_button("open_change_list", IconName::PanelRight) + .disabled(true) + .tooltip(Tooltip::text("Changes list coming soon!")), + ) + } + + pub fn render_commit_editor( + &self, + name_and_email: Option<(SharedString, SharedString)>, + window: &mut Window, + cx: &mut Context, + ) -> impl IntoElement { + let editor = self.commit_editor.clone(); + let can_commit = !self.commit_pending && self.can_commit && !editor.read(cx).is_empty(cx); + let editor_focus_handle = editor.read(cx).focus_handle(cx).clone(); + + let focus_handle_1 = self.focus_handle(cx).clone(); + let focus_handle_2 = self.focus_handle(cx).clone(); + + let panel_editor_style = panel_editor_style(true, window, cx); + + let commit_staged_button = panel_filled_button("Commit") + .tooltip(move |window, cx| { + let focus_handle = focus_handle_1.clone(); + Tooltip::for_action_in( + "Commit all staged changes", + &CommitChanges, + &focus_handle, + window, + cx, + ) + }) + .when(!can_commit, |this| { + this.disabled(true).style(ButtonStyle::Transparent) + }); + // .on_click({ + // let name_and_email = name_and_email.clone(); + // cx.listener(move |this, _: &ClickEvent, window, cx| { + // this.commit_changes(&CommitChanges, name_and_email.clone(), window, cx) + // }) + // }); + + let commit_all_button = panel_filled_button("Commit All") + .tooltip(move |window, cx| { + let focus_handle = focus_handle_2.clone(); + Tooltip::for_action_in( + "Commit all changes, including unstaged changes", + &CommitAllChanges, + &focus_handle, + window, + cx, + ) + }) + .when(!can_commit, |this| { + this.disabled(true).style(ButtonStyle::Transparent) + }); + // .on_click({ + // let name_and_email = name_and_email.clone(); + // cx.listener(move |this, _: &ClickEvent, window, cx| { + // this.commit_tracked_changes( + // &CommitAllChanges, + // name_and_email.clone(), + // window, + // cx, + // ) + // }) + // }); + + let co_author_button = panel_icon_button("add-co-author", IconName::UserGroup) + .icon_color(if self.enable_auto_coauthors { + Color::Muted + } else { + Color::Accent + }) + .icon_size(IconSize::Small) + .toggle_state(self.enable_auto_coauthors) + // .on_click({ + // cx.listener(move |this, _: &ClickEvent, _, cx| { + // this.toggle_auto_coauthors(cx); + // }) + // }) + .tooltip(move |window, cx| { + Tooltip::with_meta( + "Toggle automatic co-authors", + None, + "Automatically adds current collaborators", + window, + cx, + ) + }); + + panel_editor_container(window, cx) + .id("commit-editor-container") + .relative() + .w_full() + .border_t_1() + .border_color(cx.theme().colors().border) + .h(px(140.)) + .bg(cx.theme().colors().editor_background) + .on_click(cx.listener(move |_, _: &ClickEvent, window, _cx| { + window.focus(&editor_focus_handle); + })) + .child(EditorElement::new(&self.commit_editor, panel_editor_style)) + .child(div().flex_1()) + .child( + h_flex() + .items_center() + .h_8() + .justify_between() + .gap_1() + .child(co_author_button) + .child(commit_all_button) + .child(commit_staged_button), + ) + } + + pub fn render_footer(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + h_flex() + .w_full() + .justify_between() + .child(h_flex().child("cmd+esc clear message")) + .child( + h_flex() + .child(panel_filled_button("Commit")) + .child(panel_filled_button("Commit All")), + ) + } + + fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context) { + cx.emit(DismissEvent); + } +} + +impl Render for QuickCommitModal { + fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement { + v_flex() + .id("quick-commit-modal") + .key_context("QuickCommit") + .on_action(cx.listener(Self::dismiss)) + .relative() + .bg(cx.theme().colors().elevated_surface_background) + .rounded(px(16.)) + .border_1() + .border_color(cx.theme().colors().border) + .py_2() + .px_4() + .w(self.width.unwrap_or(px(640.))) + .h(px(450.)) + .flex_1() + .overflow_hidden() + .child(self.render_header(window, cx)) + .child( + v_flex() + .flex_1() + // TODO: pass name_and_email + .child(self.render_commit_editor(None, window, cx)), + ) + .child(self.render_footer(window, cx)) + } +} diff --git a/crates/panel/Cargo.toml b/crates/panel/Cargo.toml index 4e7c81804d32b329bbc701b5e068777ab24d4a5b..3c51e6d6dcdb31922c07bd1d16923fdd10eeceb7 100644 --- a/crates/panel/Cargo.toml +++ b/crates/panel/Cargo.toml @@ -12,6 +12,9 @@ workspace = true path = "src/panel.rs" [dependencies] +editor.workspace = true gpui.workspace = true +settings.workspace = true +theme.workspace = true ui.workspace = true workspace.workspace = true diff --git a/crates/panel/src/panel.rs b/crates/panel/src/panel.rs index 017a362b0ef15f6bcb7adc9dbeaca7541c62d30a..934d8281a31f7869047dae7d07afe6b60719db29 100644 --- a/crates/panel/src/panel.rs +++ b/crates/panel/src/panel.rs @@ -1,5 +1,8 @@ //! # panel -use gpui::actions; +use editor::{Editor, EditorElement, EditorStyle}; +use gpui::{actions, Entity, TextStyle}; +use settings::Settings; +use theme::ThemeSettings; use ui::{prelude::*, Tab}; actions!(panel, [NextPanelTab, PreviousPanelTab]); @@ -46,7 +49,8 @@ pub fn panel_button(label: impl Into) -> ui::Button { let id = ElementId::Name(label.clone().to_lowercase().replace(' ', "_").into()); ui::Button::new(id, label) .label_size(ui::LabelSize::Small) - .layer(ui::ElevationIndex::Surface) + // TODO: Change this once we use on_surface_bg in button_like + .layer(ui::ElevationIndex::ModalSurface) .size(ui::ButtonSize::Compact) } @@ -57,10 +61,65 @@ pub fn panel_filled_button(label: impl Into) -> ui::Button { pub fn panel_icon_button(id: impl Into, icon: IconName) -> ui::IconButton { let id = ElementId::Name(id.into()); ui::IconButton::new(id, icon) - .layer(ui::ElevationIndex::Surface) + // TODO: Change this once we use on_surface_bg in button_like + .layer(ui::ElevationIndex::ModalSurface) .size(ui::ButtonSize::Compact) } pub fn panel_filled_icon_button(id: impl Into, icon: IconName) -> ui::IconButton { panel_icon_button(id, icon).style(ui::ButtonStyle::Filled) } + +pub fn panel_editor_container(_window: &mut Window, cx: &mut App) -> Div { + v_flex() + .size_full() + .gap(px(8.)) + .p_2() + .bg(cx.theme().colors().editor_background) +} + +pub fn panel_editor_style(monospace: bool, window: &mut Window, cx: &mut App) -> EditorStyle { + let settings = ThemeSettings::get_global(cx); + + let font_size = TextSize::Small.rems(cx).to_pixels(window.rem_size()); + + let (font_family, font_features, font_weight, line_height) = if monospace { + ( + settings.buffer_font.family.clone(), + settings.buffer_font.features.clone(), + settings.buffer_font.weight, + font_size * settings.buffer_line_height.value(), + ) + } else { + ( + settings.ui_font.family.clone(), + settings.ui_font.features.clone(), + settings.ui_font.weight, + window.line_height(), + ) + }; + + EditorStyle { + background: cx.theme().colors().editor_background, + local_player: cx.theme().players().local(), + text: TextStyle { + color: cx.theme().colors().text, + font_family, + font_features, + font_size: TextSize::Small.rems(cx).into(), + font_weight, + line_height: line_height.into(), + ..Default::default() + }, + ..Default::default() + } +} + +pub fn panel_editor_element( + editor: &Entity, + monospace: bool, + window: &mut Window, + cx: &mut App, +) -> EditorElement { + EditorElement::new(editor, panel_editor_style(monospace, window, cx)) +} diff --git a/crates/ui/src/components/button/button.rs b/crates/ui/src/components/button/button.rs index 4194b3c8d299642815585f1831411dfe99216888..0209fd3d17ccf9c66ee8f565eb60d38270f404e5 100644 --- a/crates/ui/src/components/button/button.rs +++ b/crates/ui/src/components/button/button.rs @@ -95,7 +95,7 @@ pub struct Button { selected_icon: Option, selected_icon_color: Option, key_binding: Option, - keybinding_position: KeybindingPosition, + key_binding_position: KeybindingPosition, alpha: Option, } @@ -121,7 +121,7 @@ impl Button { selected_icon: None, selected_icon_color: None, key_binding: None, - keybinding_position: KeybindingPosition::default(), + key_binding_position: KeybindingPosition::default(), alpha: None, } } @@ -197,7 +197,7 @@ impl Button { /// This method allows you to specify where the keybinding should be displayed /// in relation to the button's label. pub fn key_binding_position(mut self, position: KeybindingPosition) -> Self { - self.keybinding_position = position; + self.key_binding_position = position; self } @@ -427,7 +427,7 @@ impl RenderOnce for Button { .child( h_flex() .when( - self.keybinding_position == KeybindingPosition::Start, + self.key_binding_position == KeybindingPosition::Start, |this| this.flex_row_reverse(), ) .gap(DynamicSpacing::Base06.rems(cx)) diff --git a/crates/ui/src/components/button/button_like.rs b/crates/ui/src/components/button/button_like.rs index 0b78be078669aeffc8d44342cc0a34a250a756b4..96d093c249ab4a37955f00728cb8c3607877e80c 100644 --- a/crates/ui/src/components/button/button_like.rs +++ b/crates/ui/src/components/button/button_like.rs @@ -506,7 +506,9 @@ impl RenderOnce for ButtonLike { .group("") .flex_none() .h(self.height.unwrap_or(self.size.rems().into())) - .when_some(self.width, |this, width| this.w(width).justify_center()) + .when_some(self.width, |this, width| { + this.w(width).justify_center().text_center() + }) .when_some(self.rounding, |this, rounding| match rounding { ButtonLikeRounding::All => this.rounded_md(), ButtonLikeRounding::Left => this.rounded_l_md(), diff --git a/crates/ui/src/components/button/icon_button.rs b/crates/ui/src/components/button/icon_button.rs index c28c5ae9ac0dc1ff2f4e678a9b82b23a017b1cff..204ea8e564c8889fa7d7b36783dbae7bfb934f1c 100644 --- a/crates/ui/src/components/button/icon_button.rs +++ b/crates/ui/src/components/button/icon_button.rs @@ -22,6 +22,7 @@ pub struct IconButton { icon_size: IconSize, icon_color: Color, selected_icon: Option, + selected_icon_color: Option, indicator: Option, indicator_border_color: Option, alpha: Option, @@ -36,6 +37,7 @@ impl IconButton { icon_size: IconSize::default(), icon_color: Color::Default, selected_icon: None, + selected_icon_color: None, indicator: None, indicator_border_color: None, alpha: None, @@ -69,6 +71,12 @@ impl IconButton { self } + /// Sets the icon color used when the button is in a selected state. + pub fn selected_icon_color(mut self, color: impl Into>) -> Self { + self.selected_icon_color = color.into(); + self + } + pub fn indicator(mut self, indicator: Indicator) -> Self { self.indicator = Some(indicator); self @@ -181,6 +189,7 @@ impl RenderOnce for IconButton { .disabled(is_disabled) .toggle_state(is_selected) .selected_icon(self.selected_icon) + .selected_icon_color(self.selected_icon_color) .when_some(selected_style, |this, style| this.selected_style(style)) .when_some(self.indicator, |this, indicator| { this.indicator(indicator) diff --git a/crates/ui/src/components/toggle.rs b/crates/ui/src/components/toggle.rs index 13811883bcc7275a1aedb53b13a26199bf4f01e2..1f095065c30386375b7e1bfeb0d1adce5d2f3c8e 100644 --- a/crates/ui/src/components/toggle.rs +++ b/crates/ui/src/components/toggle.rs @@ -450,6 +450,64 @@ impl RenderOnce for Switch { } } +/// A [`Switch`] that has a [`Label`]. +#[derive(IntoElement)] +// #[component(scope = "input")] +pub struct SwitchWithLabel { + id: ElementId, + label: Label, + toggle_state: ToggleState, + on_click: Arc, + disabled: bool, +} + +impl SwitchWithLabel { + /// Creates a switch with an attached label. + pub fn new( + id: impl Into, + label: Label, + toggle_state: impl Into, + on_click: impl Fn(&ToggleState, &mut Window, &mut App) + 'static, + ) -> Self { + Self { + id: id.into(), + label, + toggle_state: toggle_state.into(), + on_click: Arc::new(on_click), + disabled: false, + } + } + + /// Sets the disabled state of the [`SwitchWithLabel`]. + pub fn disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } +} + +impl RenderOnce for SwitchWithLabel { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + h_flex() + .id(SharedString::from(format!("{}-container", self.id))) + .gap(DynamicSpacing::Base08.rems(cx)) + .child( + Switch::new(self.id.clone(), self.toggle_state) + .disabled(self.disabled) + .on_click({ + let on_click = self.on_click.clone(); + move |checked, window, cx| { + (on_click)(checked, window, cx); + } + }), + ) + .child( + div() + .id(SharedString::from(format!("{}-label", self.id))) + .child(self.label), + ) + } +} + impl ComponentPreview for Checkbox { fn preview(_window: &mut Window, _cx: &App) -> AnyElement { v_flex() From 4f7200527cba08740c9b4d4b56657267c4c82fe2 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 10 Feb 2025 19:13:12 +0200 Subject: [PATCH 42/42] Revert "Fix `editor::GoToDiagnostics` cycle (#24446)" (#24568) This reverts commit 4f65cfa93d183295289a4313e550c434b8ab7fc7. Release Notes: - N/A --- crates/editor/src/editor.rs | 24 ++--- crates/editor/src/editor_tests.rs | 170 ------------------------------ 2 files changed, 6 insertions(+), 188 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 15317e099938422cf2ddc457cfec7ad994faa579..410509139571e74e9caf72d6f156df915af768a0 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -10299,26 +10299,14 @@ impl Editor { if entry.diagnostic.is_primary && entry.diagnostic.severity <= DiagnosticSeverity::WARNING && entry.range.start != entry.range.end + // if we match with the active diagnostic, skip it + && Some(entry.diagnostic.group_id) + != self.active_diagnostics.as_ref().map(|d| d.group_id) { - let entry_group = entry.diagnostic.group_id; - let in_next_group = self.active_diagnostics.as_ref().map_or( - true, - |active| match direction { - Direction::Prev => { - entry_group != active.group_id - && (active.group_id == 0 || entry_group < active.group_id) - } - Direction::Next => { - entry_group != active.group_id - && (entry_group == 0 || entry_group > active.group_id) - } - }, - ); - if in_next_group { - return Some((entry.range, entry.diagnostic.group_id)); - } + Some((entry.range, entry.diagnostic.group_id)) + } else { + None } - None }); if let Some((primary_range, group_id)) = group { diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 5247c629c06919ce503c02545193de7143e7cafc..68bd0514dd9bac55c7032a25d46067dd191b83ba 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -10653,176 +10653,6 @@ async fn go_to_prev_overlapping_diagnostic( "}); } -#[gpui::test] -async fn cycle_through_same_place_diagnostics( - executor: BackgroundExecutor, - cx: &mut gpui::TestAppContext, -) { - init_test(cx, |_| {}); - - let mut cx = EditorTestContext::new(cx).await; - let lsp_store = - cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store()); - - cx.set_state(indoc! {" - ˇfn func(abc def: i32) -> u32 { - } - "}); - - cx.update(|_, cx| { - lsp_store.update(cx, |lsp_store, cx| { - lsp_store - .update_diagnostics( - LanguageServerId(0), - lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(), - version: None, - diagnostics: vec![ - lsp::Diagnostic { - range: lsp::Range::new( - lsp::Position::new(0, 11), - lsp::Position::new(0, 12), - ), - severity: Some(lsp::DiagnosticSeverity::ERROR), - ..Default::default() - }, - lsp::Diagnostic { - range: lsp::Range::new( - lsp::Position::new(0, 12), - lsp::Position::new(0, 15), - ), - severity: Some(lsp::DiagnosticSeverity::ERROR), - ..Default::default() - }, - lsp::Diagnostic { - range: lsp::Range::new( - lsp::Position::new(0, 12), - lsp::Position::new(0, 15), - ), - severity: Some(lsp::DiagnosticSeverity::ERROR), - ..Default::default() - }, - lsp::Diagnostic { - range: lsp::Range::new( - lsp::Position::new(0, 25), - lsp::Position::new(0, 28), - ), - severity: Some(lsp::DiagnosticSeverity::ERROR), - ..Default::default() - }, - ], - }, - &[], - cx, - ) - .unwrap() - }); - }); - executor.run_until_parked(); - - //// Backward - - // Fourth diagnostic - cx.update_editor(|editor, window, cx| { - editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx); - }); - cx.assert_editor_state(indoc! {" - fn func(abc def: i32) -> ˇu32 { - } - "}); - - // Third diagnostic - cx.update_editor(|editor, window, cx| { - editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx); - }); - cx.assert_editor_state(indoc! {" - fn func(abc ˇdef: i32) -> u32 { - } - "}); - - // Second diagnostic, same place - cx.update_editor(|editor, window, cx| { - editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx); - }); - cx.assert_editor_state(indoc! {" - fn func(abc ˇdef: i32) -> u32 { - } - "}); - - // First diagnostic - cx.update_editor(|editor, window, cx| { - editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx); - }); - cx.assert_editor_state(indoc! {" - fn func(abcˇ def: i32) -> u32 { - } - "}); - - // Wrapped over, fourth diagnostic - cx.update_editor(|editor, window, cx| { - editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx); - }); - cx.assert_editor_state(indoc! {" - fn func(abc def: i32) -> ˇu32 { - } - "}); - - cx.update_editor(|editor, window, cx| { - editor.move_to_beginning(&MoveToBeginning, window, cx); - }); - cx.assert_editor_state(indoc! {" - ˇfn func(abc def: i32) -> u32 { - } - "}); - - //// Forward - - // First diagnostic - cx.update_editor(|editor, window, cx| { - editor.go_to_diagnostic(&GoToDiagnostic, window, cx); - }); - cx.assert_editor_state(indoc! {" - fn func(abcˇ def: i32) -> u32 { - } - "}); - - // Second diagnostic - cx.update_editor(|editor, window, cx| { - editor.go_to_diagnostic(&GoToDiagnostic, window, cx); - }); - cx.assert_editor_state(indoc! {" - fn func(abc ˇdef: i32) -> u32 { - } - "}); - - // Third diagnostic, same place - cx.update_editor(|editor, window, cx| { - editor.go_to_diagnostic(&GoToDiagnostic, window, cx); - }); - cx.assert_editor_state(indoc! {" - fn func(abc ˇdef: i32) -> u32 { - } - "}); - - // Fourth diagnostic - cx.update_editor(|editor, window, cx| { - editor.go_to_diagnostic(&GoToDiagnostic, window, cx); - }); - cx.assert_editor_state(indoc! {" - fn func(abc def: i32) -> ˇu32 { - } - "}); - - // Wrapped around, first diagnostic - cx.update_editor(|editor, window, cx| { - editor.go_to_diagnostic(&GoToDiagnostic, window, cx); - }); - cx.assert_editor_state(indoc! {" - fn func(abcˇ def: i32) -> u32 { - } - "}); -} - #[gpui::test] async fn test_diagnostics_with_links(cx: &mut TestAppContext) { init_test(cx, |_| {});