diff --git a/Cargo.lock b/Cargo.lock index 46d8deb62b..b675614376 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,9 +79,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f2135563fb5c609d2b2b87c1e8ce7bc41b0b45430fa9661f457981503dd5bf0" +checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab" dependencies = [ "memchr", ] @@ -91,36 +91,25 @@ name = "ai" version = "0.1.0" dependencies = [ "anyhow", - "chrono", - "client", - "collections", - "ctor", - "editor", - "env_logger 0.9.3", - "fs", + "async-trait", + "bincode", "futures 0.3.28", "gpui", - "indoc", "isahc", - "language", + "lazy_static", "log", - "menu", + "matrixmultiply", "ordered-float", "parking_lot 0.11.2", - "project", + "parse_duration", + "postage", "rand 0.8.5", "regex", - "schemars", - "search", + "rusqlite", "serde", "serde_json", - "settings", - "smol", - "theme", - "tiktoken-rs 0.4.5", + "tiktoken-rs 0.5.4", "util", - "uuid 1.4.1", - "workspace", ] [[package]] @@ -305,6 +294,44 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" +[[package]] +name = "assistant" +version = "0.1.0" +dependencies = [ + "ai", + "anyhow", + "chrono", + "client", + "collections", + "ctor", + "editor", + "env_logger 0.9.3", + "fs", + "futures 0.3.28", + "gpui", + "indoc", + "isahc", + "language", + "log", + "menu", + "ordered-float", + "parking_lot 0.11.2", + "project", + "rand 0.8.5", + "regex", + "schemars", + "search", + "serde", + "serde_json", + "settings", + "smol", + "theme", + "tiktoken-rs 0.4.5", + "util", + "uuid 1.4.1", + "workspace", +] + [[package]] name = "async-broadcast" version = "0.4.1" @@ -2144,7 +2171,7 @@ dependencies = [ "convert_case 0.4.0", "proc-macro2", "quote", - "rustc_version 0.4.0", + "rustc_version", "syn 1.0.109", ] @@ -2586,6 +2613,7 @@ dependencies = [ name = "file_finder" version = "0.1.0" dependencies = [ + "collections", "ctor", "editor", "env_logger 0.9.3", @@ -3237,7 +3265,7 @@ dependencies = [ "indexmap 1.9.3", "slab", "tokio", - "tokio-util 0.7.8", + "tokio-util 0.7.9", "tracing", ] @@ -3358,9 +3386,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] name = "hex" @@ -3654,7 +3682,7 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ - "hermit-abi 0.3.2", + "hermit-abi 0.3.3", "libc", "windows-sys", ] @@ -3711,8 +3739,8 @@ version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ - "hermit-abi 0.3.2", - "rustix 0.38.13", + "hermit-abi 0.3.3", + "rustix 0.38.14", "windows-sys", ] @@ -4280,9 +4308,9 @@ checksum = "73cbba799671b762df5a175adf59ce145165747bb891505c43d09aefbbf38beb" [[package]] name = "matrixmultiply" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "090126dc04f95dc0d1c1c91f61bdd474b3930ca064c1edc8a849da2c6cbe1e77" +checksum = "7574c1cf36da4798ab73da5b215bbf444f50718207754cb522201d78d1cd0ff2" dependencies = [ "autocfg", "rawpointer", @@ -4556,6 +4584,19 @@ dependencies = [ "tempfile", ] +[[package]] +name = "ndarray" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32" +dependencies = [ + "matrixmultiply", + "num-complex 0.4.4", + "num-integer", + "num-traits", + "rawpointer", +] + [[package]] name = "ndk" version = "0.7.0" @@ -4682,7 +4723,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36" dependencies = [ "num-bigint 0.2.6", - "num-complex", + "num-complex 0.2.4", "num-integer", "num-iter", "num-rational 0.2.4", @@ -4738,6 +4779,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-complex" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" +dependencies = [ + "num-traits", +] + [[package]] name = "num-derive" version = "0.3.3" @@ -4809,7 +4859,7 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.3.2", + "hermit-abi 0.3.3", "libc", ] @@ -4846,7 +4896,7 @@ dependencies = [ "rmp", "rmpv", "tokio", - "tokio-util 0.7.8", + "tokio-util 0.7.9", ] [[package]] @@ -5147,11 +5197,11 @@ dependencies = [ [[package]] name = "pathfinder_simd" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39fe46acc5503595e5949c17b818714d26fdf9b4920eacf3b2947f0199f4a6ff" +checksum = "0444332826c70dc47be74a7c6a5fc44e23a7905ad6858d4162b658320455ef93" dependencies = [ - "rustc_version 0.3.3", + "rustc_version", ] [[package]] @@ -5186,17 +5236,6 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" -[[package]] -name = "pest" -version = "2.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a4d085fd991ac8d5b05a147b437791b4260b76326baf0fc60cf7c9c27ecd33" -dependencies = [ - "memchr", - "thiserror", - "ucd-trie", -] - [[package]] name = "petgraph" version = "0.6.4" @@ -5732,7 +5771,7 @@ dependencies = [ name = "quick_action_bar" version = "0.1.0" dependencies = [ - "ai", + "assistant", "editor", "gpui", "search", @@ -5868,9 +5907,9 @@ checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" [[package]] name = "rayon" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" dependencies = [ "either", "rayon-core", @@ -5878,14 +5917,12 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" dependencies = [ - "crossbeam-channel", "crossbeam-deque", "crossbeam-utils", - "num_cpus", ] [[package]] @@ -6335,22 +6372,13 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" -[[package]] -name = "rustc_version" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" -dependencies = [ - "semver 0.11.0", -] - [[package]] name = "rustc_version" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.18", + "semver", ] [[package]] @@ -6385,9 +6413,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.13" +version = "0.38.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662" +checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f" dependencies = [ "bitflags 2.4.0", "errno 0.3.3", @@ -6736,9 +6764,9 @@ dependencies = [ name = "semantic_index" version = "0.1.0" dependencies = [ + "ai", "anyhow", "async-trait", - "bincode", "client", "collections", "ctor", @@ -6747,15 +6775,13 @@ dependencies = [ "futures 0.3.28", "globset", "gpui", - "isahc", "language", "lazy_static", "log", - "matrixmultiply", + "ndarray", "node_runtime", "ordered-float", "parking_lot 0.11.2", - "parse_duration", "picker", "postage", "pretty_assertions", @@ -6789,30 +6815,12 @@ dependencies = [ "zed", ] -[[package]] -name = "semver" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" -dependencies = [ - "semver-parser", -] - [[package]] name = "semver" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" -[[package]] -name = "semver-parser" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" -dependencies = [ - "pest", -] - [[package]] name = "seq-macro" version = "0.2.2" @@ -6982,9 +6990,9 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if 1.0.0", "cpufeatures", @@ -7157,9 +7165,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" [[package]] name = "smol" @@ -7394,13 +7402,16 @@ name = "storybook" version = "0.1.0" dependencies = [ "anyhow", + "clap 4.4.4", "gpui2", "log", "rust-embed", "serde", "settings", "simplelog", + "strum", "theme", + "ui", "util", ] @@ -7421,6 +7432,28 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.37", +] + [[package]] name = "subtle" version = "2.4.1" @@ -7440,15 +7473,15 @@ dependencies = [ [[package]] name = "sval" -version = "2.6.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b031320a434d3e9477ccf9b5756d57d4272937b8d22cb88af80b7633a1b78b1" +checksum = "05d11eec9fbe2bc8bc71e7349f0e7534db9a96d961fb9f302574275b7880ad06" [[package]] name = "sval_buffer" -version = "2.6.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bf7e9412af26b342f3f2cc5cc4122b0105e9d16eb76046cd14ed10106cf6028" +checksum = "6b7451f69a93c5baf2653d5aa8bb4178934337f16c22830a50b06b386f72d761" dependencies = [ "sval", "sval_ref", @@ -7456,18 +7489,18 @@ dependencies = [ [[package]] name = "sval_dynamic" -version = "2.6.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0ef628e8a77a46ed3338db8d1b08af77495123cc229453084e47cd716d403cf" +checksum = "c34f5a2cc12b4da2adfb59d5eedfd9b174a23cc3fae84cec71dcbcd9302068f5" dependencies = [ "sval", ] [[package]] name = "sval_fmt" -version = "2.6.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dc09e9364c2045ab5fa38f7b04d077b3359d30c4c2b3ec4bae67a358bd64326" +checksum = "2f578b2301341e246d00b35957f2952c4ec554ad9c7cfaee10bc86bc92896578" dependencies = [ "itoa", "ryu", @@ -7476,9 +7509,9 @@ dependencies = [ [[package]] name = "sval_json" -version = "2.6.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ada6f627e38cbb8860283649509d87bc4a5771141daa41c78fd31f2b9485888d" +checksum = "8346c00f5dc6efe18bea8d13c1f7ca4f112b20803434bf3657ac17c0f74cbc4b" dependencies = [ "itoa", "ryu", @@ -7487,18 +7520,18 @@ dependencies = [ [[package]] name = "sval_ref" -version = "2.6.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703ca1942a984bd0d9b5a4c0a65ab8b4b794038d080af4eb303c71bc6bf22d7c" +checksum = "6617cc89952f792aebc0f4a1a76bc51e80c70b18c491bd52215c7989c4c3dd06" dependencies = [ "sval", ] [[package]] name = "sval_serde" -version = "2.6.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830926cd0581f7c3e5d51efae4d35c6b6fc4db583842652891ba2f1bed8db046" +checksum = "fe3d1e59f023341d9af75d86f3bc148a6704f3f831eef0dd90bbe9cb445fa024" dependencies = [ "serde", "sval", @@ -7649,7 +7682,7 @@ dependencies = [ "cfg-if 1.0.0", "fastrand 2.0.0", "redox_syscall 0.3.5", - "rustix 0.38.13", + "rustix 0.38.14", "windows-sys", ] @@ -8051,9 +8084,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" dependencies = [ "bytes 1.5.0", "futures-core", @@ -8152,7 +8185,7 @@ dependencies = [ "rand 0.8.5", "slab", "tokio", - "tokio-util 0.7.8", + "tokio-util 0.7.9", "tower-layer", "tower-service", "tracing", @@ -8598,10 +8631,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] -name = "ucd-trie" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" +name = "ui" +version = "0.1.0" +dependencies = [ + "anyhow", + "gpui2", + "serde", + "settings", + "theme", +] [[package]] name = "unicase" @@ -8671,9 +8709,9 @@ checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" [[package]] name = "unicode-width" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "unicode_categories" @@ -8864,6 +8902,7 @@ dependencies = [ "async-trait", "collections", "command_palette", + "diagnostics", "editor", "futures 0.3.28", "gpui", @@ -8885,6 +8924,7 @@ dependencies = [ "tokio", "util", "workspace", + "zed-actions", ] [[package]] @@ -9386,7 +9426,7 @@ dependencies = [ "either", "home", "once_cell", - "rustix 0.38.13", + "rustix 0.38.14", ] [[package]] @@ -9471,9 +9511,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ "winapi 0.3.9", ] @@ -9796,11 +9836,11 @@ dependencies = [ [[package]] name = "zed" -version = "0.106.0" +version = "0.107.0" dependencies = [ "activity_indicator", - "ai", "anyhow", + "assistant", "async-compression", "async-recursion 0.3.2", "async-tar", @@ -9865,12 +9905,14 @@ dependencies = [ "rpc", "rsa", "rust-embed", + "schemars", "search", "semantic_index", "serde", "serde_derive", "serde_json", "settings", + "shellexpand", "simplelog", "smallvec", "smol", diff --git a/Cargo.toml b/Cargo.toml index c1876434ad..f09d44e8da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "crates/activity_indicator", "crates/ai", + "crates/assistant", "crates/audio", "crates/auto_update", "crates/breadcrumbs", @@ -69,6 +70,7 @@ members = [ "crates/text", "crates/theme", "crates/theme_selector", + "crates/ui", "crates/util", "crates/semantic_index", "crates/vim", diff --git a/README.md b/README.md index 6c502ebc74..b3d4987526 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea sudo xcodebuild -license ``` -* Install homebrew, node and rustup-init (rutup, rust, cargo, etc.) +* Install homebrew, node and rustup-init (rustup, rust, cargo, etc.) ``` /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" brew install node rustup-init @@ -36,7 +36,7 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea brew install foreman ``` -* Ensure the Zed.dev website is checked out in a sibling directory and install it's dependencies: +* Ensure the Zed.dev website is checked out in a sibling directory and install its dependencies: ``` cd .. diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 8fbe87de2b..8422d53abc 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -30,6 +30,7 @@ "cmd-s": "workspace::Save", "cmd-shift-s": "workspace::SaveAs", "cmd-=": "zed::IncreaseBufferFontSize", + "cmd-+": "zed::IncreaseBufferFontSize", "cmd--": "zed::DecreaseBufferFontSize", "cmd-0": "zed::ResetBufferFontSize", "cmd-,": "zed::OpenSettings", @@ -249,6 +250,7 @@ "bindings": { "escape": "project_search::ToggleFocus", "alt-tab": "search::CycleMode", + "cmd-shift-h": "search::ToggleReplace", "alt-cmd-g": "search::ActivateRegexMode", "alt-cmd-s": "search::ActivateSemanticMode", "alt-cmd-x": "search::ActivateTextMode" @@ -261,11 +263,19 @@ "down": "search::NextHistoryQuery" } }, + { + "context": "ProjectSearchBar && in_replace", + "bindings": { + "enter": "search::ReplaceNext", + "cmd-enter": "search::ReplaceAll" + } + }, { "context": "ProjectSearchView", "bindings": { "escape": "project_search::ToggleFocus", "alt-tab": "search::CycleMode", + "cmd-shift-h": "search::ToggleReplace", "alt-cmd-g": "search::ActivateRegexMode", "alt-cmd-s": "search::ActivateSemanticMode", "alt-cmd-x": "search::ActivateTextMode" @@ -277,6 +287,7 @@ "cmd-f": "project_search::ToggleFocus", "cmd-g": "search::SelectNextMatch", "cmd-shift-g": "search::SelectPrevMatch", + "cmd-shift-h": "search::ToggleReplace", "alt-enter": "search::SelectAllMatches", "alt-cmd-c": "search::ToggleCaseSensitive", "alt-cmd-w": "search::ToggleWholeWord", @@ -498,6 +509,22 @@ "cmd-k cmd-down": [ "workspace::ActivatePaneInDirection", "Down" + ], + "cmd-k shift-left": [ + "workspace::SwapPaneInDirection", + "Left" + ], + "cmd-k shift-right": [ + "workspace::SwapPaneInDirection", + "Right" + ], + "cmd-k shift-up": [ + "workspace::SwapPaneInDirection", + "Up" + ], + "cmd-k shift-down": [ + "workspace::SwapPaneInDirection", + "Down" ] } }, @@ -562,7 +589,7 @@ } }, { - "context": "ProjectSearchBar", + "context": "ProjectSearchBar && !in_replace", "bindings": { "cmd-enter": "project_search::SearchInNew" } @@ -588,14 +615,20 @@ } }, { - "context": "CollabPanel", + "context": "CollabPanel && not_editing", "bindings": { "ctrl-backspace": "collab_panel::Remove", "space": "menu::Confirm" } }, { - "context": "CollabPanel > Editor", + "context": "(CollabPanel && editing) > Editor", + "bindings": { + "space": "collab_panel::InsertSpace" + } + }, + { + "context": "(CollabPanel && not_editing) > Editor", "bindings": { "cmd-c": "collab_panel::StartLinkChannel", "cmd-x": "collab_panel::StartMoveChannel", diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 9e69240d27..5362f7c0e5 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -18,6 +18,7 @@ } } ], + ":": "command_palette::Toggle", "h": "vim::Left", "left": "vim::Left", "backspace": "vim::Backspace", @@ -125,6 +126,21 @@ "g shift-t": "pane::ActivatePrevItem", "g d": "editor::GoToDefinition", "g shift-d": "editor::GoToTypeDefinition", + "g n": "vim::SelectNext", + "g shift-n": "vim::SelectPrevious", + "g >": [ + "editor::SelectNext", + { + "replace_newest": true + } + ], + "g <": [ + "editor::SelectPrevious", + { + "replace_newest": true + } + ], + "g a": "editor::SelectAllMatches", "g s": "outline::Toggle", "g shift-s": "project_symbols::Toggle", "g .": "editor::ToggleCodeActions", // zed specific @@ -205,13 +221,13 @@ "shift-z shift-q": [ "pane::CloseActiveItem", { - "saveBehavior": "dontSave" + "saveIntent": "skip" } ], "shift-z shift-z": [ "pane::CloseActiveItem", { - "saveBehavior": "promptOnConflict" + "saveIntent": "saveAll" } ], // Count support @@ -300,6 +316,38 @@ "workspace::ActivatePaneInDirection", "Down" ], + "ctrl-w shift-left": [ + "workspace::SwapPaneInDirection", + "Left" + ], + "ctrl-w shift-right": [ + "workspace::SwapPaneInDirection", + "Right" + ], + "ctrl-w shift-up": [ + "workspace::SwapPaneInDirection", + "Up" + ], + "ctrl-w shift-down": [ + "workspace::SwapPaneInDirection", + "Down" + ], + "ctrl-w shift-h": [ + "workspace::SwapPaneInDirection", + "Left" + ], + "ctrl-w shift-l": [ + "workspace::SwapPaneInDirection", + "Right" + ], + "ctrl-w shift-k": [ + "workspace::SwapPaneInDirection", + "Up" + ], + "ctrl-w shift-j": [ + "workspace::SwapPaneInDirection", + "Down" + ], "ctrl-w g t": "pane::ActivateNextItem", "ctrl-w ctrl-g t": "pane::ActivateNextItem", "ctrl-w g shift-t": "pane::ActivatePrevItem", @@ -318,7 +366,17 @@ "ctrl-w c": "pane::CloseAllItems", "ctrl-w ctrl-c": "pane::CloseAllItems", "ctrl-w q": "pane::CloseAllItems", - "ctrl-w ctrl-q": "pane::CloseAllItems" + "ctrl-w ctrl-q": "pane::CloseAllItems", + "ctrl-w o": "workspace::CloseInactiveTabsAndPanes", + "ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes", + "ctrl-w n": [ + "workspace::NewFileInDirection", + "Up" + ], + "ctrl-w ctrl-n": [ + "workspace::NewFileInDirection", + "Up" + ] } }, { diff --git a/assets/settings/default.json b/assets/settings/default.json index 22ea266533..1f8068d109 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -372,6 +372,27 @@ "semantic_index": { "enabled": false }, + // Settings specific to our elixir integration + "elixir": { + // Set Zed to use the experimental Next LS LSP server. + // Note that changing this setting requires a restart of Zed + // to take effect. + // + // May take 3 values: + // 1. Use the standard elixir-ls LSP server + // "next": "off" + // 2. Use a bundled version of the next Next LS LSP server + // "next": "on", + // 3. Use a local build of the next Next LS LSP server: + // "next": { + // "local": { + // "path": "~/next-ls/bin/start", + // "arguments": ["--stdio"] + // } + // }, + // + "next": "off" + }, // Different settings for specific languages. "languages": { "Plain Text": { diff --git a/crates/ai/Cargo.toml b/crates/ai/Cargo.toml index 8002b0d35d..a2c70ce8c6 100644 --- a/crates/ai/Cargo.toml +++ b/crates/ai/Cargo.toml @@ -9,39 +9,26 @@ path = "src/ai.rs" doctest = false [dependencies] -client = { path = "../client" } -collections = { path = "../collections"} -editor = { path = "../editor" } -fs = { path = "../fs" } gpui = { path = "../gpui" } -language = { path = "../language" } -menu = { path = "../menu" } -search = { path = "../search" } -settings = { path = "../settings" } -theme = { path = "../theme" } util = { path = "../util" } -uuid = { version = "1.1.2", features = ["v4"] } -workspace = { path = "../workspace" } - +async-trait.workspace = true anyhow.workspace = true -chrono = { version = "0.4", features = ["serde"] } futures.workspace = true -indoc.workspace = true -isahc.workspace = true +lazy_static.workspace = true ordered-float.workspace = true parking_lot.workspace = true +isahc.workspace = true regex.workspace = true -schemars.workspace = true serde.workspace = true serde_json.workspace = true -smol.workspace = true -tiktoken-rs = "0.4" +postage.workspace = true +rand.workspace = true +log.workspace = true +parse_duration = "2.1.1" +tiktoken-rs = "0.5.0" +matrixmultiply = "0.3.7" +rusqlite = { version = "0.27.0", features = ["blob", "array", "modern_sqlite"] } +bincode = "1.3.3" [dev-dependencies] -editor = { path = "../editor", features = ["test-support"] } -project = { path = "../project", features = ["test-support"] } - -ctor.workspace = true -env_logger.workspace = true -log.workspace = true -rand.workspace = true +gpui = { path = "../gpui", features = ["test-support"] } diff --git a/crates/ai/src/ai.rs b/crates/ai/src/ai.rs index dfd9a523b4..5256a6a643 100644 --- a/crates/ai/src/ai.rs +++ b/crates/ai/src/ai.rs @@ -1,294 +1,2 @@ -pub mod assistant; -mod assistant_settings; -mod codegen; -mod streaming_diff; - -use anyhow::{anyhow, Result}; -pub use assistant::AssistantPanel; -use assistant_settings::OpenAIModel; -use chrono::{DateTime, Local}; -use collections::HashMap; -use fs::Fs; -use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt}; -use gpui::{executor::Background, AppContext}; -use isahc::{http::StatusCode, Request, RequestExt}; -use regex::Regex; -use serde::{Deserialize, Serialize}; -use std::{ - cmp::Reverse, - ffi::OsStr, - fmt::{self, Display}, - io, - path::PathBuf, - sync::Arc, -}; -use util::paths::CONVERSATIONS_DIR; - -const OPENAI_API_URL: &'static str = "https://api.openai.com/v1"; - -// Data types for chat completion requests -#[derive(Debug, Default, Serialize)] -pub struct OpenAIRequest { - model: String, - messages: Vec, - stream: bool, -} - -#[derive( - Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize, -)] -struct MessageId(usize); - -#[derive(Clone, Debug, Serialize, Deserialize)] -struct MessageMetadata { - role: Role, - sent_at: DateTime, - status: MessageStatus, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -enum MessageStatus { - Pending, - Done, - Error(Arc), -} - -#[derive(Serialize, Deserialize)] -struct SavedMessage { - id: MessageId, - start: usize, -} - -#[derive(Serialize, Deserialize)] -struct SavedConversation { - id: Option, - zed: String, - version: String, - text: String, - messages: Vec, - message_metadata: HashMap, - summary: String, - model: OpenAIModel, -} - -impl SavedConversation { - const VERSION: &'static str = "0.1.0"; -} - -struct SavedConversationMetadata { - title: String, - path: PathBuf, - mtime: chrono::DateTime, -} - -impl SavedConversationMetadata { - pub async fn list(fs: Arc) -> Result> { - fs.create_dir(&CONVERSATIONS_DIR).await?; - - let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?; - let mut conversations = Vec::::new(); - while let Some(path) = paths.next().await { - let path = path?; - if path.extension() != Some(OsStr::new("json")) { - continue; - } - - let pattern = r" - \d+.zed.json$"; - let re = Regex::new(pattern).unwrap(); - - let metadata = fs.metadata(&path).await?; - if let Some((file_name, metadata)) = path - .file_name() - .and_then(|name| name.to_str()) - .zip(metadata) - { - let title = re.replace(file_name, ""); - conversations.push(Self { - title: title.into_owned(), - path, - mtime: metadata.mtime.into(), - }); - } - } - conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime)); - - Ok(conversations) - } -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -struct RequestMessage { - role: Role, - content: String, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct ResponseMessage { - role: Option, - content: Option, -} - -#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)] -#[serde(rename_all = "lowercase")] -enum Role { - User, - Assistant, - System, -} - -impl Role { - pub fn cycle(&mut self) { - *self = match self { - Role::User => Role::Assistant, - Role::Assistant => Role::System, - Role::System => Role::User, - } - } -} - -impl Display for Role { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Role::User => write!(f, "User"), - Role::Assistant => write!(f, "Assistant"), - Role::System => write!(f, "System"), - } - } -} - -#[derive(Deserialize, Debug)] -pub struct OpenAIResponseStreamEvent { - pub id: Option, - pub object: String, - pub created: u32, - pub model: String, - pub choices: Vec, - pub usage: Option, -} - -#[derive(Deserialize, Debug)] -pub struct Usage { - pub prompt_tokens: u32, - pub completion_tokens: u32, - pub total_tokens: u32, -} - -#[derive(Deserialize, Debug)] -pub struct ChatChoiceDelta { - pub index: u32, - pub delta: ResponseMessage, - pub finish_reason: Option, -} - -#[derive(Deserialize, Debug)] -struct OpenAIUsage { - prompt_tokens: u64, - completion_tokens: u64, - total_tokens: u64, -} - -#[derive(Deserialize, Debug)] -struct OpenAIChoice { - text: String, - index: u32, - logprobs: Option, - finish_reason: Option, -} - -pub fn init(cx: &mut AppContext) { - assistant::init(cx); -} - -pub async fn stream_completion( - api_key: String, - executor: Arc, - mut request: OpenAIRequest, -) -> Result>> { - request.stream = true; - - let (tx, rx) = futures::channel::mpsc::unbounded::>(); - - let json_data = serde_json::to_string(&request)?; - let mut response = Request::post(format!("{OPENAI_API_URL}/chat/completions")) - .header("Content-Type", "application/json") - .header("Authorization", format!("Bearer {}", api_key)) - .body(json_data)? - .send_async() - .await?; - - let status = response.status(); - if status == StatusCode::OK { - executor - .spawn(async move { - let mut lines = BufReader::new(response.body_mut()).lines(); - - fn parse_line( - line: Result, - ) -> Result> { - if let Some(data) = line?.strip_prefix("data: ") { - let event = serde_json::from_str(&data)?; - Ok(Some(event)) - } else { - Ok(None) - } - } - - while let Some(line) = lines.next().await { - if let Some(event) = parse_line(line).transpose() { - let done = event.as_ref().map_or(false, |event| { - event - .choices - .last() - .map_or(false, |choice| choice.finish_reason.is_some()) - }); - if tx.unbounded_send(event).is_err() { - break; - } - - if done { - break; - } - } - } - - anyhow::Ok(()) - }) - .detach(); - - Ok(rx) - } else { - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; - - #[derive(Deserialize)] - struct OpenAIResponse { - error: OpenAIError, - } - - #[derive(Deserialize)] - struct OpenAIError { - message: String, - } - - match serde_json::from_str::(&body) { - Ok(response) if !response.error.message.is_empty() => Err(anyhow!( - "Failed to connect to OpenAI API: {}", - response.error.message, - )), - - _ => Err(anyhow!( - "Failed to connect to OpenAI API: {} {}", - response.status(), - body, - )), - } - } -} - -#[cfg(test)] -#[ctor::ctor] -fn init_logger() { - if std::env::var("RUST_LOG").is_ok() { - env_logger::init(); - } -} +pub mod completion; +pub mod embedding; diff --git a/crates/ai/src/completion.rs b/crates/ai/src/completion.rs new file mode 100644 index 0000000000..170b2268f9 --- /dev/null +++ b/crates/ai/src/completion.rs @@ -0,0 +1,212 @@ +use anyhow::{anyhow, Result}; +use futures::{ + future::BoxFuture, io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, FutureExt, + Stream, StreamExt, +}; +use gpui::executor::Background; +use isahc::{http::StatusCode, Request, RequestExt}; +use serde::{Deserialize, Serialize}; +use std::{ + fmt::{self, Display}, + io, + sync::Arc, +}; + +pub const OPENAI_API_URL: &'static str = "https://api.openai.com/v1"; + +#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum Role { + User, + Assistant, + System, +} + +impl Role { + pub fn cycle(&mut self) { + *self = match self { + Role::User => Role::Assistant, + Role::Assistant => Role::System, + Role::System => Role::User, + } + } +} + +impl Display for Role { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Role::User => write!(f, "User"), + Role::Assistant => write!(f, "Assistant"), + Role::System => write!(f, "System"), + } + } +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct RequestMessage { + pub role: Role, + pub content: String, +} + +#[derive(Debug, Default, Serialize)] +pub struct OpenAIRequest { + pub model: String, + pub messages: Vec, + pub stream: bool, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct ResponseMessage { + pub role: Option, + pub content: Option, +} + +#[derive(Deserialize, Debug)] +pub struct OpenAIUsage { + pub prompt_tokens: u32, + pub completion_tokens: u32, + pub total_tokens: u32, +} + +#[derive(Deserialize, Debug)] +pub struct ChatChoiceDelta { + pub index: u32, + pub delta: ResponseMessage, + pub finish_reason: Option, +} + +#[derive(Deserialize, Debug)] +pub struct OpenAIResponseStreamEvent { + pub id: Option, + pub object: String, + pub created: u32, + pub model: String, + pub choices: Vec, + pub usage: Option, +} + +pub async fn stream_completion( + api_key: String, + executor: Arc, + mut request: OpenAIRequest, +) -> Result>> { + request.stream = true; + + let (tx, rx) = futures::channel::mpsc::unbounded::>(); + + let json_data = serde_json::to_string(&request)?; + let mut response = Request::post(format!("{OPENAI_API_URL}/chat/completions")) + .header("Content-Type", "application/json") + .header("Authorization", format!("Bearer {}", api_key)) + .body(json_data)? + .send_async() + .await?; + + let status = response.status(); + if status == StatusCode::OK { + executor + .spawn(async move { + let mut lines = BufReader::new(response.body_mut()).lines(); + + fn parse_line( + line: Result, + ) -> Result> { + if let Some(data) = line?.strip_prefix("data: ") { + let event = serde_json::from_str(&data)?; + Ok(Some(event)) + } else { + Ok(None) + } + } + + while let Some(line) = lines.next().await { + if let Some(event) = parse_line(line).transpose() { + let done = event.as_ref().map_or(false, |event| { + event + .choices + .last() + .map_or(false, |choice| choice.finish_reason.is_some()) + }); + if tx.unbounded_send(event).is_err() { + break; + } + + if done { + break; + } + } + } + + anyhow::Ok(()) + }) + .detach(); + + Ok(rx) + } else { + let mut body = String::new(); + response.body_mut().read_to_string(&mut body).await?; + + #[derive(Deserialize)] + struct OpenAIResponse { + error: OpenAIError, + } + + #[derive(Deserialize)] + struct OpenAIError { + message: String, + } + + match serde_json::from_str::(&body) { + Ok(response) if !response.error.message.is_empty() => Err(anyhow!( + "Failed to connect to OpenAI API: {}", + response.error.message, + )), + + _ => Err(anyhow!( + "Failed to connect to OpenAI API: {} {}", + response.status(), + body, + )), + } + } +} + +pub trait CompletionProvider { + fn complete( + &self, + prompt: OpenAIRequest, + ) -> BoxFuture<'static, Result>>>; +} + +pub struct OpenAICompletionProvider { + api_key: String, + executor: Arc, +} + +impl OpenAICompletionProvider { + pub fn new(api_key: String, executor: Arc) -> Self { + Self { api_key, executor } + } +} + +impl CompletionProvider for OpenAICompletionProvider { + fn complete( + &self, + prompt: OpenAIRequest, + ) -> BoxFuture<'static, Result>>> { + let request = stream_completion(self.api_key.clone(), self.executor.clone(), prompt); + async move { + let response = request.await?; + let stream = response + .filter_map(|response| async move { + match response { + Ok(mut response) => Some(Ok(response.choices.pop()?.delta.content?)), + Err(error) => Some(Err(error)), + } + }) + .boxed(); + Ok(stream) + } + .boxed() + } +} diff --git a/crates/semantic_index/src/embedding.rs b/crates/ai/src/embedding.rs similarity index 91% rename from crates/semantic_index/src/embedding.rs rename to crates/ai/src/embedding.rs index b0124bf7df..332470aa54 100644 --- a/crates/semantic_index/src/embedding.rs +++ b/crates/ai/src/embedding.rs @@ -27,8 +27,30 @@ lazy_static! { } #[derive(Debug, PartialEq, Clone)] -pub struct Embedding(Vec); +pub struct Embedding(pub Vec); +// This is needed for semantic index functionality +// Unfortunately it has to live wherever the "Embedding" struct is created. +// Keeping this in here though, introduces a 'rusqlite' dependency into AI +// which is less than ideal +impl FromSql for Embedding { + fn column_result(value: ValueRef) -> FromSqlResult { + let bytes = value.as_blob()?; + let embedding: Result, Box> = bincode::deserialize(bytes); + if embedding.is_err() { + return Err(rusqlite::types::FromSqlError::Other(embedding.unwrap_err())); + } + Ok(Embedding(embedding.unwrap())) + } +} + +impl ToSql for Embedding { + fn to_sql(&self) -> rusqlite::Result { + let bytes = bincode::serialize(&self.0) + .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?; + Ok(ToSqlOutput::Owned(rusqlite::types::Value::Blob(bytes))) + } +} impl From> for Embedding { fn from(value: Vec) -> Self { Embedding(value) @@ -63,24 +85,24 @@ impl Embedding { } } -impl FromSql for Embedding { - fn column_result(value: ValueRef) -> FromSqlResult { - let bytes = value.as_blob()?; - let embedding: Result, Box> = bincode::deserialize(bytes); - if embedding.is_err() { - return Err(rusqlite::types::FromSqlError::Other(embedding.unwrap_err())); - } - Ok(Embedding(embedding.unwrap())) - } -} +// impl FromSql for Embedding { +// fn column_result(value: ValueRef) -> FromSqlResult { +// let bytes = value.as_blob()?; +// let embedding: Result, Box> = bincode::deserialize(bytes); +// if embedding.is_err() { +// return Err(rusqlite::types::FromSqlError::Other(embedding.unwrap_err())); +// } +// Ok(Embedding(embedding.unwrap())) +// } +// } -impl ToSql for Embedding { - fn to_sql(&self) -> rusqlite::Result { - let bytes = bincode::serialize(&self.0) - .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?; - Ok(ToSqlOutput::Owned(rusqlite::types::Value::Blob(bytes))) - } -} +// impl ToSql for Embedding { +// fn to_sql(&self) -> rusqlite::Result { +// let bytes = bincode::serialize(&self.0) +// .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?; +// Ok(ToSqlOutput::Owned(rusqlite::types::Value::Blob(bytes))) +// } +// } #[derive(Clone)] pub struct OpenAIEmbeddings { @@ -117,6 +139,7 @@ struct OpenAIEmbeddingUsage { #[async_trait] pub trait EmbeddingProvider: Sync + Send { + fn is_authenticated(&self) -> bool; async fn embed_batch(&self, spans: Vec) -> Result>; fn max_tokens_per_batch(&self) -> usize; fn truncate(&self, span: &str) -> (String, usize); @@ -127,6 +150,9 @@ pub struct DummyEmbeddings {} #[async_trait] impl EmbeddingProvider for DummyEmbeddings { + fn is_authenticated(&self) -> bool { + true + } fn rate_limit_expiration(&self) -> Option { None } @@ -229,6 +255,9 @@ impl OpenAIEmbeddings { #[async_trait] impl EmbeddingProvider for OpenAIEmbeddings { + fn is_authenticated(&self) -> bool { + OPENAI_API_KEY.as_ref().is_some() + } fn max_tokens_per_batch(&self) -> usize { 50000 } diff --git a/crates/assistant/Cargo.toml b/crates/assistant/Cargo.toml new file mode 100644 index 0000000000..5d141b32d5 --- /dev/null +++ b/crates/assistant/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "assistant" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/assistant.rs" +doctest = false + +[dependencies] +ai = { path = "../ai" } +client = { path = "../client" } +collections = { path = "../collections"} +editor = { path = "../editor" } +fs = { path = "../fs" } +gpui = { path = "../gpui" } +language = { path = "../language" } +menu = { path = "../menu" } +search = { path = "../search" } +settings = { path = "../settings" } +theme = { path = "../theme" } +util = { path = "../util" } +uuid = { version = "1.1.2", features = ["v4"] } +workspace = { path = "../workspace" } + +anyhow.workspace = true +chrono = { version = "0.4", features = ["serde"] } +futures.workspace = true +indoc.workspace = true +isahc.workspace = true +ordered-float.workspace = true +parking_lot.workspace = true +regex.workspace = true +schemars.workspace = true +serde.workspace = true +serde_json.workspace = true +smol.workspace = true +tiktoken-rs = "0.4" + +[dev-dependencies] +editor = { path = "../editor", features = ["test-support"] } +project = { path = "../project", features = ["test-support"] } + +ctor.workspace = true +env_logger.workspace = true +log.workspace = true +rand.workspace = true diff --git a/crates/ai/README.zmd b/crates/assistant/README.zmd similarity index 100% rename from crates/ai/README.zmd rename to crates/assistant/README.zmd diff --git a/crates/ai/features.zmd b/crates/assistant/features.zmd similarity index 100% rename from crates/ai/features.zmd rename to crates/assistant/features.zmd diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs new file mode 100644 index 0000000000..258684db47 --- /dev/null +++ b/crates/assistant/src/assistant.rs @@ -0,0 +1,112 @@ +pub mod assistant_panel; +mod assistant_settings; +mod codegen; +mod streaming_diff; + +use ai::completion::Role; +use anyhow::Result; +pub use assistant_panel::AssistantPanel; +use assistant_settings::OpenAIModel; +use chrono::{DateTime, Local}; +use collections::HashMap; +use fs::Fs; +use futures::StreamExt; +use gpui::AppContext; +use regex::Regex; +use serde::{Deserialize, Serialize}; +use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc}; +use util::paths::CONVERSATIONS_DIR; + +#[derive( + Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize, +)] +struct MessageId(usize); + +#[derive(Clone, Debug, Serialize, Deserialize)] +struct MessageMetadata { + role: Role, + sent_at: DateTime, + status: MessageStatus, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +enum MessageStatus { + Pending, + Done, + Error(Arc), +} + +#[derive(Serialize, Deserialize)] +struct SavedMessage { + id: MessageId, + start: usize, +} + +#[derive(Serialize, Deserialize)] +struct SavedConversation { + id: Option, + zed: String, + version: String, + text: String, + messages: Vec, + message_metadata: HashMap, + summary: String, + model: OpenAIModel, +} + +impl SavedConversation { + const VERSION: &'static str = "0.1.0"; +} + +struct SavedConversationMetadata { + title: String, + path: PathBuf, + mtime: chrono::DateTime, +} + +impl SavedConversationMetadata { + pub async fn list(fs: Arc) -> Result> { + fs.create_dir(&CONVERSATIONS_DIR).await?; + + let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?; + let mut conversations = Vec::::new(); + while let Some(path) = paths.next().await { + let path = path?; + if path.extension() != Some(OsStr::new("json")) { + continue; + } + + let pattern = r" - \d+.zed.json$"; + let re = Regex::new(pattern).unwrap(); + + let metadata = fs.metadata(&path).await?; + if let Some((file_name, metadata)) = path + .file_name() + .and_then(|name| name.to_str()) + .zip(metadata) + { + let title = re.replace(file_name, ""); + conversations.push(Self { + title: title.into_owned(), + path, + mtime: metadata.mtime.into(), + }); + } + } + conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime)); + + Ok(conversations) + } +} + +pub fn init(cx: &mut AppContext) { + assistant_panel::init(cx); +} + +#[cfg(test)] +#[ctor::ctor] +fn init_logger() { + if std::env::var("RUST_LOG").is_ok() { + env_logger::init(); + } +} diff --git a/crates/ai/src/assistant.rs b/crates/assistant/src/assistant_panel.rs similarity index 99% rename from crates/ai/src/assistant.rs rename to crates/assistant/src/assistant_panel.rs index 263382c03e..42e5fb7897 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -1,8 +1,11 @@ use crate::{ assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel}, - codegen::{self, Codegen, CodegenKind, OpenAICompletionProvider}, - stream_completion, MessageId, MessageMetadata, MessageStatus, OpenAIRequest, RequestMessage, - Role, SavedConversation, SavedConversationMetadata, SavedMessage, OPENAI_API_URL, + codegen::{self, Codegen, CodegenKind}, + MessageId, MessageMetadata, MessageStatus, Role, SavedConversation, SavedConversationMetadata, + SavedMessage, +}; +use ai::completion::{ + stream_completion, OpenAICompletionProvider, OpenAIRequest, RequestMessage, OPENAI_API_URL, }; use anyhow::{anyhow, Result}; use chrono::{DateTime, Local}; diff --git a/crates/ai/src/assistant_settings.rs b/crates/assistant/src/assistant_settings.rs similarity index 100% rename from crates/ai/src/assistant_settings.rs rename to crates/assistant/src/assistant_settings.rs diff --git a/crates/ai/src/codegen.rs b/crates/assistant/src/codegen.rs similarity index 94% rename from crates/ai/src/codegen.rs rename to crates/assistant/src/codegen.rs index e7da46cdf9..e956d72260 100644 --- a/crates/ai/src/codegen.rs +++ b/crates/assistant/src/codegen.rs @@ -1,59 +1,14 @@ -use crate::{ - stream_completion, - streaming_diff::{Hunk, StreamingDiff}, - OpenAIRequest, -}; +use crate::streaming_diff::{Hunk, StreamingDiff}; +use ai::completion::{CompletionProvider, OpenAIRequest}; use anyhow::Result; use editor::{ multi_buffer, Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, }; -use futures::{ - channel::mpsc, future::BoxFuture, stream::BoxStream, FutureExt, SinkExt, Stream, StreamExt, -}; -use gpui::{executor::Background, Entity, ModelContext, ModelHandle, Task}; +use futures::{channel::mpsc, SinkExt, Stream, StreamExt}; +use gpui::{Entity, ModelContext, ModelHandle, Task}; use language::{Rope, TransactionId}; use std::{cmp, future, ops::Range, sync::Arc}; -pub trait CompletionProvider { - fn complete( - &self, - prompt: OpenAIRequest, - ) -> BoxFuture<'static, Result>>>; -} - -pub struct OpenAICompletionProvider { - api_key: String, - executor: Arc, -} - -impl OpenAICompletionProvider { - pub fn new(api_key: String, executor: Arc) -> Self { - Self { api_key, executor } - } -} - -impl CompletionProvider for OpenAICompletionProvider { - fn complete( - &self, - prompt: OpenAIRequest, - ) -> BoxFuture<'static, Result>>> { - let request = stream_completion(self.api_key.clone(), self.executor.clone(), prompt); - async move { - let response = request.await?; - let stream = response - .filter_map(|response| async move { - match response { - Ok(mut response) => Some(Ok(response.choices.pop()?.delta.content?)), - Err(error) => Some(Err(error)), - } - }) - .boxed(); - Ok(stream) - } - .boxed() - } -} - pub enum Event { Finished, Undone, @@ -397,13 +352,17 @@ fn strip_markdown_codeblock( #[cfg(test)] mod tests { use super::*; - use futures::stream; + use futures::{ + future::BoxFuture, + stream::{self, BoxStream}, + }; use gpui::{executor::Deterministic, TestAppContext}; use indoc::indoc; use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point}; use parking_lot::Mutex; use rand::prelude::*; use settings::SettingsStore; + use smol::future::FutureExt; #[gpui::test(iterations = 10)] async fn test_transform_autoindent( diff --git a/crates/ai/src/streaming_diff.rs b/crates/assistant/src/streaming_diff.rs similarity index 100% rename from crates/ai/src/streaming_diff.rs rename to crates/assistant/src/streaming_diff.rs diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 3c5e18713b..8c570c7165 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -394,9 +394,14 @@ impl ActiveCall { cx.spawn(|this, mut cx| async move { let result = invite.await; + if result.is_ok() { + this.update(&mut cx, |this, cx| this.report_call_event("invite", cx)); + } else { + // TODO: Resport collaboration error + } + this.update(&mut cx, |this, cx| { this.pending_invites.remove(&called_user_id); - this.report_call_event("invite", cx); cx.notify(); }); result @@ -461,13 +466,7 @@ impl ActiveCall { .borrow_mut() .take() .ok_or_else(|| anyhow!("no incoming call"))?; - Self::report_call_event_for_room( - "decline incoming", - Some(call.room_id), - None, - &self.client, - cx, - ); + report_call_event_for_room("decline incoming", call.room_id, None, &self.client, cx); self.client.send(proto::DeclineCall { room_id: call.room_id, })?; @@ -597,31 +596,46 @@ impl ActiveCall { &self.pending_invites } - fn report_call_event(&self, operation: &'static str, cx: &AppContext) { - let (room_id, channel_id) = match self.room() { - Some(room) => { - let room = room.read(cx); - (Some(room.id()), room.channel_id()) - } - None => (None, None), - }; - Self::report_call_event_for_room(operation, room_id, channel_id, &self.client, cx) - } - - pub fn report_call_event_for_room( - operation: &'static str, - room_id: Option, - channel_id: Option, - client: &Arc, - cx: &AppContext, - ) { - let telemetry = client.telemetry(); - let telemetry_settings = *settings::get::(cx); - let event = ClickhouseEvent::Call { - operation, - room_id, - channel_id, - }; - telemetry.report_clickhouse_event(event, telemetry_settings); + pub fn report_call_event(&self, operation: &'static str, cx: &AppContext) { + if let Some(room) = self.room() { + let room = room.read(cx); + report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client, cx); + } } } + +pub fn report_call_event_for_room( + operation: &'static str, + room_id: u64, + channel_id: Option, + client: &Arc, + cx: &AppContext, +) { + let telemetry = client.telemetry(); + let telemetry_settings = *settings::get::(cx); + let event = ClickhouseEvent::Call { + operation, + room_id: Some(room_id), + channel_id, + }; + telemetry.report_clickhouse_event(event, telemetry_settings); +} + +pub fn report_call_event_for_channel( + operation: &'static str, + channel_id: u64, + client: &Arc, + cx: &AppContext, +) { + let room = ActiveCall::global(cx).read(cx).room(); + + let telemetry = client.telemetry(); + let telemetry_settings = *settings::get::(cx); + + let event = ClickhouseEvent::Call { + operation, + room_id: room.map(|r| r.read(cx).id()), + channel_id: Some(channel_id), + }; + telemetry.report_clickhouse_event(event, telemetry_settings); +} diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index 22e7d6637c..1d9e409748 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -1,5 +1,5 @@ use anyhow::{anyhow, Result}; -use call::ActiveCall; +use call::report_call_event_for_channel; use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId}; use client::{ proto::{self, PeerId}, @@ -52,14 +52,9 @@ impl ChannelView { cx.spawn(|mut cx| async move { let channel_view = channel_view.await?; pane.update(&mut cx, |pane, cx| { - let room_id = ActiveCall::global(cx) - .read(cx) - .room() - .map(|room| room.read(cx).id()); - ActiveCall::report_call_event_for_room( + report_call_event_for_channel( "open channel notes", - room_id, - Some(channel_id), + channel_id, &workspace.read(cx).app_state().client, cx, ); diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index a13e7a09ee..16a9ec563b 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -136,6 +136,7 @@ actions!( StartMoveChannel, StartLinkChannel, MoveOrLinkToSelected, + InsertSpace, ] ); @@ -184,6 +185,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(CollabPanel::select_next); cx.add_action(CollabPanel::select_prev); cx.add_action(CollabPanel::confirm); + cx.add_action(CollabPanel::insert_space); cx.add_action(CollabPanel::remove); cx.add_action(CollabPanel::remove_selected_channel); cx.add_action(CollabPanel::show_inline_context_menu); @@ -2518,6 +2520,14 @@ impl CollabPanel { } } + fn insert_space(&mut self, _: &InsertSpace, cx: &mut ViewContext) { + if self.channel_editing_state.is_some() { + self.channel_name_editor.update(cx, |editor, cx| { + editor.insert(" ", cx); + }); + } + } + fn confirm_channel_edit(&mut self, cx: &mut ViewContext) -> bool { if let Some(editing_state) = &mut self.channel_editing_state { match editing_state { @@ -3054,6 +3064,19 @@ impl View for CollabPanel { .on_click(MouseButton::Left, |_, _, cx| cx.focus_self()) .into_any_named("collab panel") } + + fn update_keymap_context( + &self, + keymap: &mut gpui::keymap_matcher::KeymapContext, + _: &AppContext, + ) { + Self::reset_to_default_keymap_context(keymap); + if self.channel_editing_state.is_some() { + keymap.add_identifier("editing"); + } else { + keymap.add_identifier("not_editing"); + } + } } impl Panel for CollabPanel { diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 3dca2ec76d..84a9b3b6b6 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -10,7 +10,7 @@ mod panel_settings; mod project_shared_notification; mod sharing_status_indicator; -use call::{ActiveCall, Room}; +use call::{report_call_event_for_room, ActiveCall, Room}; use gpui::{ actions, geometry::{ @@ -55,18 +55,18 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) { let client = call.client(); let toggle_screen_sharing = room.update(cx, |room, cx| { if room.is_screen_sharing() { - ActiveCall::report_call_event_for_room( + report_call_event_for_room( "disable screen share", - Some(room.id()), + room.id(), room.channel_id(), &client, cx, ); Task::ready(room.unshare_screen(cx)) } else { - ActiveCall::report_call_event_for_room( + report_call_event_for_room( "enable screen share", - Some(room.id()), + room.id(), room.channel_id(), &client, cx, @@ -83,23 +83,13 @@ pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) { if let Some(room) = call.room().cloned() { let client = call.client(); room.update(cx, |room, cx| { - if room.is_muted(cx) { - ActiveCall::report_call_event_for_room( - "enable microphone", - Some(room.id()), - room.channel_id(), - &client, - cx, - ); + let operation = if room.is_muted(cx) { + "enable microphone" } else { - ActiveCall::report_call_event_for_room( - "disable microphone", - Some(room.id()), - room.channel_id(), - &client, - cx, - ); - } + "disable microphone" + }; + report_call_event_for_room(operation, room.id(), room.channel_id(), &client, cx); + room.toggle_mute(cx) }) .map(|task| task.detach_and_log_err(cx)) diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 4f9bb231ce..90c4481374 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -18,6 +18,15 @@ actions!(command_palette, [Toggle]); pub type CommandPalette = Picker; +pub type CommandPaletteInterceptor = + Box Option>; + +pub struct CommandInterceptResult { + pub action: Box, + pub string: String, + pub positions: Vec, +} + pub struct CommandPaletteDelegate { actions: Vec, matches: Vec, @@ -117,7 +126,7 @@ impl PickerDelegate for CommandPaletteDelegate { } }) .collect::>(); - let actions = cx.read(move |cx| { + let mut actions = cx.read(move |cx| { let hit_counts = cx.optional_global::(); actions.sort_by_key(|action| { ( @@ -136,7 +145,7 @@ impl PickerDelegate for CommandPaletteDelegate { char_bag: command.name.chars().collect(), }) .collect::>(); - let matches = if query.is_empty() { + let mut matches = if query.is_empty() { candidates .into_iter() .enumerate() @@ -158,6 +167,40 @@ impl PickerDelegate for CommandPaletteDelegate { ) .await }; + let intercept_result = cx.read(|cx| { + if cx.has_global::() { + cx.global::()(&query, cx) + } else { + None + } + }); + if let Some(CommandInterceptResult { + action, + string, + positions, + }) = intercept_result + { + if let Some(idx) = matches + .iter() + .position(|m| actions[m.candidate_id].action.id() == action.id()) + { + matches.remove(idx); + } + actions.push(Command { + name: string.clone(), + action, + keystrokes: vec![], + }); + matches.insert( + 0, + StringMatch { + candidate_id: actions.len() - 1, + string, + positions, + score: 0.0, + }, + ) + } picker .update(&mut cx, |picker, _| { let delegate = picker.delegate_mut(); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2e15dd3a92..ca19ad24cb 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -104,7 +104,7 @@ use sum_tree::TreeMap; use text::Rope; use theme::{DiagnosticStyle, Theme, ThemeSettings}; use util::{post_inc, RangeExt, ResultExt, TryFutureExt}; -use workspace::{ItemNavHistory, ViewId, Workspace}; +use workspace::{ItemNavHistory, SplitDirection, ViewId, Workspace}; use crate::git::diff_hunk_to_display; @@ -364,6 +364,7 @@ pub fn init_settings(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) { init_settings(cx); cx.add_action(Editor::new_file); + cx.add_action(Editor::new_file_in_direction); cx.add_action(Editor::cancel); cx.add_action(Editor::newline); cx.add_action(Editor::newline_above); @@ -1141,12 +1142,14 @@ struct CodeActionsMenu { impl CodeActionsMenu { fn select_first(&mut self, cx: &mut ViewContext) { self.selected_item = 0; + self.list.scroll_to(ScrollTarget::Show(self.selected_item)); cx.notify() } fn select_prev(&mut self, cx: &mut ViewContext) { if self.selected_item > 0 { self.selected_item -= 1; + self.list.scroll_to(ScrollTarget::Show(self.selected_item)); cx.notify() } } @@ -1154,12 +1157,14 @@ impl CodeActionsMenu { fn select_next(&mut self, cx: &mut ViewContext) { if self.selected_item + 1 < self.actions.len() { self.selected_item += 1; + self.list.scroll_to(ScrollTarget::Show(self.selected_item)); cx.notify() } } fn select_last(&mut self, cx: &mut ViewContext) { self.selected_item = self.actions.len() - 1; + self.list.scroll_to(ScrollTarget::Show(self.selected_item)); cx.notify() } @@ -1212,7 +1217,9 @@ impl CodeActionsMenu { workspace.update(cx, |workspace, cx| { if let Some(task) = Editor::confirm_code_action( workspace, - &Default::default(), + &ConfirmCodeAction { + item_ix: Some(item_ix), + }, cx, ) { task.detach_and_log_err(cx); @@ -1637,6 +1644,26 @@ impl Editor { } } + pub fn new_file_in_direction( + workspace: &mut Workspace, + action: &workspace::NewFileInDirection, + cx: &mut ViewContext, + ) { + let project = workspace.project().clone(); + if project.read(cx).is_remote() { + cx.propagate_action(); + } else if let Some(buffer) = project + .update(cx, |project, cx| project.create_buffer("", None, cx)) + .log_err() + { + workspace.split_item( + action.0, + Box::new(cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx))), + cx, + ); + } + } + pub fn replica_id(&self, cx: &AppContext) -> ReplicaId { self.buffer.read(cx).replica_id() } @@ -4631,7 +4658,13 @@ impl Editor { } pub fn convert_to_title_case(&mut self, _: &ConvertToTitleCase, cx: &mut ViewContext) { - self.manipulate_text(cx, |text| text.to_case(Case::Title)) + self.manipulate_text(cx, |text| { + // Hack to get around the fact that to_case crate doesn't support '\n' as a word boundary + // https://github.com/rutrum/convert-case/issues/16 + text.split("\n") + .map(|line| line.to_case(Case::Title)) + .join("\n") + }) } pub fn convert_to_snake_case(&mut self, _: &ConvertToSnakeCase, cx: &mut ViewContext) { @@ -4647,7 +4680,13 @@ impl Editor { _: &ConvertToUpperCamelCase, cx: &mut ViewContext, ) { - self.manipulate_text(cx, |text| text.to_case(Case::UpperCamel)) + self.manipulate_text(cx, |text| { + // Hack to get around the fact that to_case crate doesn't support '\n' as a word boundary + // https://github.com/rutrum/convert-case/issues/16 + text.split("\n") + .map(|line| line.to_case(Case::UpperCamel)) + .join("\n") + }) } pub fn convert_to_lower_camel_case( @@ -7135,7 +7174,7 @@ impl Editor { ); }); if split { - workspace.split_item(Box::new(editor), cx); + workspace.split_item(SplitDirection::Right, Box::new(editor), cx); } else { workspace.add_item(Box::new(editor), cx); } @@ -8566,6 +8605,29 @@ impl Editor { self.handle_input(text, cx); } + + pub fn supports_inlay_hints(&self, cx: &AppContext) -> bool { + let Some(project) = self.project.as_ref() else { + return false; + }; + let project = project.read(cx); + + let mut supports = false; + self.buffer().read(cx).for_each_buffer(|buffer| { + if !supports { + supports = project + .language_servers_for_buffer(buffer.read(cx), cx) + .any( + |(_, server)| match server.capabilities().inlay_hint_provider { + Some(lsp::OneOf::Left(enabled)) => enabled, + Some(lsp::OneOf::Right(_)) => true, + None => false, + }, + ) + } + }); + supports + } } pub trait CollaborationHub { diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 7acf0c652f..dee27e0121 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -1429,7 +1429,7 @@ async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) { assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.)); editor.scroll_screen(&ScrollAmount::Page(-0.5), cx); - assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 2.)); + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 1.)); editor.scroll_screen(&ScrollAmount::Page(0.5), cx); assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.)); }); @@ -2792,6 +2792,34 @@ async fn test_manipulate_text(cx: &mut TestAppContext) { «hello worldˇ» "}); + // Test multiple line, single selection case + // Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary + cx.set_state(indoc! {" + «The quick brown + fox jumps over + the lazy dogˇ» + "}); + cx.update_editor(|e, cx| e.convert_to_title_case(&ConvertToTitleCase, cx)); + cx.assert_editor_state(indoc! {" + «The Quick Brown + Fox Jumps Over + The Lazy Dogˇ» + "}); + + // Test multiple line, single selection case + // Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary + cx.set_state(indoc! {" + «The quick brown + fox jumps over + the lazy dogˇ» + "}); + cx.update_editor(|e, cx| e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, cx)); + cx.assert_editor_state(indoc! {" + «TheQuickBrown + FoxJumpsOver + TheLazyDogˇ» + "}); + // From here on out, test more complex cases of manipulate_text() // Test no selection case - should affect words cursors are in diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 8aa7a1e40e..dd75d2bab6 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -941,7 +941,7 @@ async fn fetch_and_update_hints( }) .await; if let Some(new_update) = new_update { - log::info!( + log::debug!( "Applying update for range {fetch_range_to_log:?}: remove from editor: {}, remove from cache: {}, add to cache: {}", new_update.remove_from_visible.len(), new_update.remove_from_cache.len(), diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 1fee309181..c87606070e 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -996,7 +996,9 @@ impl SearchableItem for Editor { }; if let Some(replacement) = query.replacement_for(&text) { - self.edit([(identifier.clone(), Arc::from(&*replacement))], cx); + self.transact(cx, |this, cx| { + this.edit([(identifier.clone(), Arc::from(&*replacement))], cx); + }); } } fn match_index_for_direction( diff --git a/crates/editor/src/scroll/scroll_amount.rs b/crates/editor/src/scroll/scroll_amount.rs index 0edab2bdfc..2cb22d1516 100644 --- a/crates/editor/src/scroll/scroll_amount.rs +++ b/crates/editor/src/scroll/scroll_amount.rs @@ -15,9 +15,13 @@ impl ScrollAmount { Self::Line(count) => *count, Self::Page(count) => editor .visible_line_count() - // subtract one to leave an anchor line - // round towards zero (so page-up and page-down are symmetric) - .map(|l| (l * count).trunc() - count.signum()) + .map(|mut l| { + // for full pages subtract one to leave an anchor line + if count.abs() == 1.0 { + l -= 1.0 + } + (l * count).trunc() + }) .unwrap_or(0.), } } diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index 0bae32f1f7..0ef54dc3d5 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -3,8 +3,8 @@ use crate::{ }; use futures::Future; use gpui::{ - keymap_matcher::Keystroke, AnyWindowHandle, AppContext, ContextHandle, ModelContext, - ViewContext, ViewHandle, + executor::Foreground, keymap_matcher::Keystroke, AnyWindowHandle, AppContext, ContextHandle, + ModelContext, ViewContext, ViewHandle, }; use indoc::indoc; use language::{Buffer, BufferSnapshot}; @@ -114,6 +114,7 @@ impl<'a> EditorTestContext<'a> { let keystroke = Keystroke::parse(keystroke_text).unwrap(); self.cx.dispatch_keystroke(self.window, keystroke, false); + keystroke_under_test_handle } @@ -126,6 +127,16 @@ impl<'a> EditorTestContext<'a> { for keystroke_text in keystroke_texts.into_iter() { self.simulate_keystroke(keystroke_text); } + // it is common for keyboard shortcuts to kick off async actions, so this ensures that they are complete + // before returning. + // NOTE: we don't do this in simulate_keystroke() because a possible cause of bugs is that typing too + // quickly races with async actions. + if let Foreground::Deterministic { cx_id: _, executor } = self.cx.foreground().as_ref() { + executor.run_until_parked(); + } else { + unreachable!(); + } + keystrokes_under_test_handle } diff --git a/crates/file_finder/Cargo.toml b/crates/file_finder/Cargo.toml index 6f6be7427b..55e4304643 100644 --- a/crates/file_finder/Cargo.toml +++ b/crates/file_finder/Cargo.toml @@ -10,6 +10,7 @@ doctest = false [dependencies] editor = { path = "../editor" } +collections = { path = "../collections" } fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } menu = { path = "../menu" } diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index c2d8cc52b2..6e587d8c98 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -1,5 +1,6 @@ +use collections::HashMap; use editor::{scroll::autoscroll::Autoscroll, Bias, Editor}; -use fuzzy::PathMatch; +use fuzzy::{CharBag, PathMatch, PathMatchCandidate}; use gpui::{ actions, elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext, WeakViewHandle, }; @@ -32,38 +33,114 @@ pub struct FileFinderDelegate { history_items: Vec, } -#[derive(Debug)] -enum Matches { - History(Vec), - Search(Vec), +#[derive(Debug, Default)] +struct Matches { + history: Vec<(FoundPath, Option)>, + search: Vec, } #[derive(Debug)] enum Match<'a> { - History(&'a FoundPath), + History(&'a FoundPath, Option<&'a PathMatch>), Search(&'a PathMatch), } impl Matches { fn len(&self) -> usize { - match self { - Self::History(items) => items.len(), - Self::Search(items) => items.len(), - } + self.history.len() + self.search.len() } fn get(&self, index: usize) -> Option> { - match self { - Self::History(items) => items.get(index).map(Match::History), - Self::Search(items) => items.get(index).map(Match::Search), + if index < self.history.len() { + self.history + .get(index) + .map(|(path, path_match)| Match::History(path, path_match.as_ref())) + } else { + self.search + .get(index - self.history.len()) + .map(Match::Search) + } + } + + fn push_new_matches( + &mut self, + history_items: &Vec, + query: &PathLikeWithPosition, + mut new_search_matches: Vec, + extend_old_matches: bool, + ) { + let matching_history_paths = matching_history_item_paths(history_items, query); + new_search_matches + .retain(|path_match| !matching_history_paths.contains_key(&path_match.path)); + let history_items_to_show = history_items + .iter() + .filter_map(|history_item| { + Some(( + history_item.clone(), + Some( + matching_history_paths + .get(&history_item.project.path)? + .clone(), + ), + )) + }) + .collect::>(); + self.history = history_items_to_show; + if extend_old_matches { + self.search + .retain(|path_match| !matching_history_paths.contains_key(&path_match.path)); + util::extend_sorted( + &mut self.search, + new_search_matches.into_iter(), + 100, + |a, b| b.cmp(a), + ) + } else { + self.search = new_search_matches; } } } -impl Default for Matches { - fn default() -> Self { - Self::History(Vec::new()) +fn matching_history_item_paths( + history_items: &Vec, + query: &PathLikeWithPosition, +) -> HashMap, PathMatch> { + let history_items_by_worktrees = history_items + .iter() + .map(|found_path| { + let path = &found_path.project.path; + let candidate = PathMatchCandidate { + path, + char_bag: CharBag::from_iter(path.to_string_lossy().to_lowercase().chars()), + }; + (found_path.project.worktree_id, candidate) + }) + .fold( + HashMap::default(), + |mut candidates, (worktree_id, new_candidate)| { + candidates + .entry(worktree_id) + .or_insert_with(Vec::new) + .push(new_candidate); + candidates + }, + ); + let mut matching_history_paths = HashMap::default(); + for (worktree, candidates) in history_items_by_worktrees { + let max_results = candidates.len() + 1; + matching_history_paths.extend( + fuzzy::match_fixed_path_set( + candidates, + worktree.to_usize(), + query.path_like.path_query(), + false, + max_results, + ) + .into_iter() + .map(|path_match| (Arc::clone(&path_match.path), path_match)), + ); } + matching_history_paths } #[derive(Debug, Clone, PartialEq, Eq)] @@ -81,66 +158,82 @@ impl FoundPath { actions!(file_finder, [Toggle]); pub fn init(cx: &mut AppContext) { - cx.add_action(toggle_file_finder); + cx.add_action(toggle_or_cycle_file_finder); FileFinder::init(cx); } const MAX_RECENT_SELECTIONS: usize = 20; -fn toggle_file_finder(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { - workspace.toggle_modal(cx, |workspace, cx| { - let project = workspace.project().read(cx); +fn toggle_or_cycle_file_finder( + workspace: &mut Workspace, + _: &Toggle, + cx: &mut ViewContext, +) { + match workspace.modal::() { + Some(file_finder) => file_finder.update(cx, |file_finder, cx| { + let current_index = file_finder.delegate().selected_index(); + file_finder.select_next(&menu::SelectNext, cx); + let new_index = file_finder.delegate().selected_index(); + if current_index == new_index { + file_finder.select_first(&menu::SelectFirst, cx); + } + }), + None => { + workspace.toggle_modal(cx, |workspace, cx| { + let project = workspace.project().read(cx); - let currently_opened_path = workspace - .active_item(cx) - .and_then(|item| item.project_path(cx)) - .map(|project_path| { - let abs_path = project - .worktree_for_id(project_path.worktree_id, cx) - .map(|worktree| worktree.read(cx).abs_path().join(&project_path.path)); - FoundPath::new(project_path, abs_path) - }); + let currently_opened_path = workspace + .active_item(cx) + .and_then(|item| item.project_path(cx)) + .map(|project_path| { + let abs_path = project + .worktree_for_id(project_path.worktree_id, cx) + .map(|worktree| worktree.read(cx).abs_path().join(&project_path.path)); + FoundPath::new(project_path, abs_path) + }); - // if exists, bubble the currently opened path to the top - let history_items = currently_opened_path - .clone() - .into_iter() - .chain( - workspace - .recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx) + // if exists, bubble the currently opened path to the top + let history_items = currently_opened_path + .clone() .into_iter() - .filter(|(history_path, _)| { - Some(history_path) - != currently_opened_path - .as_ref() - .map(|found_path| &found_path.project) - }) - .filter(|(_, history_abs_path)| { - history_abs_path.as_ref() - != currently_opened_path - .as_ref() - .and_then(|found_path| found_path.absolute.as_ref()) - }) - .map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)), - ) - .collect(); + .chain( + workspace + .recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx) + .into_iter() + .filter(|(history_path, _)| { + Some(history_path) + != currently_opened_path + .as_ref() + .map(|found_path| &found_path.project) + }) + .filter(|(_, history_abs_path)| { + history_abs_path.as_ref() + != currently_opened_path + .as_ref() + .and_then(|found_path| found_path.absolute.as_ref()) + }) + .map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)), + ) + .collect(); - let project = workspace.project().clone(); - let workspace = cx.handle().downgrade(); - let finder = cx.add_view(|cx| { - Picker::new( - FileFinderDelegate::new( - workspace, - project, - currently_opened_path, - history_items, - cx, - ), - cx, - ) - }); - finder - }); + let project = workspace.project().clone(); + let workspace = cx.handle().downgrade(); + let finder = cx.add_view(|cx| { + Picker::new( + FileFinderDelegate::new( + workspace, + project, + currently_opened_path, + history_items, + cx, + ), + cx, + ) + }); + finder + }); + } + } } pub enum Event { @@ -255,24 +348,14 @@ impl FileFinderDelegate { ) { if search_id >= self.latest_search_id { self.latest_search_id = search_id; - if self.latest_search_did_cancel + let extend_old_matches = self.latest_search_did_cancel && Some(query.path_like.path_query()) == self .latest_search_query .as_ref() - .map(|query| query.path_like.path_query()) - { - match &mut self.matches { - Matches::History(_) => self.matches = Matches::Search(matches), - Matches::Search(search_matches) => { - util::extend_sorted(search_matches, matches.into_iter(), 100, |a, b| { - b.cmp(a) - }) - } - } - } else { - self.matches = Matches::Search(matches); - } + .map(|query| query.path_like.path_query()); + self.matches + .push_new_matches(&self.history_items, &query, matches, extend_old_matches); self.latest_search_query = Some(query); self.latest_search_did_cancel = did_cancel; cx.notify(); @@ -286,7 +369,7 @@ impl FileFinderDelegate { ix: usize, ) -> (String, Vec, String, Vec) { let (file_name, file_name_positions, full_path, full_path_positions) = match path_match { - Match::History(found_path) => { + Match::History(found_path, found_path_match) => { let worktree_id = found_path.project.worktree_id; let project_relative_path = &found_path.project.path; let has_worktree = self @@ -318,14 +401,22 @@ impl FileFinderDelegate { path = Arc::from(absolute_path.as_path()); } } - self.labels_for_path_match(&PathMatch { + + let mut path_match = PathMatch { score: ix as f64, positions: Vec::new(), worktree_id: worktree_id.to_usize(), path, path_prefix: "".into(), distance_to_relative_ancestor: usize::MAX, - }) + }; + if let Some(found_path_match) = found_path_match { + path_match + .positions + .extend(found_path_match.positions.iter()) + } + + self.labels_for_path_match(&path_match) } Match::Search(path_match) => self.labels_for_path_match(path_match), }; @@ -406,8 +497,9 @@ impl PickerDelegate for FileFinderDelegate { if raw_query.is_empty() { let project = self.project.read(cx); self.latest_search_id = post_inc(&mut self.search_count); - self.matches = Matches::History( - self.history_items + self.matches = Matches { + history: self + .history_items .iter() .filter(|history_item| { project @@ -421,8 +513,10 @@ impl PickerDelegate for FileFinderDelegate { .is_some()) }) .cloned() + .map(|p| (p, None)) .collect(), - ); + search: Vec::new(), + }; cx.notify(); Task::ready(()) } else { @@ -454,7 +548,7 @@ impl PickerDelegate for FileFinderDelegate { } }; match m { - Match::History(history_match) => { + Match::History(history_match, _) => { let worktree_id = history_match.project.worktree_id; if workspace .project() @@ -866,11 +960,11 @@ mod tests { finder.update(cx, |finder, cx| { let delegate = finder.delegate_mut(); - let matches = match &delegate.matches { - Matches::Search(path_matches) => path_matches, - _ => panic!("Search matches expected"), - } - .clone(); + assert!( + delegate.matches.history.is_empty(), + "Search matches expected" + ); + let matches = delegate.matches.search.clone(); // Simulate a search being cancelled after the time limit, // returning only a subset of the matches that would have been found. @@ -893,12 +987,11 @@ mod tests { cx, ); - match &delegate.matches { - Matches::Search(new_matches) => { - assert_eq!(new_matches.as_slice(), &matches[0..4]) - } - _ => panic!("Search matches expected"), - }; + assert!( + delegate.matches.history.is_empty(), + "Search matches expected" + ); + assert_eq!(delegate.matches.search.as_slice(), &matches[0..4]); }); } @@ -1006,10 +1099,11 @@ mod tests { cx.read(|cx| { let finder = finder.read(cx); let delegate = finder.delegate(); - let matches = match &delegate.matches { - Matches::Search(path_matches) => path_matches, - _ => panic!("Search matches expected"), - }; + assert!( + delegate.matches.history.is_empty(), + "Search matches expected" + ); + let matches = delegate.matches.search.clone(); assert_eq!(matches.len(), 1); let (file_name, file_name_positions, full_path, full_path_positions) = @@ -1088,10 +1182,11 @@ mod tests { finder.read_with(cx, |f, _| { let delegate = f.delegate(); - let matches = match &delegate.matches { - Matches::Search(path_matches) => path_matches, - _ => panic!("Search matches expected"), - }; + assert!( + delegate.matches.history.is_empty(), + "Search matches expected" + ); + let matches = delegate.matches.search.clone(); assert_eq!(matches[0].path.as_ref(), Path::new("dir2/a.txt")); assert_eq!(matches[1].path.as_ref(), Path::new("dir1/a.txt")); }); @@ -1459,6 +1554,255 @@ mod tests { ); } + #[gpui::test] + async fn test_toggle_panel_new_selections( + deterministic: Arc, + cx: &mut gpui::TestAppContext, + ) { + let app_state = init_test(cx); + + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + "test": { + "first.rs": "// First Rust file", + "second.rs": "// Second Rust file", + "third.rs": "// Third Rust file", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); + + // generate some history to select from + open_close_queried_buffer( + "fir", + 1, + "first.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + open_close_queried_buffer( + "sec", + 1, + "second.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + open_close_queried_buffer( + "thi", + 1, + "third.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + let current_history = open_close_queried_buffer( + "sec", + 1, + "second.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + + for expected_selected_index in 0..current_history.len() { + cx.dispatch_action(window.into(), Toggle); + let selected_index = cx.read(|cx| { + workspace + .read(cx) + .modal::() + .unwrap() + .read(cx) + .delegate() + .selected_index() + }); + assert_eq!( + selected_index, expected_selected_index, + "Should select the next item in the history" + ); + } + + cx.dispatch_action(window.into(), Toggle); + let selected_index = cx.read(|cx| { + workspace + .read(cx) + .modal::() + .unwrap() + .read(cx) + .delegate() + .selected_index() + }); + assert_eq!( + selected_index, 0, + "Should wrap around the history and start all over" + ); + } + + #[gpui::test] + async fn test_search_preserves_history_items( + deterministic: Arc, + cx: &mut gpui::TestAppContext, + ) { + let app_state = init_test(cx); + + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + "test": { + "first.rs": "// First Rust file", + "second.rs": "// Second Rust file", + "third.rs": "// Third Rust file", + "fourth.rs": "// Fourth Rust file", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); + let worktree_id = cx.read(|cx| { + let worktrees = workspace.read(cx).worktrees(cx).collect::>(); + assert_eq!(worktrees.len(), 1,); + + WorktreeId::from_usize(worktrees[0].id()) + }); + + // generate some history to select from + open_close_queried_buffer( + "fir", + 1, + "first.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + open_close_queried_buffer( + "sec", + 1, + "second.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + open_close_queried_buffer( + "thi", + 1, + "third.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + open_close_queried_buffer( + "sec", + 1, + "second.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + + cx.dispatch_action(window.into(), Toggle); + let first_query = "f"; + let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); + finder + .update(cx, |finder, cx| { + finder + .delegate_mut() + .update_matches(first_query.to_string(), cx) + }) + .await; + finder.read_with(cx, |finder, _| { + let delegate = finder.delegate(); + assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out"); + let history_match = delegate.matches.history.first().unwrap(); + assert!(history_match.1.is_some(), "Should have path matches for history items after querying"); + assert_eq!(history_match.0, FoundPath::new( + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/first.rs")), + }, + Some(PathBuf::from("/src/test/first.rs")) + )); + assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present"); + assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs")); + }); + + let second_query = "fsdasdsa"; + let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); + finder + .update(cx, |finder, cx| { + finder + .delegate_mut() + .update_matches(second_query.to_string(), cx) + }) + .await; + finder.read_with(cx, |finder, _| { + let delegate = finder.delegate(); + assert!( + delegate.matches.history.is_empty(), + "No history entries should match {second_query}" + ); + assert!( + delegate.matches.search.is_empty(), + "No search entries should match {second_query}" + ); + }); + + let first_query_again = first_query; + let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); + finder + .update(cx, |finder, cx| { + finder + .delegate_mut() + .update_matches(first_query_again.to_string(), cx) + }) + .await; + finder.read_with(cx, |finder, _| { + let delegate = finder.delegate(); + assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query_again}, it should be present and others should be filtered out, even after non-matching query"); + let history_match = delegate.matches.history.first().unwrap(); + assert!(history_match.1.is_some(), "Should have path matches for history items after querying"); + assert_eq!(history_match.0, FoundPath::new( + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/first.rs")), + }, + Some(PathBuf::from("/src/test/first.rs")) + )); + assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query"); + assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs")); + }); + } + async fn open_close_queried_buffer( input: &str, expected_matches: usize, @@ -1528,13 +1872,8 @@ mod tests { let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); active_pane .update(cx, |pane, cx| { - pane.close_active_item( - &workspace::CloseActiveItem { - save_behavior: None, - }, - cx, - ) - .unwrap() + pane.close_active_item(&workspace::CloseActiveItem { save_intent: None }, cx) + .unwrap() }) .await .unwrap(); diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index ecaee4534e..97175cb55e 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -507,7 +507,7 @@ impl FakeFs { state.emit_event(&[path]); } - fn write_file_internal(&self, path: impl AsRef, content: String) -> Result<()> { + pub fn write_file_internal(&self, path: impl AsRef, content: String) -> Result<()> { let mut state = self.state.lock(); let path = path.as_ref(); let inode = state.next_inode; diff --git a/crates/fuzzy/src/fuzzy.rs b/crates/fuzzy/src/fuzzy.rs index 4968023644..b9595df61f 100644 --- a/crates/fuzzy/src/fuzzy.rs +++ b/crates/fuzzy/src/fuzzy.rs @@ -4,5 +4,7 @@ mod paths; mod strings; pub use char_bag::CharBag; -pub use paths::{match_path_sets, PathMatch, PathMatchCandidate, PathMatchCandidateSet}; +pub use paths::{ + match_fixed_path_set, match_path_sets, PathMatch, PathMatchCandidate, PathMatchCandidateSet, +}; pub use strings::{match_strings, StringMatch, StringMatchCandidate}; diff --git a/crates/fuzzy/src/paths.rs b/crates/fuzzy/src/paths.rs index 1cb7174fcc..4eb31936a8 100644 --- a/crates/fuzzy/src/paths.rs +++ b/crates/fuzzy/src/paths.rs @@ -90,6 +90,44 @@ impl Ord for PathMatch { } } +pub fn match_fixed_path_set( + candidates: Vec, + worktree_id: usize, + query: &str, + smart_case: bool, + max_results: usize, +) -> Vec { + let lowercase_query = query.to_lowercase().chars().collect::>(); + let query = query.chars().collect::>(); + let query_char_bag = CharBag::from(&lowercase_query[..]); + + let mut matcher = Matcher::new( + &query, + &lowercase_query, + query_char_bag, + smart_case, + max_results, + ); + + let mut results = Vec::new(); + matcher.match_candidates( + &[], + &[], + candidates.into_iter(), + &mut results, + &AtomicBool::new(false), + |candidate, score| PathMatch { + score, + worktree_id, + positions: Vec::new(), + path: candidate.path.clone(), + path_prefix: Arc::from(""), + distance_to_relative_ancestor: usize::MAX, + }, + ); + results +} + pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>( candidate_sets: &'a [Set], query: &str, diff --git a/crates/gpui/src/app/window.rs b/crates/gpui/src/app/window.rs index 626a969bd8..4eca6f3a30 100644 --- a/crates/gpui/src/app/window.rs +++ b/crates/gpui/src/app/window.rs @@ -33,6 +33,7 @@ use std::{ any::{type_name, Any, TypeId}, mem, ops::{Deref, DerefMut, Range, Sub}, + sync::Arc, }; use taffy::{ tree::{Measurable, MeasureFunc}, @@ -56,7 +57,7 @@ pub struct Window { pub(crate) rendered_views: HashMap>, scene: SceneBuilder, pub(crate) text_style_stack: Vec, - pub(crate) theme_stack: Vec>, + pub(crate) theme_stack: Vec>, pub(crate) new_parents: HashMap, pub(crate) views_to_notify_if_ancestors_change: HashMap>, titlebar_height: f32, @@ -1336,18 +1337,21 @@ impl<'a> WindowContext<'a> { self.window.text_style_stack.pop(); } - pub fn theme(&self) -> &T { + pub fn theme(&self) -> Arc { self.window .theme_stack .iter() .rev() - .find_map(|theme| theme.downcast_ref()) + .find_map(|theme| { + let entry = Arc::clone(theme); + entry.downcast::().ok() + }) .ok_or_else(|| anyhow!("no theme provided of type {}", type_name::())) .unwrap() } - pub fn push_theme(&mut self, theme: T) { - self.window.theme_stack.push(Box::new(theme)); + pub fn push_theme(&mut self, theme: T) { + self.window.theme_stack.push(Arc::new(theme)); } pub fn pop_theme(&mut self) { diff --git a/crates/gpui/src/font_cache.rs b/crates/gpui/src/font_cache.rs index 4f0d4fd461..b2dc79c87b 100644 --- a/crates/gpui/src/font_cache.rs +++ b/crates/gpui/src/font_cache.rs @@ -98,7 +98,12 @@ impl FontCache { } Err(anyhow!( - "could not find a non-empty font family matching one of the given names" + "could not find a non-empty font family matching one of the given names: {}", + names + .iter() + .map(|name| format!("`{name}`")) + .collect::>() + .join(", ") )) } diff --git a/crates/gpui2/src/style.rs b/crates/gpui2/src/style.rs index 3e4acd57e7..ecb9d79a6c 100644 --- a/crates/gpui2/src/style.rs +++ b/crates/gpui2/src/style.rs @@ -320,174 +320,114 @@ use crate as gpui2; // // Example: // // Sets the padding to 0.5rem, just like class="p-2" in Tailwind. -// fn p_2(mut self) -> Self where Self: Sized; -pub trait StyleHelpers: Styleable