diff --git a/.github/workflows/release_actions.yml b/.github/workflows/release_actions.yml index 4ccab09cbe..71909ae177 100644 --- a/.github/workflows/release_actions.yml +++ b/.github/workflows/release_actions.yml @@ -16,8 +16,4 @@ jobs: Restart your Zed or head to https://zed.dev/releases/stable/latest to grab it. - ```md - # Changelog - ${{ github.event.release.body }} - ``` diff --git a/Cargo.lock b/Cargo.lock index a9b2e29ea0..60ed830683 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -177,6 +177,28 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" +[[package]] +name = "alsa" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8512c9117059663fb5606788fbca3619e2a91dac0e3fe516242eab1fa6be5e44" +dependencies = [ + "alsa-sys", + "bitflags", + "libc", + "nix", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "ambient-authority" version = "0.0.1" @@ -590,6 +612,19 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "audio" +version = "0.1.0" +dependencies = [ + "anyhow", + "collections", + "gpui", + "log", + "parking_lot 0.11.2", + "rodio", + "util", +] + [[package]] name = "auto_update" version = "0.1.0" @@ -756,6 +791,26 @@ dependencies = [ "serde", ] +[[package]] +name = "bindgen" +version = "0.64.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 1.0.109", +] + [[package]] name = "bindgen" version = "0.65.1" @@ -857,7 +912,7 @@ checksum = "0754613691538d51f329cce9af41d7b7ca150bc973056f1156611489475f54f7" dependencies = [ "borsh-derive-internal", "borsh-schema-derive-internal", - "proc-macro-crate", + "proc-macro-crate 0.1.5", "proc-macro2", "syn 1.0.109", ] @@ -986,6 +1041,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-broadcast", + "audio", "client", "collections", "fs", @@ -1082,6 +1138,12 @@ dependencies = [ "jobserver", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cexpr" version = "0.6.0" @@ -1155,7 +1217,7 @@ dependencies = [ "bitflags", "clap_derive 3.2.25", "clap_lex 0.2.4", - "indexmap", + "indexmap 1.9.3", "once_cell", "strsim", "termcolor", @@ -1226,6 +1288,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +[[package]] +name = "claxon" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688" + [[package]] name = "cli" version = "0.1.0" @@ -1333,10 +1401,11 @@ dependencies = [ [[package]] name = "collab" -version = "0.15.0" +version = "0.16.0" dependencies = [ "anyhow", "async-tungstenite", + "audio", "axum", "axum-extra", "base64 0.13.1", @@ -1415,6 +1484,7 @@ dependencies = [ "picker", "postage", "project", + "recent_projects", "serde", "serde_derive", "settings", @@ -1444,6 +1514,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes 1.4.0", + "memchr", +] + [[package]] name = "command_palette" version = "0.1.0" @@ -1540,11 +1620,17 @@ name = "core-foundation" version = "0.9.3" source = "git+https://github.com/servo/core-foundation-rs?rev=079665882507dd5e2ff77db3de5070c1f6c0fb85#079665882507dd5e2ff77db3de5070c1f6c0fb85" dependencies = [ - "core-foundation-sys", + "core-foundation-sys 0.8.3", "libc", "uuid 0.5.1", ] +[[package]] +name = "core-foundation-sys" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b" + [[package]] name = "core-foundation-sys" version = "0.8.3" @@ -1594,6 +1680,51 @@ dependencies = [ "libc", ] +[[package]] +name = "coreaudio-rs" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb17e2d1795b1996419648915df94bc7103c28f7b48062d7acf4652fc371b2ff" +dependencies = [ + "bitflags", + "core-foundation-sys 0.6.2", + "coreaudio-sys", +] + +[[package]] +name = "coreaudio-sys" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f034b2258e6c4ade2f73bf87b21047567fb913ee9550837c2316d139b0262b24" +dependencies = [ + "bindgen 0.64.0", +] + +[[package]] +name = "cpal" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d959d90e938c5493000514b446987c07aed46c668faaa7d34d6c7a67b1a578c" +dependencies = [ + "alsa", + "core-foundation-sys 0.8.3", + "coreaudio-rs", + "dasp_sample", + "jni 0.19.0", + "js-sys", + "libc", + "mach2", + "ndk", + "ndk-context", + "oboe", + "once_cell", + "parking_lot 0.12.1", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.46.0", +] + [[package]] name = "cpp_demangle" version = "0.3.5" @@ -1924,6 +2055,12 @@ dependencies = [ "parking_lot_core 0.9.7", ] +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + [[package]] name = "data-url" version = "0.1.1" @@ -2233,6 +2370,12 @@ dependencies = [ "serde", ] +[[package]] +name = "equivalent" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" + [[package]] name = "erased-serde" version = "0.3.25" @@ -2549,6 +2692,7 @@ dependencies = [ "smol", "sum_tree", "tempfile", + "time 0.3.21", "util", ] @@ -2793,7 +2937,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" dependencies = [ "fallible-iterator", - "indexmap", + "indexmap 1.9.3", "stable_deref_trait", ] @@ -2889,7 +3033,7 @@ dependencies = [ "anyhow", "async-task", "backtrace", - "bindgen", + "bindgen 0.65.1", "block", "cc", "cocoa", @@ -2961,7 +3105,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 1.9.3", "slab", "tokio", "tokio-util 0.7.8", @@ -2995,6 +3139,12 @@ dependencies = [ "ahash 0.8.3", ] +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" + [[package]] name = "hashlink" version = "0.8.1" @@ -3105,6 +3255,12 @@ dependencies = [ "digest 0.10.6", ] +[[package]] +name = "hound" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d13cdbd5dbb29f9c88095bbdc2590c9cba0d0a1269b983fef6b2cdd7e9f4db1" + [[package]] name = "http" version = "0.2.9" @@ -3213,11 +3369,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" dependencies = [ "android_system_properties", - "core-foundation-sys", + "core-foundation-sys 0.8.3", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows", + "windows 0.48.0", ] [[package]] @@ -3287,6 +3443,16 @@ dependencies = [ "serde", ] +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", +] + [[package]] name = "indoc" version = "1.0.9" @@ -3459,6 +3625,40 @@ dependencies = [ "cc", ] +[[package]] +name = "jni" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + +[[package]] +name = "jni" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "jobserver" version = "0.1.26" @@ -3661,6 +3861,17 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" +[[package]] +name = "lewton" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030" +dependencies = [ + "byteorder", + "ogg", + "tinyvec", +] + [[package]] name = "libc" version = "0.2.144" @@ -3793,7 +4004,6 @@ dependencies = [ "gpui", "hmac 0.12.1", "jwt", - "lazy_static", "live_kit_server", "log", "media", @@ -3893,6 +4103,15 @@ dependencies = [ "libc", ] +[[package]] +name = "mach2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d0d1830bcd151a6fc4aea1369af235b36c1528fe976b8ff678683c9995eade8" +dependencies = [ + "libc", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -3949,7 +4168,7 @@ name = "media" version = "0.1.0" dependencies = [ "anyhow", - "bindgen", + "bindgen 0.65.1", "block", "bytes 1.4.0", "core-foundation", @@ -4207,6 +4426,35 @@ dependencies = [ "tempfile", ] +[[package]] +name = "ndk" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0" +dependencies = [ + "bitflags", + "jni-sys", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.4.1+23.1.7779620" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf2aae958bd232cac5069850591667ad422d263686d75b52a065f9badeee5a3" +dependencies = [ + "jni-sys", +] + [[package]] name = "net2" version = "0.2.38" @@ -4315,6 +4563,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -4367,6 +4626,27 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "nvim-rs" version = "0.5.0" @@ -4409,7 +4689,7 @@ checksum = "e42c982f2d955fac81dd7e1d0e1426a7d702acd9c98d19ab01083a6a0328c424" dependencies = [ "crc32fast", "hashbrown 0.11.2", - "indexmap", + "indexmap 1.9.3", "memchr", ] @@ -4422,6 +4702,38 @@ dependencies = [ "memchr", ] +[[package]] +name = "oboe" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8868cc237ee02e2d9618539a23a8d228b9bb3fc2e7a5b11eed3831de77c395d0" +dependencies = [ + "jni 0.20.0", + "ndk", + "ndk-context", + "num-derive", + "num-traits", + "oboe-sys", +] + +[[package]] +name = "oboe-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f44155e7fb718d3cfddcf70690b2b51ac4412f347cd9e4fbe511abe9cd7b5f2" +dependencies = [ + "cc", +] + +[[package]] +name = "ogg" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e" +dependencies = [ + "byteorder", +] + [[package]] name = "once_cell" version = "1.17.1" @@ -4711,7 +5023,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4" dependencies = [ "fixedbitset", - "indexmap", + "indexmap 1.9.3", ] [[package]] @@ -4788,7 +5100,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bd9647b268a3d3e14ff09c23201133a62589c658db02bb7388c7246aafe0590" dependencies = [ "base64 0.21.0", - "indexmap", + "indexmap 1.9.3", "line-wrap", "quick-xml", "serde", @@ -4921,6 +5233,16 @@ dependencies = [ "toml", ] +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -5033,6 +5355,7 @@ dependencies = [ "language", "menu", "postage", + "pretty_assertions", "project", "schemars", "serde", @@ -5332,6 +5655,12 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "raw-window-handle" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" + [[package]] name = "rayon" version = "1.7.0" @@ -5375,6 +5704,7 @@ version = "0.1.0" dependencies = [ "db", "editor", + "futures 0.3.28", "fuzzy", "gpui", "language", @@ -5615,6 +5945,19 @@ dependencies = [ "rmp", ] +[[package]] +name = "rodio" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdf1d4dea18dff2e9eb6dca123724f8b60ef44ad74a9ad283cdfe025df7e73fa" +dependencies = [ + "claxon", + "cpal", + "hound", + "lewton", + "symphonia", +] + [[package]] name = "rope" version = "0.1.0" @@ -6116,7 +6459,7 @@ checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" dependencies = [ "bitflags", "core-foundation", - "core-foundation-sys", + "core-foundation-sys 0.8.3", "libc", "security-framework-sys", ] @@ -6127,7 +6470,7 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4" dependencies = [ - "core-foundation-sys", + "core-foundation-sys 0.8.3", "libc", ] @@ -6201,7 +6544,7 @@ version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" dependencies = [ - "indexmap", + "indexmap 1.9.3", "itoa 1.0.6", "ryu", "serde", @@ -6213,7 +6556,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d7b9ce5b0a63c6269b9623ed828b39259545a6ec0d8a35d6135ad6af6232add" dependencies = [ - "indexmap", + "indexmap 1.9.3", "itoa 0.4.8", "ryu", "serde", @@ -6248,7 +6591,7 @@ version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" dependencies = [ - "indexmap", + "indexmap 1.9.3", "ryu", "serde", "yaml-rust", @@ -6622,7 +6965,7 @@ dependencies = [ "hex", "hkdf", "hmac 0.12.1", - "indexmap", + "indexmap 1.9.3", "itoa 1.0.6", "libc", "libsqlite3-sys", @@ -6773,6 +7116,56 @@ dependencies = [ "siphasher", ] +[[package]] +name = "symphonia" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e48dba70095f265fdb269b99619b95d04c89e619538138383e63310b14d941" +dependencies = [ + "lazy_static", + "symphonia-bundle-mp3", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-bundle-mp3" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f31d7fece546f1e6973011a9eceae948133bbd18fd3d52f6073b1e38ae6368a" +dependencies = [ + "bitflags", + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-core" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c73eb88fee79705268cc7b742c7bc93a7b76e092ab751d0833866970754142" +dependencies = [ + "arrayvec 0.7.2", + "bitflags", + "bytemuck", + "lazy_static", + "log", +] + +[[package]] +name = "symphonia-metadata" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89c3e1937e31d0e068bbe829f66b2f2bfaa28d056365279e0ef897172c3320c0" +dependencies = [ + "encoding_rs", + "lazy_static", + "log", + "symphonia-core", +] + [[package]] name = "syn" version = "1.0.109" @@ -6818,7 +7211,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a902e9050fca0a5d6877550b769abd2bd1ce8c04634b941dbe2809735e1a1e33" dependencies = [ "cfg-if 1.0.0", - "core-foundation-sys", + "core-foundation-sys 0.8.3", "libc", "ntapi 0.4.1", "once_cell", @@ -6987,7 +7380,7 @@ dependencies = [ "anyhow", "fs", "gpui", - "indexmap", + "indexmap 1.9.3", "parking_lot 0.11.2", "schemars", "serde", @@ -7293,6 +7686,23 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" + +[[package]] +name = "toml_edit" +version = "0.19.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266f016b7f039eec8a1a80dfe6156b633d208b9fccca5e4db1d6775b0c4e34a7" +dependencies = [ + "indexmap 2.0.0", + "toml_datetime", + "winnow", +] + [[package]] name = "tonic" version = "0.6.2" @@ -7332,7 +7742,7 @@ checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ "futures-core", "futures-util", - "indexmap", + "indexmap 1.9.3", "pin-project", "pin-project-lite 0.2.9", "rand 0.8.5", @@ -7987,7 +8397,6 @@ dependencies = [ "indoc", "itertools", "language", - "lazy_static", "log", "nvim-rs", "parking_lot 0.11.2", @@ -8189,7 +8598,7 @@ version = "0.85.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "570460c58b21e9150d2df0eaaedbb7816c34bcec009ae0dcc976e40ba81463e7" dependencies = [ - "indexmap", + "indexmap 1.9.3", ] [[package]] @@ -8203,7 +8612,7 @@ dependencies = [ "backtrace", "bincode", "cfg-if 1.0.0", - "indexmap", + "indexmap 1.9.3", "lazy_static", "libc", "log", @@ -8277,7 +8686,7 @@ dependencies = [ "anyhow", "cranelift-entity", "gimli 0.26.2", - "indexmap", + "indexmap 1.9.3", "log", "more-asserts", "object 0.28.4", @@ -8347,7 +8756,7 @@ dependencies = [ "backtrace", "cc", "cfg-if 1.0.0", - "indexmap", + "indexmap 1.9.3", "libc", "log", "mach", @@ -8603,6 +9012,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdacb41e6a96a052c6cb63a144f24900236121c6f63f4f8219fef5977ecb0c25" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows" version = "0.48.0" @@ -8759,6 +9177,15 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +[[package]] +name = "winnow" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca0ace3845f0d96209f0375e6d367e3eb87eb65d27d445bdc9f1843a26f39448" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.10.1" @@ -8910,7 +9337,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.94.0" +version = "0.95.0" dependencies = [ "activity_indicator", "ai", @@ -8919,6 +9346,7 @@ dependencies = [ "async-recursion 0.3.2", "async-tar", "async-trait", + "audio", "auto_update", "backtrace", "breadcrumbs", @@ -8948,7 +9376,7 @@ dependencies = [ "gpui", "ignore", "image", - "indexmap", + "indexmap 1.9.3", "install_cli", "isahc", "journal", diff --git a/Cargo.toml b/Cargo.toml index 3f3953096e..1708ccfc0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "crates/activity_indicator", "crates/ai", + "crates/audio", "crates/auto_update", "crates/breadcrumbs", "crates/call", @@ -101,6 +102,7 @@ time = { version = "0.3", features = ["serde", "serde-well-known"] } toml = { version = "0.5" } tree-sitter = "0.20" unindent = { version = "0.1.7" } +pretty_assertions = "1.3.0" [patch.crates-io] tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "49226023693107fba9a1191136a4f47f38cdca73" } diff --git a/assets/keymaps/atom.json b/assets/keymaps/atom.json index 60acf5ea6f..af845ae4f2 100644 --- a/assets/keymaps/atom.json +++ b/assets/keymaps/atom.json @@ -24,9 +24,7 @@ ], "ctrl-shift-down": "editor::AddSelectionBelow", "ctrl-shift-up": "editor::AddSelectionAbove", - "cmd-shift-backspace": "editor::DeleteToBeginningOfLine", - "cmd-shift-enter": "editor::NewlineAbove", - "cmd-enter": "editor::NewlineBelow" + "cmd-shift-backspace": "editor::DeleteToBeginningOfLine" } }, { diff --git a/assets/keymaps/sublime_text.json b/assets/keymaps/sublime_text.json index 2d32b77d58..ca20802295 100644 --- a/assets/keymaps/sublime_text.json +++ b/assets/keymaps/sublime_text.json @@ -24,9 +24,7 @@ "ctrl-.": "editor::GoToHunk", "ctrl-,": "editor::GoToPrevHunk", "ctrl-backspace": "editor::DeleteToPreviousWordStart", - "ctrl-delete": "editor::DeleteToNextWordEnd", - "cmd-shift-enter": "editor::NewlineAbove", - "cmd-enter": "editor::NewlineBelow" + "ctrl-delete": "editor::DeleteToNextWordEnd" } }, { diff --git a/assets/keymaps/textmate.json b/assets/keymaps/textmate.json index 06be727429..591d6e443f 100644 --- a/assets/keymaps/textmate.json +++ b/assets/keymaps/textmate.json @@ -12,8 +12,6 @@ "ctrl-shift-d": "editor::DuplicateLine", "cmd-b": "editor::GoToDefinition", "cmd-j": "editor::ScrollCursorCenter", - "cmd-alt-enter": "editor::NewlineAbove", - "cmd-enter": "editor::NewlineBelow", "cmd-shift-l": "editor::SelectLine", "cmd-shift-t": "outline::Toggle", "alt-backspace": "editor::DeleteToPreviousWordStart", @@ -56,7 +54,9 @@ }, { "context": "Editor && mode == full", - "bindings": {} + "bindings": { + "cmd-alt-enter": "editor::NewlineAbove" + } }, { "context": "BufferSearchBar", diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index afee6fcd2e..639daef614 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -35,8 +35,11 @@ "l": "vim::Right", "right": "vim::Right", "$": "vim::EndOfLine", + "^": "vim::FirstNonWhitespace", "shift-g": "vim::EndOfDocument", "w": "vim::NextWordStart", + "{": "vim::StartOfParagraph", + "}": "vim::EndOfParagraph", "shift-w": [ "vim::NextWordStart", { @@ -92,7 +95,10 @@ ], "ctrl-o": "pane::GoBack", "ctrl-]": "editor::GoToDefinition", - "escape": "editor::Cancel", + "escape": [ + "vim::SwitchMode", + "Normal" + ], "0": "vim::StartOfLine", // When no number operator present, use start of line motion "1": [ "vim::Number", @@ -165,7 +171,6 @@ "shift-a": "vim::InsertEndOfLine", "x": "vim::DeleteRight", "shift-x": "vim::DeleteLeft", - "^": "vim::FirstNonWhitespace", "o": "vim::InsertLineBelow", "shift-o": "vim::InsertLineAbove", "~": "vim::ChangeCase", @@ -305,6 +310,10 @@ "vim::PushOperator", "Replace" ], + "ctrl-c": [ + "vim::SwitchMode", + "Normal" + ], "> >": "editor::Indent", "< <": "editor::Outdent" } @@ -321,7 +330,10 @@ "bindings": { "tab": "vim::Tab", "enter": "vim::Enter", - "escape": "editor::Cancel" + "escape": [ + "vim::SwitchMode", + "Normal" + ] } } ] diff --git a/assets/settings/default.json b/assets/settings/default.json index c413db5788..9ae5c916b5 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -71,15 +71,17 @@ // "never" "show": "auto", // Whether to show git diff indicators in the scrollbar. - "git_diff": true + "git_diff": true, + // Whether to show selections in the scrollbar. + "selections": true }, // Inlay hint related settings "inlay_hints": { // Global switch to toggle hints on and off, switched off by default. - "enabled": false, + "enabled": false, // Toggle certain types of hints on and off, all switched on by default. "show_type_hints": true, - "show_parameter_hints": true, + "show_parameter_hints": true, // Corresponds to null/None LSP hint type value. "show_other_hints": true }, diff --git a/assets/sounds/joined_call.wav b/assets/sounds/joined_call.wav new file mode 100644 index 0000000000..cf6e5ba4df Binary files /dev/null and b/assets/sounds/joined_call.wav differ diff --git a/assets/sounds/leave_call.wav b/assets/sounds/leave_call.wav new file mode 100644 index 0000000000..478b28204f Binary files /dev/null and b/assets/sounds/leave_call.wav differ diff --git a/assets/sounds/mute.wav b/assets/sounds/mute.wav new file mode 100644 index 0000000000..69e8456f6c Binary files /dev/null and b/assets/sounds/mute.wav differ diff --git a/assets/sounds/start_screenshare.wav b/assets/sounds/start_screenshare.wav new file mode 100644 index 0000000000..7b72a90af1 Binary files /dev/null and b/assets/sounds/start_screenshare.wav differ diff --git a/assets/sounds/stop_screenshare.wav b/assets/sounds/stop_screenshare.wav new file mode 100644 index 0000000000..1fe13e21b4 Binary files /dev/null and b/assets/sounds/stop_screenshare.wav differ diff --git a/assets/sounds/unmute.wav b/assets/sounds/unmute.wav new file mode 100644 index 0000000000..f8c90f6916 Binary files /dev/null and b/assets/sounds/unmute.wav differ diff --git a/crates/ai/src/ai.rs b/crates/ai/src/ai.rs index 812fb05121..7cc5f08f7c 100644 --- a/crates/ai/src/ai.rs +++ b/crates/ai/src/ai.rs @@ -12,6 +12,7 @@ use regex::Regex; use serde::{Deserialize, Serialize}; use std::{ cmp::Reverse, + ffi::OsStr, fmt::{self, Display}, path::PathBuf, sync::Arc, @@ -80,6 +81,9 @@ impl SavedConversationMetadata { 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(); diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 6375c2fe4d..35c88486f7 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -147,8 +147,9 @@ impl AssistantPanel { .await .log_err() .unwrap_or_default(); - this.update(&mut cx, |this, _| { - this.saved_conversations = saved_conversations + this.update(&mut cx, |this, cx| { + this.saved_conversations = saved_conversations; + cx.notify(); }) .ok(); } @@ -1911,7 +1912,7 @@ impl ConversationEditor { let Some(panel) = workspace.panel::(cx) else { return; }; - let Some(editor) = workspace.active_item(cx).and_then(|item| item.downcast::()) else { + let Some(editor) = workspace.active_item(cx).and_then(|item| item.act_as::(cx)) else { return; }; diff --git a/crates/audio/Cargo.toml b/crates/audio/Cargo.toml new file mode 100644 index 0000000000..182e421eb8 --- /dev/null +++ b/crates/audio/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "audio" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/audio.rs" +doctest = false + +[dependencies] +gpui = { path = "../gpui" } +collections = { path = "../collections" } +util = { path = "../util" } + +rodio = "0.17.1" + +log.workspace = true + +anyhow.workspace = true +parking_lot.workspace = true + +[dev-dependencies] diff --git a/crates/audio/src/assets.rs b/crates/audio/src/assets.rs new file mode 100644 index 0000000000..b58e1f6aee --- /dev/null +++ b/crates/audio/src/assets.rs @@ -0,0 +1,44 @@ +use std::{io::Cursor, sync::Arc}; + +use anyhow::Result; +use collections::HashMap; +use gpui::{AppContext, AssetSource}; +use rodio::{ + source::{Buffered, SamplesConverter}, + Decoder, Source, +}; + +type Sound = Buffered>>, f32>>; + +pub struct SoundRegistry { + cache: Arc>>, + assets: Box, +} + +impl SoundRegistry { + pub fn new(source: impl AssetSource) -> Arc { + Arc::new(Self { + cache: Default::default(), + assets: Box::new(source), + }) + } + + pub fn global(cx: &AppContext) -> Arc { + cx.global::>().clone() + } + + pub fn get(&self, name: &str) -> Result> { + if let Some(wav) = self.cache.lock().get(name) { + return Ok(wav.clone()); + } + + let path = format!("sounds/{}.wav", name); + let bytes = self.assets.load(&path)?.into_owned(); + let cursor = Cursor::new(bytes); + let source = Decoder::new(cursor)?.convert_samples::().buffered(); + + self.cache.lock().insert(name.to_string(), source.clone()); + + Ok(source) + } +} diff --git a/crates/audio/src/audio.rs b/crates/audio/src/audio.rs new file mode 100644 index 0000000000..233b0f62aa --- /dev/null +++ b/crates/audio/src/audio.rs @@ -0,0 +1,67 @@ +use assets::SoundRegistry; +use gpui::{AppContext, AssetSource}; +use rodio::{OutputStream, OutputStreamHandle}; +use util::ResultExt; + +mod assets; + +pub fn init(source: impl AssetSource, cx: &mut AppContext) { + cx.set_global(SoundRegistry::new(source)); + cx.set_global(Audio::new()); +} + +pub enum Sound { + Joined, + Leave, + Mute, + Unmute, + StartScreenshare, + StopScreenshare, +} + +impl Sound { + fn file(&self) -> &'static str { + match self { + Self::Joined => "joined_call", + Self::Leave => "leave_call", + Self::Mute => "mute", + Self::Unmute => "unmute", + Self::StartScreenshare => "start_screenshare", + Self::StopScreenshare => "stop_screenshare", + } + } +} + +pub struct Audio { + _output_stream: Option, + output_handle: Option, +} + +impl Audio { + pub fn new() -> Self { + let (_output_stream, output_handle) = OutputStream::try_default().log_err().unzip(); + + Self { + _output_stream, + output_handle, + } + } + + pub fn play_sound(sound: Sound, cx: &AppContext) { + if !cx.has_global::() { + return; + } + + let this = cx.global::(); + + let Some(output_handle) = this.output_handle.as_ref() else { + return; + }; + + let Some(source) = SoundRegistry::global(cx).get(sound.file()).log_err() else { + return; + }; + + output_handle.play_raw(source).log_err(); + } +} diff --git a/crates/call/Cargo.toml b/crates/call/Cargo.toml index 4e83b552fb..61f3593247 100644 --- a/crates/call/Cargo.toml +++ b/crates/call/Cargo.toml @@ -19,6 +19,7 @@ test-support = [ ] [dependencies] +audio = { path = "../audio" } client = { path = "../client" } collections = { path = "../collections" } gpui = { path = "../gpui" } diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index da298f9ca2..87e6faf988 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -3,6 +3,7 @@ use crate::{ IncomingCall, }; use anyhow::{anyhow, Result}; +use audio::{Audio, Sound}; use client::{ proto::{self, PeerId}, Client, TypedEnvelope, User, UserStore, @@ -151,6 +152,7 @@ impl Room { let connect = room.connect(&connection_info.server_url, &connection_info.token); cx.spawn(|this, mut cx| async move { connect.await?; + this.update(&mut cx, |this, cx| this.share_microphone(cx)) .await?; @@ -176,6 +178,8 @@ impl Room { let maintain_connection = cx.spawn_weak(|this, cx| Self::maintain_connection(this, client.clone(), cx).log_err()); + Audio::play_sound(Sound::Joined, cx); + Self { id, live_kit: live_kit_room, @@ -265,6 +269,7 @@ impl Room { room.apply_room_update(room_proto, cx)?; anyhow::Ok(()) })?; + Ok(room) }) } @@ -306,6 +311,8 @@ impl Room { } } + Audio::play_sound(Sound::Leave, cx); + self.status = RoomStatus::Offline; self.remote_participants.clear(); self.pending_participants.clear(); @@ -656,6 +663,8 @@ impl Room { }, ); + Audio::play_sound(Sound::Joined, cx); + if let Some(live_kit) = this.live_kit.as_ref() { let video_tracks = live_kit.room.remote_video_tracks(&user.id.to_string()); @@ -922,6 +931,7 @@ impl Room { cx.spawn(|this, mut cx| async move { let project = Project::remote(id, client, user_store, language_registry, fs, cx.clone()).await?; + this.update(&mut cx, |this, cx| { this.joined_projects.retain(|project| { if let Some(project) = project.upgrade(cx) { @@ -1212,6 +1222,9 @@ impl Room { }; cx.notify(); } + + Audio::play_sound(Sound::StartScreenshare, cx); + Ok(()) } Err(error) => { @@ -1227,38 +1240,20 @@ impl Room { }) }) } - fn set_mute( - live_kit: &mut LiveKitRoom, - should_mute: bool, - cx: &mut ModelContext, - ) -> Result>> { - if !should_mute { - // clear user muting state. - live_kit.muted_by_user = false; - } - match &mut live_kit.microphone_track { - LocalTrack::None => Err(anyhow!("microphone was not shared")), - LocalTrack::Pending { muted, .. } => { - *muted = should_mute; - cx.notify(); - Ok(Task::Ready(Some(Ok(())))) - } - LocalTrack::Published { - track_publication, - muted, - } => { - *muted = should_mute; - cx.notify(); - Ok(cx.background().spawn(track_publication.set_mute(*muted))) - } - } - } + pub fn toggle_mute(&mut self, cx: &mut ModelContext) -> Result>> { let should_mute = !self.is_muted(); if let Some(live_kit) = self.live_kit.as_mut() { - let ret = Self::set_mute(live_kit, should_mute, cx); + let (ret_task, old_muted) = live_kit.set_mute(should_mute, cx)?; live_kit.muted_by_user = should_mute; - ret + + if old_muted == true && live_kit.deafened == true { + if let Some(task) = self.toggle_deafen(cx).ok() { + task.detach(); + } + } + + Ok(ret_task) } else { Err(anyhow!("LiveKit not started")) } @@ -1274,7 +1269,7 @@ impl Room { // When deafening, mute user's mic as well. // When undeafening, unmute user's mic unless it was manually muted prior to deafening. if live_kit.deafened || !live_kit.muted_by_user { - mute_task = Some(Self::set_mute(live_kit, live_kit.deafened, cx)?); + mute_task = Some(live_kit.set_mute(live_kit.deafened, cx)?.0); }; for participant in self.remote_participants.values() { for track in live_kit @@ -1319,6 +1314,8 @@ impl Room { } => { live_kit.room.unpublish_track(track_publication); cx.notify(); + + Audio::play_sound(Sound::StopScreenshare, cx); Ok(()) } } @@ -1347,6 +1344,51 @@ struct LiveKitRoom { _maintain_tracks: [Task<()>; 2], } +impl LiveKitRoom { + fn set_mute( + self: &mut LiveKitRoom, + should_mute: bool, + cx: &mut ModelContext, + ) -> Result<(Task>, bool)> { + if !should_mute { + // clear user muting state. + self.muted_by_user = false; + } + + let (result, old_muted) = match &mut self.microphone_track { + LocalTrack::None => Err(anyhow!("microphone was not shared")), + LocalTrack::Pending { muted, .. } => { + let old_muted = *muted; + *muted = should_mute; + cx.notify(); + Ok((Task::Ready(Some(Ok(()))), old_muted)) + } + LocalTrack::Published { + track_publication, + muted, + } => { + let old_muted = *muted; + *muted = should_mute; + cx.notify(); + Ok(( + cx.background().spawn(track_publication.set_mute(*muted)), + old_muted, + )) + } + }?; + + if old_muted != should_mute { + if should_mute { + Audio::play_sound(Sound::Mute, cx); + } else { + Audio::play_sound(Sound::Unmute, cx); + } + } + + Ok((result, old_muted)) + } +} + enum LocalTrack { None, Pending { diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index bdf677512c..2f742814a8 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -201,6 +201,7 @@ impl Bundle { self.zed_version_string() ); } + Self::LocalPath { executable, .. } => { let executable_parent = executable .parent() diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index a87234ded7..c61fdeebfb 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] default-run = "collab" edition = "2021" name = "collab" -version = "0.15.0" +version = "0.16.0" publish = false [[bin]] @@ -57,6 +57,7 @@ tracing-log = "0.1.3" tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] } [dev-dependencies] +audio = { path = "../audio" } collections = { path = "../collections", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } call = { path = "../call", features = ["test-support"] } @@ -67,7 +68,7 @@ fs = { path = "../fs", features = ["test-support"] } git = { path = "../git", features = ["test-support"] } live_kit_client = { path = "../live_kit_client", features = ["test-support"] } lsp = { path = "../lsp", features = ["test-support"] } -pretty_assertions = "1.3.0" +pretty_assertions.workspace = true project = { path = "../project", features = ["test-support"] } rpc = { path = "../rpc", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"] } diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 208da22efe..e16fa9edb1 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -3517,7 +3517,6 @@ pub use test::*; mod test { use super::*; use gpui::executor::Background; - use lazy_static::lazy_static; use parking_lot::Mutex; use sea_orm::ConnectionTrait; use sqlx::migrate::MigrateDatabase; @@ -3566,9 +3565,7 @@ mod test { } pub fn postgres(background: Arc) -> Self { - lazy_static! { - static ref LOCK: Mutex<()> = Mutex::new(()); - } + static LOCK: Mutex<()> = Mutex::new(()); let _guard = LOCK.lock(); let mut rng = StdRng::from_entropy(); diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index b51c5240a8..b1d0bedb2c 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -203,6 +203,7 @@ impl TestServer { language::init(cx); editor::init_settings(cx); workspace::init(app_state.clone(), cx); + audio::init((), cx); call::init(client.clone(), user_store.clone(), cx); }); diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index b20844a065..66dc19d690 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -18,7 +18,7 @@ use gpui::{ }; use indoc::indoc; use language::{ - language_settings::{AllLanguageSettings, Formatter, InlayHintKind, InlayHintSettings}, + language_settings::{AllLanguageSettings, Formatter, InlayHintSettings}, tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, LanguageConfig, OffsetRangeExt, Point, Rope, }; @@ -7843,7 +7843,6 @@ async fn test_mutual_editor_inlay_hint_cache_update( }); }); }); - let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]); let mut language = Language::new( LanguageConfig { @@ -7955,10 +7954,6 @@ async fn test_mutual_editor_inlay_hint_cache_update( "Host should get its first hints when opens an editor" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.allowed_hint_kinds, allowed_hint_kinds, - "Cache should use editor settings to get the allowed hint kinds" - ); assert_eq!( inlay_cache.version, edits_made, "Host editor update the cache version after every cache/view change", @@ -7982,10 +7977,6 @@ async fn test_mutual_editor_inlay_hint_cache_update( "Client should get its first hints when opens an editor" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.allowed_hint_kinds, allowed_hint_kinds, - "Cache should use editor settings to get the allowed hint kinds" - ); assert_eq!( inlay_cache.version, edits_made, "Guest editor update the cache version after every cache/view change" @@ -8007,10 +7998,6 @@ async fn test_mutual_editor_inlay_hint_cache_update( "Host should get hints from the 1st edit and 1st LSP query" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.allowed_hint_kinds, allowed_hint_kinds, - "Inlay kinds settings never change during the test" - ); assert_eq!(inlay_cache.version, edits_made); }); editor_b.update(cx_b, |editor, _| { @@ -8025,10 +8012,6 @@ async fn test_mutual_editor_inlay_hint_cache_update( "Guest should get hints the 1st edit and 2nd LSP query" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.allowed_hint_kinds, allowed_hint_kinds, - "Inlay kinds settings never change during the test" - ); assert_eq!(inlay_cache.version, edits_made); }); @@ -8054,10 +8037,6 @@ async fn test_mutual_editor_inlay_hint_cache_update( 4th query was made by guest (but not applied) due to cache invalidation logic" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.allowed_hint_kinds, allowed_hint_kinds, - "Inlay kinds settings never change during the test" - ); assert_eq!(inlay_cache.version, edits_made); }); editor_b.update(cx_b, |editor, _| { @@ -8074,10 +8053,6 @@ async fn test_mutual_editor_inlay_hint_cache_update( "Guest should get hints from 3rd edit, 6th LSP query" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.allowed_hint_kinds, allowed_hint_kinds, - "Inlay kinds settings never change during the test" - ); assert_eq!(inlay_cache.version, edits_made); }); @@ -8103,10 +8078,6 @@ async fn test_mutual_editor_inlay_hint_cache_update( "Host should react to /refresh LSP request and get new hints from 7th LSP query" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.allowed_hint_kinds, allowed_hint_kinds, - "Inlay kinds settings never change during the test" - ); assert_eq!( inlay_cache.version, edits_made, "Host should accepted all edits and bump its cache version every time" @@ -8128,10 +8099,6 @@ async fn test_mutual_editor_inlay_hint_cache_update( "Guest should get a /refresh LSP request propagated by host and get new hints from 8th LSP query" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.allowed_hint_kinds, allowed_hint_kinds, - "Inlay kinds settings never change during the test" - ); assert_eq!( inlay_cache.version, edits_made, @@ -8164,9 +8131,9 @@ async fn test_inlay_hint_refresh_is_forwarded( store.update_user_settings::(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { enabled: false, - show_type_hints: true, + show_type_hints: false, show_parameter_hints: false, - show_other_hints: true, + show_other_hints: false, }) }); }); @@ -8177,13 +8144,12 @@ async fn test_inlay_hint_refresh_is_forwarded( settings.defaults.inlay_hints = Some(InlayHintSettings { enabled: true, show_type_hints: true, - show_parameter_hints: false, + show_parameter_hints: true, show_other_hints: true, }) }); }); }); - let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]); let mut language = Language::new( LanguageConfig { @@ -8299,10 +8265,6 @@ async fn test_inlay_hint_refresh_is_forwarded( "Host should get no hints due to them turned off" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.allowed_hint_kinds, allowed_hint_kinds, - "Host should have allowed hint kinds set despite hints are off" - ); assert_eq!( inlay_cache.version, 0, "Host should not increment its cache version due to no changes", @@ -8318,10 +8280,6 @@ async fn test_inlay_hint_refresh_is_forwarded( "Client should get its first hints when opens an editor" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.allowed_hint_kinds, allowed_hint_kinds, - "Cache should use editor settings to get the allowed hint kinds" - ); assert_eq!( inlay_cache.version, edits_made, "Guest editor update the cache version after every cache/view change" @@ -8339,7 +8297,6 @@ async fn test_inlay_hint_refresh_is_forwarded( "Host should get nop hints due to them turned off, even after the /refresh" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds); assert_eq!( inlay_cache.version, 0, "Host should not increment its cache version due to no changes", @@ -8355,10 +8312,6 @@ async fn test_inlay_hint_refresh_is_forwarded( "Guest should get a /refresh LSP request propagated by host despite host hints are off" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.allowed_hint_kinds, allowed_hint_kinds, - "Inlay kinds settings never change during the test" - ); assert_eq!( inlay_cache.version, edits_made, "Guest should accepted all edits and bump its cache version every time" diff --git a/crates/collab/src/tests/randomized_integration_tests.rs b/crates/collab/src/tests/randomized_integration_tests.rs index a95938f6b8..f5dfe17d6f 100644 --- a/crates/collab/src/tests/randomized_integration_tests.rs +++ b/crates/collab/src/tests/randomized_integration_tests.rs @@ -37,9 +37,9 @@ use util::ResultExt; lazy_static::lazy_static! { static ref PLAN_LOAD_PATH: Option = path_env_var("LOAD_PLAN"); static ref PLAN_SAVE_PATH: Option = path_env_var("SAVE_PLAN"); - static ref LOADED_PLAN_JSON: Mutex>> = Default::default(); - static ref PLAN: Mutex>>> = Default::default(); } +static LOADED_PLAN_JSON: Mutex>> = Mutex::new(None); +static PLAN: Mutex>>> = Mutex::new(None); #[gpui::test(iterations = 100, on_failure = "on_failure")] async fn test_random_collaboration( diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index ee410ccba7..f81885c07a 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -35,6 +35,7 @@ gpui = { path = "../gpui" } menu = { path = "../menu" } picker = { path = "../picker" } project = { path = "../project" } +recent_projects = {path = "../recent_projects"} settings = { path = "../settings" } theme = { path = "../theme" } theme_selector = { path = "../theme_selector" } @@ -42,6 +43,7 @@ util = { path = "../util" } workspace = { path = "../workspace" } zed-actions = {path = "../zed-actions"} + anyhow.workspace = true futures.workspace = true log.workspace = true diff --git a/crates/collab_ui/src/branch_list.rs b/crates/collab_ui/src/branch_list.rs new file mode 100644 index 0000000000..16fefbd2eb --- /dev/null +++ b/crates/collab_ui/src/branch_list.rs @@ -0,0 +1,238 @@ +use anyhow::{anyhow, bail}; +use fuzzy::{StringMatch, StringMatchCandidate}; +use gpui::{elements::*, AppContext, MouseState, Task, ViewContext, ViewHandle}; +use picker::{Picker, PickerDelegate, PickerEvent}; +use std::{ops::Not, sync::Arc}; +use util::ResultExt; +use workspace::{Toast, Workspace}; + +pub fn init(cx: &mut AppContext) { + Picker::::init(cx); +} + +pub type BranchList = Picker; + +pub fn build_branch_list( + workspace: ViewHandle, + cx: &mut ViewContext, +) -> BranchList { + Picker::new( + BranchListDelegate { + matches: vec![], + workspace, + selected_index: 0, + last_query: String::default(), + }, + cx, + ) + .with_theme(|theme| theme.picker.clone()) +} + +pub struct BranchListDelegate { + matches: Vec, + workspace: ViewHandle, + selected_index: usize, + last_query: String, +} + +impl PickerDelegate for BranchListDelegate { + fn placeholder_text(&self) -> Arc { + "Select branch...".into() + } + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext>) { + self.selected_index = ix; + } + + fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()> { + cx.spawn(move |picker, mut cx| async move { + let Some(candidates) = picker + .read_with(&mut cx, |view, cx| { + let delegate = view.delegate(); + let project = delegate.workspace.read(cx).project().read(&cx); + let mut cwd = + project + .visible_worktrees(cx) + .next() + .unwrap() + .read(cx) + .abs_path() + .to_path_buf(); + cwd.push(".git"); + let Some(repo) = project.fs().open_repo(&cwd) else {bail!("Project does not have associated git repository.")}; + let mut branches = repo + .lock() + .branches()?; + const RECENT_BRANCHES_COUNT: usize = 10; + if query.is_empty() && branches.len() > RECENT_BRANCHES_COUNT { + // Truncate list of recent branches + // Do a partial sort to show recent-ish branches first. + branches.select_nth_unstable_by(RECENT_BRANCHES_COUNT - 1, |lhs, rhs| { + rhs.unix_timestamp.cmp(&lhs.unix_timestamp) + }); + branches.truncate(RECENT_BRANCHES_COUNT); + branches.sort_unstable_by(|lhs, rhs| lhs.name.cmp(&rhs.name)); + } + Ok(branches + .iter() + .cloned() + .enumerate() + .map(|(ix, command)| StringMatchCandidate { + id: ix, + char_bag: command.name.chars().collect(), + string: command.name.into(), + }) + .collect::>()) + }) + .log_err() else { return; }; + let Some(candidates) = candidates.log_err() else {return;}; + let matches = if query.is_empty() { + candidates + .into_iter() + .enumerate() + .map(|(index, candidate)| StringMatch { + candidate_id: index, + string: candidate.string, + positions: Vec::new(), + score: 0.0, + }) + .collect() + } else { + fuzzy::match_strings( + &candidates, + &query, + true, + 10000, + &Default::default(), + cx.background(), + ) + .await + }; + picker + .update(&mut cx, |picker, _| { + let delegate = picker.delegate_mut(); + delegate.matches = matches; + if delegate.matches.is_empty() { + delegate.selected_index = 0; + } else { + delegate.selected_index = + core::cmp::min(delegate.selected_index, delegate.matches.len() - 1); + } + delegate.last_query = query; + }) + .log_err(); + }) + } + + fn confirm(&mut self, cx: &mut ViewContext>) { + let current_pick = self.selected_index(); + let current_pick = self.matches[current_pick].string.clone(); + cx.spawn(|picker, mut cx| async move { + picker.update(&mut cx, |this, cx| { + let project = this.delegate().workspace.read(cx).project().read(cx); + let mut cwd = project + .visible_worktrees(cx) + .next() + .ok_or_else(|| anyhow!("There are no visisible worktrees."))? + .read(cx) + .abs_path() + .to_path_buf(); + cwd.push(".git"); + let status = project + .fs() + .open_repo(&cwd) + .ok_or_else(|| anyhow!("Could not open repository at path `{}`", cwd.as_os_str().to_string_lossy()))? + .lock() + .change_branch(¤t_pick); + if status.is_err() { + const GIT_CHECKOUT_FAILURE_ID: usize = 2048; + this.delegate().workspace.update(cx, |model, ctx| { + model.show_toast( + Toast::new( + GIT_CHECKOUT_FAILURE_ID, + format!("Failed to checkout branch '{current_pick}', check for conflicts or unstashed files"), + ), + ctx, + ) + }); + status?; + } + cx.emit(PickerEvent::Dismiss); + + Ok::<(), anyhow::Error>(()) + }).log_err(); + }).detach(); + } + + fn dismissed(&mut self, cx: &mut ViewContext>) { + cx.emit(PickerEvent::Dismiss); + } + + fn render_match( + &self, + ix: usize, + mouse_state: &mut MouseState, + selected: bool, + cx: &gpui::AppContext, + ) -> AnyElement> { + const DISPLAYED_MATCH_LEN: usize = 29; + let theme = &theme::current(cx); + let hit = &self.matches[ix]; + let shortened_branch_name = util::truncate_and_trailoff(&hit.string, DISPLAYED_MATCH_LEN); + let highlights = hit + .positions + .iter() + .copied() + .filter(|index| index < &DISPLAYED_MATCH_LEN) + .collect(); + let style = theme.picker.item.in_state(selected).style_for(mouse_state); + Flex::row() + .with_child( + Label::new(shortened_branch_name.clone(), style.label.clone()) + .with_highlights(highlights) + .contained() + .aligned() + .left(), + ) + .contained() + .with_style(style.container) + .constrained() + .with_height(theme.contact_finder.row_height) + .into_any() + } + fn render_header( + &self, + cx: &mut ViewContext>, + ) -> Option>> { + let theme = &theme::current(cx); + let style = theme.picker.header.clone(); + let label = if self.last_query.is_empty() { + Flex::row() + .with_child(Label::new("Recent branches", style.label.clone())) + .contained() + .with_style(style.container) + } else { + Flex::row() + .with_child(Label::new("Branches", style.label.clone())) + .with_children(self.matches.is_empty().not().then(|| { + let suffix = if self.matches.len() == 1 { "" } else { "es" }; + Label::new( + format!("{} match{}", self.matches.len(), suffix), + style.label, + ) + .flex_float() + })) + .contained() + .with_style(style.container) + }; + Some(label.into_any()) + } +} diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 2ab8928166..ed3315ab5a 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -1,5 +1,8 @@ use crate::{ - contact_notification::ContactNotification, contacts_popover, face_pile::FacePile, + branch_list::{build_branch_list, BranchList}, + contact_notification::ContactNotification, + contacts_popover, + face_pile::FacePile, toggle_deafen, toggle_mute, toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute, ToggleScreenSharing, }; @@ -18,19 +21,25 @@ use gpui::{ AppContext, Entity, ImageData, LayoutContext, ModelHandle, SceneBuilder, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, }; +use picker::PickerEvent; use project::{Project, RepositoryEntry}; +use recent_projects::{build_recent_projects, RecentProjects}; use std::{ops::Range, sync::Arc}; use theme::{AvatarStyle, Theme}; use util::ResultExt; -use workspace::{FollowNextCollaborator, Workspace}; +use workspace::{FollowNextCollaborator, Workspace, WORKSPACE_DB}; -// const MAX_TITLE_LENGTH: usize = 75; +const MAX_PROJECT_NAME_LENGTH: usize = 40; +const MAX_BRANCH_NAME_LENGTH: usize = 40; actions!( collab, [ ToggleContactsMenu, ToggleUserMenu, + ToggleVcsMenu, + ToggleProjectMenu, + SwitchBranch, ShareProject, UnshareProject, ] @@ -41,6 +50,8 @@ pub fn init(cx: &mut AppContext) { cx.add_action(CollabTitlebarItem::share_project); cx.add_action(CollabTitlebarItem::unshare_project); cx.add_action(CollabTitlebarItem::toggle_user_menu); + cx.add_action(CollabTitlebarItem::toggle_vcs_menu); + cx.add_action(CollabTitlebarItem::toggle_project_menu); } pub struct CollabTitlebarItem { @@ -49,6 +60,8 @@ pub struct CollabTitlebarItem { client: Arc, workspace: WeakViewHandle, contacts_popover: Option>, + branch_popover: Option>, + project_popover: Option>, user_menu: ViewHandle, _subscriptions: Vec, } @@ -69,12 +82,11 @@ impl View for CollabTitlebarItem { return Empty::new().into_any(); }; - let project = self.project.read(cx); let theme = theme::current(cx).clone(); let mut left_container = Flex::row(); let mut right_container = Flex::row().align_children_center(); - left_container.add_child(self.collect_title_root_names(&project, theme.clone(), cx)); + left_container.add_child(self.collect_title_root_names(theme.clone(), cx)); let user = self.user_store.read(cx).current_user(); let peer_id = self.client.peer_id(); @@ -182,52 +194,105 @@ impl CollabTitlebarItem { menu.set_position_mode(OverlayPositionMode::Local); menu }), + branch_popover: None, + project_popover: None, _subscriptions: subscriptions, } } fn collect_title_root_names( &self, - project: &Project, theme: Arc, - cx: &ViewContext, + cx: &mut ViewContext, ) -> AnyElement { - let mut names_and_branches = project.visible_worktrees(cx).map(|worktree| { - let worktree = worktree.read(cx); - (worktree.root_name(), worktree.root_git_entry()) - }); + let project = self.project.read(cx); - let (name, entry) = names_and_branches.next().unwrap_or(("", None)); + let (name, entry) = { + let mut names_and_branches = project.visible_worktrees(cx).map(|worktree| { + let worktree = worktree.read(cx); + (worktree.root_name(), worktree.root_git_entry()) + }); + + names_and_branches.next().unwrap_or(("", None)) + }; + + let name = util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH); let branch_prepended = entry .as_ref() .and_then(RepositoryEntry::branch) - .map(|branch| format!("/{branch}")); - let text_style = theme.titlebar.title.clone(); + .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH)); + let project_style = theme.titlebar.project_menu_button.clone(); + let git_style = theme.titlebar.git_menu_button.clone(); + let divider_style = theme.titlebar.project_name_divider.clone(); let item_spacing = theme.titlebar.item_spacing; - let mut highlight = text_style.clone(); - highlight.color = theme.titlebar.highlight_color; - - let style = LabelStyle { - text: text_style, - highlight_text: Some(highlight), - }; let mut ret = Flex::row().with_child( - Label::new(name.to_owned(), style.clone()) - .with_highlights((0..name.len()).into_iter().collect()) - .contained() - .aligned() - .left() - .into_any_named("title-project-name"), + Stack::new() + .with_child( + MouseEventHandler::::new(0, cx, |mouse_state, _| { + let style = project_style + .in_state(self.project_popover.is_some()) + .style_for(mouse_state); + Label::new(name, style.text.clone()) + .contained() + .with_style(style.container) + .aligned() + .left() + .into_any_named("title-project-name") + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_down(MouseButton::Left, move |_, this, cx| { + this.toggle_project_menu(&Default::default(), cx) + }) + .on_click(MouseButton::Left, move |_, _, _| {}), + ) + .with_children(self.render_project_popover_host(&theme.titlebar, cx)), ); if let Some(git_branch) = branch_prepended { ret = ret.with_child( - Label::new(git_branch, style) - .contained() - .with_margin_right(item_spacing) - .aligned() - .left() - .into_any_named("title-project-branch"), + Flex::row() + .with_child( + Label::new("/", divider_style.text) + .contained() + .with_style(divider_style.container) + .aligned() + .left(), + ) + .with_child( + Stack::new() + .with_child( + MouseEventHandler::::new( + 0, + cx, + |mouse_state, cx| { + enum BranchPopoverTooltip {} + let style = git_style + .in_state(self.branch_popover.is_some()) + .style_for(mouse_state); + Label::new(git_branch, style.text.clone()) + .contained() + .with_style(style.container.clone()) + .with_margin_right(item_spacing) + .aligned() + .left() + .with_tooltip::( + 0, + "Recent branches".into(), + None, + theme.tooltip.clone(), + cx, + ) + .into_any_named("title-project-branch") + }, + ) + .with_cursor_style(CursorStyle::PointingHand) + .on_down(MouseButton::Left, move |_, this, cx| { + this.toggle_vcs_menu(&Default::default(), cx) + }) + .on_click(MouseButton::Left, move |_, _, _| {}), + ) + .with_children(self.render_branches_popover_host(&theme.titlebar, cx)), + ), ) } ret.into_any() @@ -317,10 +382,138 @@ impl CollabTitlebarItem { ), ] }; - user_menu.show(Default::default(), AnchorCorner::TopRight, items, cx); + user_menu.toggle(Default::default(), AnchorCorner::TopRight, items, cx); }); } + fn render_branches_popover_host<'a>( + &'a self, + _theme: &'a theme::Titlebar, + cx: &'a mut ViewContext, + ) -> Option> { + self.branch_popover.as_ref().map(|child| { + let theme = theme::current(cx).clone(); + let child = ChildView::new(child, cx); + let child = MouseEventHandler::::new(0, cx, |_, _| { + child + .flex(1., true) + .contained() + .constrained() + .with_width(theme.contacts_popover.width) + .with_height(theme.contacts_popover.height) + }) + .on_click(MouseButton::Left, |_, _, _| {}) + .on_down_out(MouseButton::Left, move |_, this, cx| { + this.branch_popover.take(); + cx.emit(()); + cx.notify(); + }) + .contained() + .into_any(); + Overlay::new(child) + .with_fit_mode(OverlayFitMode::SwitchAnchor) + .with_anchor_corner(AnchorCorner::TopLeft) + .with_z_index(999) + .aligned() + .bottom() + .left() + .into_any() + }) + } + fn render_project_popover_host<'a>( + &'a self, + _theme: &'a theme::Titlebar, + cx: &'a mut ViewContext, + ) -> Option> { + self.project_popover.as_ref().map(|child| { + let theme = theme::current(cx).clone(); + let child = ChildView::new(child, cx); + let child = MouseEventHandler::::new(0, cx, |_, _| { + child + .flex(1., true) + .contained() + .constrained() + .with_width(theme.contacts_popover.width) + .with_height(theme.contacts_popover.height) + }) + .on_click(MouseButton::Left, |_, _, _| {}) + .on_down_out(MouseButton::Left, move |_, this, cx| { + this.project_popover.take(); + cx.emit(()); + cx.notify(); + }) + .into_any(); + + Overlay::new(child) + .with_fit_mode(OverlayFitMode::SwitchAnchor) + .with_anchor_corner(AnchorCorner::TopLeft) + .with_z_index(999) + .aligned() + .bottom() + .left() + .into_any() + }) + } + pub fn toggle_vcs_menu(&mut self, _: &ToggleVcsMenu, cx: &mut ViewContext) { + if self.branch_popover.take().is_none() { + if let Some(workspace) = self.workspace.upgrade(cx) { + let view = cx.add_view(|cx| build_branch_list(workspace, cx)); + cx.subscribe(&view, |this, _, event, cx| { + match event { + PickerEvent::Dismiss => { + this.branch_popover = None; + } + } + + cx.notify(); + }) + .detach(); + self.project_popover.take(); + cx.focus(&view); + self.branch_popover = Some(view); + } + } + + cx.notify(); + } + + pub fn toggle_project_menu(&mut self, _: &ToggleProjectMenu, cx: &mut ViewContext) { + let workspace = self.workspace.clone(); + if self.project_popover.take().is_none() { + cx.spawn(|this, mut cx| async move { + let workspaces = WORKSPACE_DB + .recent_workspaces_on_disk() + .await + .unwrap_or_default() + .into_iter() + .map(|(_, location)| location) + .collect(); + + let workspace = workspace.clone(); + this.update(&mut cx, move |this, cx| { + let view = cx.add_view(|cx| build_recent_projects(workspace, workspaces, cx)); + + cx.subscribe(&view, |this, _, event, cx| { + match event { + PickerEvent::Dismiss => { + this.project_popover = None; + } + } + + cx.notify(); + }) + .detach(); + cx.focus(&view); + this.branch_popover.take(); + this.project_popover = Some(view); + cx.notify(); + }) + .log_err(); + }) + .detach(); + } + cx.notify(); + } fn render_toggle_contacts_button( &self, theme: &Theme, @@ -683,6 +876,9 @@ impl CollabTitlebarItem { .into_any() }) .with_cursor_style(CursorStyle::PointingHand) + .on_down(MouseButton::Left, move |_, this, cx| { + this.user_menu.update(cx, |menu, _| menu.delay_cancel()); + }) .on_click(MouseButton::Left, move |_, this, cx| { this.toggle_user_menu(&Default::default(), cx) }) @@ -730,7 +926,7 @@ impl CollabTitlebarItem { self.contacts_popover.as_ref().map(|popover| { Overlay::new(ChildView::new(popover, cx)) .with_fit_mode(OverlayFitMode::SwitchAnchor) - .with_anchor_corner(AnchorCorner::TopRight) + .with_anchor_corner(AnchorCorner::TopLeft) .with_z_index(999) .aligned() .bottom() diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index a809b9c7e6..26d9c70a43 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -1,3 +1,4 @@ +mod branch_list; mod collab_titlebar_item; mod contact_finder; mod contact_list; @@ -28,6 +29,7 @@ actions!( ); pub fn init(app_state: &Arc, cx: &mut AppContext) { + branch_list::init(cx); collab_titlebar_item::init(cx); contact_list::init(cx); contact_finder::init(cx); diff --git a/crates/context_menu/src/context_menu.rs b/crates/context_menu/src/context_menu.rs index de78b51e9c..f58afab361 100644 --- a/crates/context_menu/src/context_menu.rs +++ b/crates/context_menu/src/context_menu.rs @@ -124,6 +124,7 @@ pub struct ContextMenu { items: Vec, selected_index: Option, visible: bool, + delay_cancel: bool, previously_focused_view_id: Option, parent_view_id: usize, _actions_observation: Subscription, @@ -178,6 +179,7 @@ impl ContextMenu { pub fn new(parent_view_id: usize, cx: &mut ViewContext) -> Self { Self { show_count: 0, + delay_cancel: false, anchor_position: Default::default(), anchor_corner: AnchorCorner::TopLeft, position_mode: OverlayPositionMode::Window, @@ -232,15 +234,22 @@ impl ContextMenu { } } + pub fn delay_cancel(&mut self) { + self.delay_cancel = true; + } + fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { - self.reset(cx); - let show_count = self.show_count; - cx.defer(move |this, cx| { - if cx.handle().is_focused(cx) && this.show_count == show_count { - let window_id = cx.window_id(); - (**cx).focus(window_id, this.previously_focused_view_id.take()); - } - }); + if !self.delay_cancel { + self.reset(cx); + let show_count = self.show_count; + cx.defer(move |this, cx| { + if cx.handle().is_focused(cx) && this.show_count == show_count { + (**cx).focus(this.previously_focused_view_id.take()); + } + }); + } else { + self.delay_cancel = false; + } } fn reset(&mut self, cx: &mut ViewContext) { @@ -293,6 +302,34 @@ impl ContextMenu { } } + pub fn toggle( + &mut self, + anchor_position: Vector2F, + anchor_corner: AnchorCorner, + items: Vec, + cx: &mut ViewContext, + ) { + if self.visible() { + self.cancel(&Cancel, cx); + } else { + let mut items = items.into_iter().peekable(); + if items.peek().is_some() { + self.items = items.collect(); + self.anchor_position = anchor_position; + self.anchor_corner = anchor_corner; + self.visible = true; + self.show_count += 1; + if !cx.is_self_focused() { + self.previously_focused_view_id = cx.focused_view_id(); + } + cx.focus_self(); + } else { + self.visible = false; + } + } + cx.notify(); + } + pub fn show( &mut self, anchor_position: Vector2F, diff --git a/crates/copilot_button/src/copilot_button.rs b/crates/copilot_button/src/copilot_button.rs index 9b0581492f..5576451b1b 100644 --- a/crates/copilot_button/src/copilot_button.rs +++ b/crates/copilot_button/src/copilot_button.rs @@ -102,6 +102,9 @@ impl View for CopilotButton { } }) .with_cursor_style(CursorStyle::PointingHand) + .on_down(MouseButton::Left, |_, this, cx| { + this.popup_menu.update(cx, |menu, _| menu.delay_cancel()); + }) .on_click(MouseButton::Left, { let status = status.clone(); move |_, this, cx| match status { @@ -186,7 +189,7 @@ impl CopilotButton { })); self.popup_menu.update(cx, |menu, cx| { - menu.show( + menu.toggle( Default::default(), AnchorCorner::BottomRight, menu_options, @@ -266,7 +269,7 @@ impl CopilotButton { menu_options.push(ContextMenuItem::action("Sign Out", SignOut)); self.popup_menu.update(cx, |menu, cx| { - menu.show( + menu.toggle( Default::default(), AnchorCorner::BottomRight, menu_options, diff --git a/crates/db/src/db.rs b/crates/db/src/db.rs index 5b8aca07e0..7b4aa74a80 100644 --- a/crates/db/src/db.rs +++ b/crates/db/src/db.rs @@ -41,12 +41,11 @@ const FALLBACK_DB_NAME: &'static str = "FALLBACK_MEMORY_DB"; const DB_FILE_NAME: &'static str = "db.sqlite"; lazy_static::lazy_static! { - // !!!!!!! CHANGE BACK TO DEFAULT FALSE BEFORE SHIPPING - static ref ZED_STATELESS: bool = std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty()); - static ref DB_FILE_OPERATIONS: Mutex<()> = Mutex::new(()); + pub static ref ZED_STATELESS: bool = std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty()); pub static ref BACKUP_DB_PATH: RwLock> = RwLock::new(None); pub static ref ALL_FILE_DB_FAILED: AtomicBool = AtomicBool::new(false); } +static DB_FILE_OPERATIONS: Mutex<()> = Mutex::new(()); /// Open or create a database at the given directory path. /// This will retry a couple times if there are failures. If opening fails once, the db directory diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 714dc74509..6e04833f17 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -20,7 +20,6 @@ use language::{ use std::{any::TypeId, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc}; use sum_tree::{Bias, TreeMap}; use tab_map::TabMap; -use text::Rope; use wrap_map::WrapMap; pub use block_map::{ @@ -28,7 +27,7 @@ pub use block_map::{ BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock, }; -pub use self::inlay_map::{Inlay, InlayProperties}; +pub use self::inlay_map::Inlay; #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum FoldStatus { @@ -246,10 +245,10 @@ impl DisplayMap { self.inlay_map.current_inlays() } - pub fn splice_inlays>( + pub fn splice_inlays( &mut self, to_remove: Vec, - to_insert: Vec<(InlayId, InlayProperties)>, + to_insert: Vec, cx: &mut ModelContext, ) { if to_remove.is_empty() && to_insert.is_empty() { diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index affb75f58d..6a59cecae8 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -2,9 +2,9 @@ use crate::{ multi_buffer::{MultiBufferChunks, MultiBufferRows}, Anchor, InlayId, MultiBufferSnapshot, ToOffset, }; -use collections::{BTreeMap, BTreeSet, HashMap}; +use collections::{BTreeMap, BTreeSet}; use gpui::fonts::HighlightStyle; -use language::{Chunk, Edit, Point, Rope, TextSummary}; +use language::{Chunk, Edit, Point, TextSummary}; use std::{ any::TypeId, cmp, @@ -13,13 +13,12 @@ use std::{ vec, }; use sum_tree::{Bias, Cursor, SumTree}; -use text::Patch; +use text::{Patch, Rope}; use super::TextHighlights; pub struct InlayMap { snapshot: InlaySnapshot, - inlays_by_id: HashMap, inlays: Vec, } @@ -43,10 +42,29 @@ pub struct Inlay { pub text: text::Rope, } -#[derive(Debug, Clone)] -pub struct InlayProperties { - pub position: Anchor, - pub text: T, +impl Inlay { + pub fn hint(id: usize, position: Anchor, hint: &project::InlayHint) -> Self { + let mut text = hint.text(); + if hint.padding_right && !text.ends_with(' ') { + text.push(' '); + } + if hint.padding_left && !text.starts_with(' ') { + text.insert(0, ' '); + } + Self { + id: InlayId::Hint(id), + position, + text: text.into(), + } + } + + pub fn suggestion>(id: usize, position: Anchor, text: T) -> Self { + Self { + id: InlayId::Suggestion(id), + position, + text: text.into(), + } + } } impl sum_tree::Item for Transform { @@ -368,7 +386,6 @@ impl InlayMap { ( Self { snapshot: snapshot.clone(), - inlays_by_id: HashMap::default(), inlays: Vec::new(), }, snapshot, @@ -510,45 +527,40 @@ impl InlayMap { } } - pub fn splice>( + pub fn splice( &mut self, to_remove: Vec, - to_insert: Vec<(InlayId, InlayProperties)>, + to_insert: Vec, ) -> (InlaySnapshot, Vec) { let snapshot = &mut self.snapshot; let mut edits = BTreeSet::new(); - self.inlays.retain(|inlay| !to_remove.contains(&inlay.id)); - for inlay_id in to_remove { - if let Some(inlay) = self.inlays_by_id.remove(&inlay_id) { + self.inlays.retain(|inlay| { + let retain = !to_remove.contains(&inlay.id); + if !retain { let offset = inlay.position.to_offset(&snapshot.buffer); edits.insert(offset); } - } - - for (existing_id, properties) in to_insert { - let inlay = Inlay { - id: existing_id, - position: properties.position, - text: properties.text.into(), - }; + retain + }); + for inlay_to_insert in to_insert { // Avoid inserting empty inlays. - if inlay.text.is_empty() { + if inlay_to_insert.text.is_empty() { continue; } - self.inlays_by_id.insert(inlay.id, inlay.clone()); - match self - .inlays - .binary_search_by(|probe| probe.position.cmp(&inlay.position, &snapshot.buffer)) - { + let offset = inlay_to_insert.position.to_offset(&snapshot.buffer); + match self.inlays.binary_search_by(|probe| { + probe + .position + .cmp(&inlay_to_insert.position, &snapshot.buffer) + }) { Ok(ix) | Err(ix) => { - self.inlays.insert(ix, inlay.clone()); + self.inlays.insert(ix, inlay_to_insert); } } - let offset = inlay.position.to_offset(&snapshot.buffer); edits.insert(offset); } @@ -606,15 +618,19 @@ impl InlayMap { } else { InlayId::Suggestion(post_inc(next_inlay_id)) }; - to_insert.push(( - inlay_id, - InlayProperties { - position: snapshot.buffer.anchor_at(position, bias), - text, - }, - )); + to_insert.push(Inlay { + id: inlay_id, + position: snapshot.buffer.anchor_at(position, bias), + text: text.into(), + }); } else { - to_remove.push(*self.inlays_by_id.keys().choose(rng).unwrap()); + to_remove.push( + self.inlays + .iter() + .choose(rng) + .map(|inlay| inlay.id) + .unwrap(), + ); } } log::info!("removing inlays: {:?}", to_remove); @@ -1095,6 +1111,7 @@ mod tests { use super::*; use crate::{InlayId, MultiBuffer}; use gpui::AppContext; + use project::{InlayHint, InlayHintLabel}; use rand::prelude::*; use settings::SettingsStore; use std::{cmp::Reverse, env, sync::Arc}; @@ -1102,6 +1119,89 @@ mod tests { use text::Patch; use util::post_inc; + #[test] + fn test_inlay_properties_label_padding() { + assert_eq!( + Inlay::hint( + 0, + Anchor::min(), + &InlayHint { + label: InlayHintLabel::String("a".to_string()), + buffer_id: 0, + position: text::Anchor::default(), + padding_left: false, + padding_right: false, + tooltip: None, + kind: None, + }, + ) + .text + .to_string(), + "a", + "Should not pad label if not requested" + ); + + assert_eq!( + Inlay::hint( + 0, + Anchor::min(), + &InlayHint { + label: InlayHintLabel::String("a".to_string()), + buffer_id: 0, + position: text::Anchor::default(), + padding_left: true, + padding_right: true, + tooltip: None, + kind: None, + }, + ) + .text + .to_string(), + " a ", + "Should pad label for every side requested" + ); + + assert_eq!( + Inlay::hint( + 0, + Anchor::min(), + &InlayHint { + label: InlayHintLabel::String(" a ".to_string()), + buffer_id: 0, + position: text::Anchor::default(), + padding_left: false, + padding_right: false, + tooltip: None, + kind: None, + }, + ) + .text + .to_string(), + " a ", + "Should not change already padded label" + ); + + assert_eq!( + Inlay::hint( + 0, + Anchor::min(), + &InlayHint { + label: InlayHintLabel::String(" a ".to_string()), + buffer_id: 0, + position: text::Anchor::default(), + padding_left: true, + padding_right: true, + tooltip: None, + kind: None, + }, + ) + .text + .to_string(), + " a ", + "Should not change already padded label" + ); + } + #[gpui::test] fn test_basic_inlays(cx: &mut AppContext) { let buffer = MultiBuffer::build_simple("abcdefghi", cx); @@ -1112,13 +1212,11 @@ mod tests { let (inlay_snapshot, _) = inlay_map.splice( Vec::new(), - vec![( - InlayId::Hint(post_inc(&mut next_inlay_id)), - InlayProperties { - position: buffer.read(cx).snapshot(cx).anchor_after(3), - text: "|123|", - }, - )], + vec![Inlay { + id: InlayId::Hint(post_inc(&mut next_inlay_id)), + position: buffer.read(cx).snapshot(cx).anchor_after(3), + text: "|123|".into(), + }], ); assert_eq!(inlay_snapshot.text(), "abc|123|defghi"); assert_eq!( @@ -1191,20 +1289,16 @@ mod tests { let (inlay_snapshot, _) = inlay_map.splice( Vec::new(), vec![ - ( - InlayId::Hint(post_inc(&mut next_inlay_id)), - InlayProperties { - position: buffer.read(cx).snapshot(cx).anchor_before(3), - text: "|123|", - }, - ), - ( - InlayId::Suggestion(post_inc(&mut next_inlay_id)), - InlayProperties { - position: buffer.read(cx).snapshot(cx).anchor_after(3), - text: "|456|", - }, - ), + Inlay { + id: InlayId::Hint(post_inc(&mut next_inlay_id)), + position: buffer.read(cx).snapshot(cx).anchor_before(3), + text: "|123|".into(), + }, + Inlay { + id: InlayId::Suggestion(post_inc(&mut next_inlay_id)), + position: buffer.read(cx).snapshot(cx).anchor_after(3), + text: "|456|".into(), + }, ], ); assert_eq!(inlay_snapshot.text(), "abx|123||456|yDzefghi"); @@ -1389,8 +1483,10 @@ mod tests { ); // The inlays can be manually removed. - let (inlay_snapshot, _) = inlay_map - .splice::(inlay_map.inlays_by_id.keys().copied().collect(), Vec::new()); + let (inlay_snapshot, _) = inlay_map.splice( + inlay_map.inlays.iter().map(|inlay| inlay.id).collect(), + Vec::new(), + ); assert_eq!(inlay_snapshot.text(), "abxJKLyDzefghi"); } @@ -1404,27 +1500,21 @@ mod tests { let (inlay_snapshot, _) = inlay_map.splice( Vec::new(), vec![ - ( - InlayId::Hint(post_inc(&mut next_inlay_id)), - InlayProperties { - position: buffer.read(cx).snapshot(cx).anchor_before(0), - text: "|123|\n", - }, - ), - ( - InlayId::Hint(post_inc(&mut next_inlay_id)), - InlayProperties { - position: buffer.read(cx).snapshot(cx).anchor_before(4), - text: "|456|", - }, - ), - ( - InlayId::Suggestion(post_inc(&mut next_inlay_id)), - InlayProperties { - position: buffer.read(cx).snapshot(cx).anchor_before(7), - text: "\n|567|\n", - }, - ), + Inlay { + id: InlayId::Hint(post_inc(&mut next_inlay_id)), + position: buffer.read(cx).snapshot(cx).anchor_before(0), + text: "|123|\n".into(), + }, + Inlay { + id: InlayId::Hint(post_inc(&mut next_inlay_id)), + position: buffer.read(cx).snapshot(cx).anchor_before(4), + text: "|456|".into(), + }, + Inlay { + id: InlayId::Suggestion(post_inc(&mut next_inlay_id)), + position: buffer.read(cx).snapshot(cx).anchor_before(7), + text: "\n|567|\n".into(), + }, ], ); assert_eq!(inlay_snapshot.text(), "|123|\nabc\n|456|def\n|567|\n\nghi"); @@ -1514,7 +1604,7 @@ mod tests { (offset, inlay.clone()) }) .collect::>(); - let mut expected_text = Rope::from(buffer_snapshot.text().as_str()); + let mut expected_text = Rope::from(buffer_snapshot.text()); for (offset, inlay) in inlays.into_iter().rev() { expected_text.replace(offset..offset, &inlay.text.to_string()); } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 64332c102a..8d7b8ffad6 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -26,7 +26,7 @@ use aho_corasick::AhoCorasick; use anyhow::{anyhow, Result}; use blink_manager::BlinkManager; use client::{ClickhouseEvent, TelemetrySettings}; -use clock::ReplicaId; +use clock::{Global, ReplicaId}; use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque}; use copilot::Copilot; pub use display_map::DisplayPoint; @@ -190,6 +190,15 @@ pub enum InlayId { Hint(usize), } +impl InlayId { + fn id(&self) -> usize { + match self { + Self::Suggestion(id) => *id, + Self::Hint(id) => *id, + } + } +} + actions!( editor, [ @@ -1195,11 +1204,11 @@ enum GotoDefinitionKind { Type, } -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Clone)] enum InlayRefreshReason { SettingsChange(InlayHintSettings), NewLinesShown, - ExcerptEdited, + BufferEdited(HashSet>), RefreshRequested, } @@ -2026,6 +2035,7 @@ impl Editor { } let selections = self.selections.all_adjusted(cx); + let mut brace_inserted = false; let mut edits = Vec::new(); let mut new_selections = Vec::with_capacity(selections.len()); let mut new_autoclose_regions = Vec::new(); @@ -2084,6 +2094,7 @@ impl Editor { selection.range(), format!("{}{}", text, bracket_pair.end).into(), )); + brace_inserted = true; continue; } } @@ -2110,6 +2121,7 @@ impl Editor { selection.end..selection.end, bracket_pair.end.as_str().into(), )); + brace_inserted = true; new_selections.push(( Selection { id: selection.id, @@ -2177,8 +2189,7 @@ impl Editor { let had_active_copilot_suggestion = this.has_active_copilot_suggestion(cx); this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections)); - // When buffer contents is updated and caret is moved, try triggering on type formatting. - if settings::get::(cx).use_on_type_format { + if !brace_inserted && settings::get::(cx).use_on_type_format { if let Some(on_type_format_task) = this.trigger_on_type_formatting(text.to_string(), cx) { @@ -2617,7 +2628,7 @@ impl Editor { return; } - let invalidate_cache = match reason { + let (invalidate_cache, required_languages) = match reason { InlayRefreshReason::SettingsChange(new_settings) => { match self.inlay_hint_cache.update_settings( &self.buffer, @@ -2633,16 +2644,18 @@ impl Editor { return; } ControlFlow::Break(None) => return, - ControlFlow::Continue(()) => InvalidationStrategy::RefreshRequested, + ControlFlow::Continue(()) => (InvalidationStrategy::RefreshRequested, None), } } - InlayRefreshReason::NewLinesShown => InvalidationStrategy::None, - InlayRefreshReason::ExcerptEdited => InvalidationStrategy::ExcerptEdited, - InlayRefreshReason::RefreshRequested => InvalidationStrategy::RefreshRequested, + InlayRefreshReason::NewLinesShown => (InvalidationStrategy::None, None), + InlayRefreshReason::BufferEdited(buffer_languages) => { + (InvalidationStrategy::BufferEdited, Some(buffer_languages)) + } + InlayRefreshReason::RefreshRequested => (InvalidationStrategy::RefreshRequested, None), }; self.inlay_hint_cache.refresh_inlay_hints( - self.excerpt_visible_offsets(cx), + self.excerpt_visible_offsets(required_languages.as_ref(), cx), invalidate_cache, cx, ) @@ -2661,8 +2674,9 @@ impl Editor { fn excerpt_visible_offsets( &self, + restrict_to_languages: Option<&HashSet>>, cx: &mut ViewContext<'_, '_, Editor>, - ) -> HashMap, Range)> { + ) -> HashMap, Global, Range)> { let multi_buffer = self.buffer().read(cx); let multi_buffer_snapshot = multi_buffer.snapshot(cx); let multi_buffer_visible_start = self @@ -2680,8 +2694,22 @@ impl Editor { .range_to_buffer_ranges(multi_buffer_visible_range, cx) .into_iter() .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty()) - .map(|(buffer, excerpt_visible_range, excerpt_id)| { - (excerpt_id, (buffer, excerpt_visible_range)) + .filter_map(|(buffer_handle, excerpt_visible_range, excerpt_id)| { + let buffer = buffer_handle.read(cx); + let language = buffer.language()?; + if let Some(restrict_to_languages) = restrict_to_languages { + if !restrict_to_languages.contains(language) { + return None; + } + } + Some(( + excerpt_id, + ( + buffer_handle, + buffer.version().clone(), + excerpt_visible_range, + ), + )) }) .collect() } @@ -2689,26 +2717,11 @@ impl Editor { fn splice_inlay_hints( &self, to_remove: Vec, - to_insert: Vec<(Anchor, InlayId, project::InlayHint)>, + to_insert: Vec, cx: &mut ViewContext, ) { - let buffer = self.buffer.read(cx).read(cx); - let new_inlays = to_insert - .into_iter() - .map(|(position, id, hint)| { - let mut text = hint.text(); - if hint.padding_right { - text.push(' '); - } - if hint.padding_left { - text.insert(0, ' '); - } - (id, InlayProperties { position, text }) - }) - .collect(); - drop(buffer); self.display_map.update(cx, |display_map, cx| { - display_map.splice_inlays(to_remove, new_inlays, cx); + display_map.splice_inlays(to_remove, to_insert, cx); }); } @@ -3393,7 +3406,7 @@ impl Editor { } self.display_map.update(cx, |map, cx| { - map.splice_inlays::<&str>(vec![suggestion.id], Vec::new(), cx) + map.splice_inlays(vec![suggestion.id], Vec::new(), cx) }); cx.notify(); true @@ -3426,7 +3439,7 @@ impl Editor { fn take_active_copilot_suggestion(&mut self, cx: &mut ViewContext) -> Option { let suggestion = self.copilot_state.suggestion.take()?; self.display_map.update(cx, |map, cx| { - map.splice_inlays::<&str>(vec![suggestion.id], Default::default(), cx); + map.splice_inlays(vec![suggestion.id], Default::default(), cx); }); let buffer = self.buffer.read(cx).read(cx); @@ -3457,21 +3470,11 @@ impl Editor { to_remove.push(suggestion.id); } - let suggestion_inlay_id = InlayId::Suggestion(post_inc(&mut self.next_inlay_id)); - let to_insert = vec![( - suggestion_inlay_id, - InlayProperties { - position: cursor, - text: text.clone(), - }, - )]; + let suggestion_inlay = + Inlay::suggestion(post_inc(&mut self.next_inlay_id), cursor, text); + self.copilot_state.suggestion = Some(suggestion_inlay.clone()); self.display_map.update(cx, move |map, cx| { - map.splice_inlays(to_remove, to_insert, cx) - }); - self.copilot_state.suggestion = Some(Inlay { - id: suggestion_inlay_id, - position: cursor, - text, + map.splice_inlays(to_remove, vec![suggestion_inlay], cx) }); cx.notify(); } else { @@ -5120,7 +5123,7 @@ impl Editor { self.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_with(|map, selection| { selection.collapse_to( - movement::start_of_paragraph(map, selection.head()), + movement::start_of_paragraph(map, selection.head(), 1), SelectionGoal::None, ) }); @@ -5140,7 +5143,7 @@ impl Editor { self.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_with(|map, selection| { selection.collapse_to( - movement::end_of_paragraph(map, selection.head()), + movement::end_of_paragraph(map, selection.head(), 1), SelectionGoal::None, ) }); @@ -5159,7 +5162,10 @@ impl Editor { self.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_heads_with(|map, head, _| { - (movement::start_of_paragraph(map, head), SelectionGoal::None) + ( + movement::start_of_paragraph(map, head, 1), + SelectionGoal::None, + ) }); }) } @@ -5176,7 +5182,10 @@ impl Editor { self.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_heads_with(|map, head, _| { - (movement::end_of_paragraph(map, head), SelectionGoal::None) + ( + movement::end_of_paragraph(map, head, 1), + SelectionGoal::None, + ) }); }) } @@ -7256,7 +7265,7 @@ impl Editor { fn on_buffer_event( &mut self, - _: ModelHandle, + multibuffer: ModelHandle, event: &multi_buffer::Event, cx: &mut ViewContext, ) { @@ -7268,7 +7277,33 @@ impl Editor { self.update_visible_copilot_suggestion(cx); } cx.emit(Event::BufferEdited); - self.refresh_inlays(InlayRefreshReason::ExcerptEdited, cx); + + if let Some(project) = &self.project { + let project = project.read(cx); + let languages_affected = multibuffer + .read(cx) + .all_buffers() + .into_iter() + .filter_map(|buffer| { + let buffer = buffer.read(cx); + let language = buffer.language()?; + if project.is_local() + && project.language_servers_for_buffer(buffer, cx).count() == 0 + { + None + } else { + Some(language) + } + }) + .cloned() + .collect::>(); + if !languages_affected.is_empty() { + self.refresh_inlays( + InlayRefreshReason::BufferEdited(languages_affected), + cx, + ); + } + } } multi_buffer::Event::ExcerptsAdded { buffer, diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 387d4d2c34..f4499b5651 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -15,6 +15,7 @@ pub struct EditorSettings { pub struct Scrollbar { pub show: ShowScrollbar, pub git_diff: bool, + pub selections: bool, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] @@ -39,6 +40,7 @@ pub struct EditorSettingsContent { pub struct ScrollbarContent { pub show: Option, pub git_diff: Option, + pub selections: Option, } impl Setting for EditorSettings { diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 657a7a744e..9e726d6cc4 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -6979,6 +6979,111 @@ async fn test_copilot_disabled_globs( assert!(copilot_requests.try_next().is_ok()); } +#[gpui::test] +async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + brackets: BracketPairConfig { + pairs: vec![BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: true, + newline: true, + }], + disabled_scopes_by_bracket_ix: Vec::new(), + }, + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { + first_trigger_character: "{".to_string(), + more_trigger_character: None, + }), + ..Default::default() + }, + ..Default::default() + })) + .await; + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/a", + json!({ + "main.rs": "fn main() { let a = 5; }", + "other.rs": "// Test file", + }), + ) + .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; + project.update(cx, |project, _| project.languages().add(Arc::new(language))); + let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let worktree_id = workspace.update(cx, |workspace, cx| { + workspace.project().read_with(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }) + }); + + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/a/main.rs", cx) + }) + .await + .unwrap(); + cx.foreground().run_until_parked(); + cx.foreground().start_waiting(); + let fake_server = fake_servers.next().await.unwrap(); + let editor_handle = workspace + .update(cx, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + fake_server.handle_request::(|params, _| async move { + assert_eq!( + params.text_document_position.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + assert_eq!( + params.text_document_position.position, + lsp::Position::new(0, 21), + ); + + Ok(Some(vec![lsp::TextEdit { + new_text: "]".to_string(), + range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)), + }])) + }); + + editor_handle.update(cx, |editor, cx| { + cx.focus(&editor_handle); + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(0, 21)..Point::new(0, 20)]) + }); + editor.handle_input("{", cx); + }); + + cx.foreground().run_until_parked(); + + buffer.read_with(cx, |buffer, _| { + assert_eq!( + buffer.text(), + "fn main() { let a = {5}; }", + "No extra braces from on type formatting should appear in the buffer" + ) + }); +} + fn empty_range(row: usize, column: usize) -> Range { let point = DisplayPoint::new(row as u32, column as u32); point..point diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index d1e6f29bbe..e96f1efe92 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1008,6 +1008,7 @@ impl EditorElement { bounds: RectF, layout: &mut LayoutState, cx: &mut ViewContext, + editor: &Editor, ) { enum ScrollbarMouseHandlers {} if layout.mode != EditorMode::Full { @@ -1050,9 +1051,74 @@ impl EditorElement { background: style.track.background_color, ..Default::default() }); + let scrollbar_settings = settings::get::(cx).scrollbar; + let theme = theme::current(cx); + let scrollbar_theme = &theme.editor.scrollbar; + if layout.is_singleton && scrollbar_settings.selections { + let start_anchor = Anchor::min(); + let end_anchor = Anchor::max(); + let mut start_row = None; + let mut end_row = None; + let color = scrollbar_theme.selections; + let border = Border { + width: 1., + color: style.thumb.border.color, + overlay: false, + top: false, + right: true, + bottom: false, + left: true, + }; + let mut push_region = |start, end| { + if let (Some(start_display), Some(end_display)) = (start, end) { + let start_y = y_for_row(start_display as f32); + let mut end_y = y_for_row(end_display as f32); + if end_y - start_y < 1. { + end_y = start_y + 1.; + } + let bounds = RectF::from_points(vec2f(left, start_y), vec2f(right, end_y)); - if layout.is_singleton && settings::get::(cx).scrollbar.git_diff { - let diff_style = theme::current(cx).editor.scrollbar.git.clone(); + scene.push_quad(Quad { + bounds, + background: Some(color), + border, + corner_radius: style.thumb.corner_radius, + }) + } + }; + for (row, _) in &editor.background_highlights_in_range( + start_anchor..end_anchor, + &layout.position_map.snapshot, + &theme, + ) { + let start_display = row.start; + let end_display = row.end; + + if start_row.is_none() { + assert_eq!(end_row, None); + start_row = Some(start_display.row()); + end_row = Some(end_display.row()); + continue; + } + if let Some(current_end) = end_row.as_mut() { + if start_display.row() > *current_end + 1 { + push_region(start_row, end_row); + start_row = Some(start_display.row()); + end_row = Some(end_display.row()); + } else { + // Merge two hunks. + *current_end = end_display.row(); + } + } else { + unreachable!(); + } + } + // We might still have a hunk that was not rendered (if there was a search hit on the last line) + push_region(start_row, end_row); + } + + if layout.is_singleton && scrollbar_settings.git_diff { + let diff_style = scrollbar_theme.git.clone(); for hunk in layout .position_map .snapshot @@ -2368,7 +2434,7 @@ impl Element for EditorElement { if !layout.blocks.is_empty() { self.paint_blocks(scene, bounds, visible_bounds, layout, editor, cx); } - self.paint_scrollbar(scene, bounds, layout, cx); + self.paint_scrollbar(scene, bounds, layout, cx, &editor); scene.pop_layer(); scene.pop_layer(); diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index af7bf3e4c5..70fb372504 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -38,14 +38,14 @@ pub struct CachedExcerptHints { #[derive(Debug, Clone, Copy)] pub enum InvalidationStrategy { RefreshRequested, - ExcerptEdited, + BufferEdited, None, } #[derive(Debug, Default)] pub struct InlaySplice { pub to_remove: Vec, - pub to_insert: Vec<(Anchor, InlayId, InlayHint)>, + pub to_insert: Vec, } struct UpdateTask { @@ -94,7 +94,7 @@ impl InvalidationStrategy { fn should_invalidate(&self) -> bool { matches!( self, - InvalidationStrategy::RefreshRequested | InvalidationStrategy::ExcerptEdited + InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited ) } } @@ -197,7 +197,7 @@ impl InlayHintCache { pub fn refresh_inlay_hints( &mut self, - mut excerpts_to_query: HashMap, Range)>, + mut excerpts_to_query: HashMap, Global, Range)>, invalidate: InvalidationStrategy, cx: &mut ViewContext, ) { @@ -285,13 +285,13 @@ impl InlayHintCache { if !old_kinds.contains(&cached_hint.kind) && new_kinds.contains(&cached_hint.kind) { - to_insert.push(( + to_insert.push(Inlay::hint( + cached_hint_id.id(), multi_buffer_snapshot.anchor_in_excerpt( *excerpt_id, cached_hint.position, ), - *cached_hint_id, - cached_hint.clone(), + &cached_hint, )); } excerpt_cache.next(); @@ -307,11 +307,11 @@ impl InlayHintCache { for (cached_hint_id, maybe_missed_cached_hint) in excerpt_cache { let cached_hint_kind = maybe_missed_cached_hint.kind; if !old_kinds.contains(&cached_hint_kind) && new_kinds.contains(&cached_hint_kind) { - to_insert.push(( + to_insert.push(Inlay::hint( + cached_hint_id.id(), multi_buffer_snapshot .anchor_in_excerpt(*excerpt_id, maybe_missed_cached_hint.position), - *cached_hint_id, - maybe_missed_cached_hint.clone(), + &maybe_missed_cached_hint, )); } } @@ -342,104 +342,113 @@ impl InlayHintCache { fn spawn_new_update_tasks( editor: &mut Editor, - excerpts_to_query: HashMap, Range)>, + excerpts_to_query: HashMap, Global, Range)>, invalidate: InvalidationStrategy, update_cache_version: usize, cx: &mut ViewContext<'_, '_, Editor>, ) { let visible_hints = Arc::new(editor.visible_inlay_hints(cx)); - for (excerpt_id, (buffer_handle, excerpt_visible_range)) in excerpts_to_query { - if !excerpt_visible_range.is_empty() { - let buffer = buffer_handle.read(cx); - let buffer_snapshot = buffer.snapshot(); - let cached_excerpt_hints = editor.inlay_hint_cache.hints.get(&excerpt_id).cloned(); - if let Some(cached_excerpt_hints) = &cached_excerpt_hints { - let new_task_buffer_version = buffer_snapshot.version(); - let cached_excerpt_hints = cached_excerpt_hints.read(); - let cached_buffer_version = &cached_excerpt_hints.buffer_version; - if cached_excerpt_hints.version > update_cache_version - || cached_buffer_version.changed_since(new_task_buffer_version) - { - return; - } - if !new_task_buffer_version.changed_since(&cached_buffer_version) - && !matches!(invalidate, InvalidationStrategy::RefreshRequested) - { - return; - } + for (excerpt_id, (buffer_handle, new_task_buffer_version, excerpt_visible_range)) in + excerpts_to_query + { + if excerpt_visible_range.is_empty() { + continue; + } + let buffer = buffer_handle.read(cx); + let buffer_snapshot = buffer.snapshot(); + if buffer_snapshot + .version() + .changed_since(&new_task_buffer_version) + { + continue; + } + + let cached_excerpt_hints = editor.inlay_hint_cache.hints.get(&excerpt_id).cloned(); + if let Some(cached_excerpt_hints) = &cached_excerpt_hints { + let cached_excerpt_hints = cached_excerpt_hints.read(); + let cached_buffer_version = &cached_excerpt_hints.buffer_version; + if cached_excerpt_hints.version > update_cache_version + || cached_buffer_version.changed_since(&new_task_buffer_version) + { + continue; + } + if !new_task_buffer_version.changed_since(&cached_buffer_version) + && !matches!(invalidate, InvalidationStrategy::RefreshRequested) + { + continue; + } + }; + + let buffer_id = buffer.remote_id(); + let excerpt_visible_range_start = buffer.anchor_before(excerpt_visible_range.start); + let excerpt_visible_range_end = buffer.anchor_after(excerpt_visible_range.end); + + let (multi_buffer_snapshot, full_excerpt_range) = + editor.buffer.update(cx, |multi_buffer, cx| { + let multi_buffer_snapshot = multi_buffer.snapshot(cx); + ( + multi_buffer_snapshot, + multi_buffer + .excerpts_for_buffer(&buffer_handle, cx) + .into_iter() + .find(|(id, _)| id == &excerpt_id) + .map(|(_, range)| range.context), + ) + }); + + if let Some(full_excerpt_range) = full_excerpt_range { + let query = ExcerptQuery { + buffer_id, + excerpt_id, + dimensions: ExcerptDimensions { + excerpt_range_start: full_excerpt_range.start, + excerpt_range_end: full_excerpt_range.end, + excerpt_visible_range_start, + excerpt_visible_range_end, + }, + cache_version: update_cache_version, + invalidate, }; - let buffer_id = buffer.remote_id(); - let excerpt_visible_range_start = buffer.anchor_before(excerpt_visible_range.start); - let excerpt_visible_range_end = buffer.anchor_after(excerpt_visible_range.end); - - let (multi_buffer_snapshot, full_excerpt_range) = - editor.buffer.update(cx, |multi_buffer, cx| { - let multi_buffer_snapshot = multi_buffer.snapshot(cx); - ( - multi_buffer_snapshot, - multi_buffer - .excerpts_for_buffer(&buffer_handle, cx) - .into_iter() - .find(|(id, _)| id == &excerpt_id) - .map(|(_, range)| range.context), - ) - }); - - if let Some(full_excerpt_range) = full_excerpt_range { - let query = ExcerptQuery { - buffer_id, - excerpt_id, - dimensions: ExcerptDimensions { - excerpt_range_start: full_excerpt_range.start, - excerpt_range_end: full_excerpt_range.end, - excerpt_visible_range_start, - excerpt_visible_range_end, - }, - cache_version: update_cache_version, - invalidate, - }; - - let new_update_task = |is_refresh_after_regular_task| { - new_update_task( - query, - multi_buffer_snapshot, - buffer_snapshot, - Arc::clone(&visible_hints), - cached_excerpt_hints, - is_refresh_after_regular_task, - cx, - ) - }; - match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) { - hash_map::Entry::Occupied(mut o) => { - let update_task = o.get_mut(); - match (update_task.invalidate, invalidate) { - (_, InvalidationStrategy::None) => {} - ( - InvalidationStrategy::ExcerptEdited, - InvalidationStrategy::RefreshRequested, - ) if !update_task.task.is_running_rx.is_closed() => { - update_task.pending_refresh = Some(query); - } - _ => { - o.insert(UpdateTask { - invalidate, - cache_version: query.cache_version, - task: new_update_task(false), - pending_refresh: None, - }); - } + let new_update_task = |is_refresh_after_regular_task| { + new_update_task( + query, + multi_buffer_snapshot, + buffer_snapshot, + Arc::clone(&visible_hints), + cached_excerpt_hints, + is_refresh_after_regular_task, + cx, + ) + }; + match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) { + hash_map::Entry::Occupied(mut o) => { + let update_task = o.get_mut(); + match (update_task.invalidate, invalidate) { + (_, InvalidationStrategy::None) => {} + ( + InvalidationStrategy::BufferEdited, + InvalidationStrategy::RefreshRequested, + ) if !update_task.task.is_running_rx.is_closed() => { + update_task.pending_refresh = Some(query); + } + _ => { + o.insert(UpdateTask { + invalidate, + cache_version: query.cache_version, + task: new_update_task(false), + pending_refresh: None, + }); } } - hash_map::Entry::Vacant(v) => { - v.insert(UpdateTask { - invalidate, - cache_version: query.cache_version, - task: new_update_task(false), - pending_refresh: None, - }); - } + } + hash_map::Entry::Vacant(v) => { + v.insert(UpdateTask { + invalidate, + cache_version: query.cache_version, + task: new_update_task(false), + pending_refresh: None, + }); } } } @@ -648,18 +657,22 @@ async fn fetch_and_update_hints( for new_hint in new_update.add_to_cache { let new_hint_position = multi_buffer_snapshot .anchor_in_excerpt(query.excerpt_id, new_hint.position); - let new_inlay_id = InlayId::Hint(post_inc(&mut editor.next_inlay_id)); + let new_inlay_id = post_inc(&mut editor.next_inlay_id); if editor .inlay_hint_cache .allowed_hint_kinds .contains(&new_hint.kind) { - splice - .to_insert - .push((new_hint_position, new_inlay_id, new_hint.clone())); + splice.to_insert.push(Inlay::hint( + new_inlay_id, + new_hint_position, + &new_hint, + )); } - cached_excerpt_hints.hints.push((new_inlay_id, new_hint)); + cached_excerpt_hints + .hints + .push((InlayId::Hint(new_inlay_id), new_hint)); } cached_excerpt_hints @@ -820,7 +833,7 @@ mod tests { use crate::{ scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount}, serde_json::json, - ExcerptRange, InlayHintSettings, + ExcerptRange, }; use futures::StreamExt; use gpui::{executor::Deterministic, TestAppContext, ViewHandle}; @@ -961,6 +974,348 @@ mod tests { }); } + #[gpui::test] + async fn test_cache_update_on_lsp_completion_tasks(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; + let lsp_request_count = Arc::new(AtomicU32::new(0)); + fake_server + .handle_request::(move |params, _| { + let task_lsp_request_count = Arc::clone(&lsp_request_count); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path(file_with_hints).unwrap(), + ); + let current_call_id = + Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, current_call_id), + label: lsp::InlayHintLabel::String(current_call_id.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }) + .next() + .await; + cx.foreground().run_until_parked(); + + let mut edits_made = 1; + editor.update(cx, |editor, cx| { + let expected_layers = vec!["0".to_string()]; + assert_eq!( + expected_layers, + cached_hint_labels(editor), + "Should get its first hints when opening the editor" + ); + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.version, edits_made, + "The editor update the cache version after every cache/view change" + ); + }); + + let progress_token = "test_progress_token"; + fake_server + .request::(lsp::WorkDoneProgressCreateParams { + token: lsp::ProgressToken::String(progress_token.to_string()), + }) + .await + .expect("work done progress create request failed"); + cx.foreground().run_until_parked(); + fake_server.notify::(lsp::ProgressParams { + token: lsp::ProgressToken::String(progress_token.to_string()), + value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin( + lsp::WorkDoneProgressBegin::default(), + )), + }); + cx.foreground().run_until_parked(); + + editor.update(cx, |editor, cx| { + let expected_layers = vec!["0".to_string()]; + assert_eq!( + expected_layers, + cached_hint_labels(editor), + "Should not update hints while the work task is running" + ); + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.version, edits_made, + "Should not update the cache while the work task is running" + ); + }); + + fake_server.notify::(lsp::ProgressParams { + token: lsp::ProgressToken::String(progress_token.to_string()), + value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End( + lsp::WorkDoneProgressEnd::default(), + )), + }); + cx.foreground().run_until_parked(); + + edits_made += 1; + editor.update(cx, |editor, cx| { + let expected_layers = vec!["1".to_string()]; + assert_eq!( + expected_layers, + cached_hint_labels(editor), + "New hints should be queried after the work task is done" + ); + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.version, edits_made, + "Cache version should udpate once after the work task is done" + ); + }); + } + + #[gpui::test] + async fn test_no_hint_updates_for_unrelated_language_files(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/a", + json!({ + "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out", + "other.md": "Test md file with some text", + }), + ) + .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; + let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let worktree_id = workspace.update(cx, |workspace, cx| { + workspace.project().read_with(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }) + }); + + let mut rs_fake_servers = None; + let mut md_fake_servers = None; + for (name, path_suffix) in [("Rust", "rs"), ("Markdown", "md")] { + let mut language = Language::new( + LanguageConfig { + name: name.into(), + path_suffixes: vec![path_suffix.to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + name, + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + match name { + "Rust" => rs_fake_servers = Some(fake_servers), + "Markdown" => md_fake_servers = Some(fake_servers), + _ => unreachable!(), + } + project.update(cx, |project, _| { + project.languages().add(Arc::new(language)); + }); + } + + let _rs_buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/a/main.rs", cx) + }) + .await + .unwrap(); + cx.foreground().run_until_parked(); + cx.foreground().start_waiting(); + let rs_fake_server = rs_fake_servers.unwrap().next().await.unwrap(); + let rs_editor = workspace + .update(cx, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + let rs_lsp_request_count = Arc::new(AtomicU32::new(0)); + rs_fake_server + .handle_request::(move |params, _| { + let task_lsp_request_count = Arc::clone(&rs_lsp_request_count); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, i), + label: lsp::InlayHintLabel::String(i.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }) + .next() + .await; + cx.foreground().run_until_parked(); + rs_editor.update(cx, |editor, cx| { + let expected_layers = vec!["0".to_string()]; + assert_eq!( + expected_layers, + cached_hint_labels(editor), + "Should get its first hints when opening the editor" + ); + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.version, 1, + "Rust editor update the cache version after every cache/view change" + ); + }); + + cx.foreground().run_until_parked(); + let _md_buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/a/other.md", cx) + }) + .await + .unwrap(); + cx.foreground().run_until_parked(); + cx.foreground().start_waiting(); + let md_fake_server = md_fake_servers.unwrap().next().await.unwrap(); + let md_editor = workspace + .update(cx, |workspace, cx| { + workspace.open_path((worktree_id, "other.md"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + let md_lsp_request_count = Arc::new(AtomicU32::new(0)); + md_fake_server + .handle_request::(move |params, _| { + let task_lsp_request_count = Arc::clone(&md_lsp_request_count); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/other.md").unwrap(), + ); + let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, i), + label: lsp::InlayHintLabel::String(i.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }) + .next() + .await; + cx.foreground().run_until_parked(); + md_editor.update(cx, |editor, cx| { + let expected_layers = vec!["0".to_string()]; + assert_eq!( + expected_layers, + cached_hint_labels(editor), + "Markdown editor should have a separate verison, repeating Rust editor rules" + ); + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!(inlay_cache.version, 1); + }); + + rs_editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input("some rs change", cx); + }); + cx.foreground().run_until_parked(); + rs_editor.update(cx, |editor, cx| { + let expected_layers = vec!["1".to_string()]; + assert_eq!( + expected_layers, + cached_hint_labels(editor), + "Rust inlay cache should change after the edit" + ); + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.version, 2, + "Every time hint cache changes, cache version should be incremented" + ); + }); + md_editor.update(cx, |editor, cx| { + let expected_layers = vec!["0".to_string()]; + assert_eq!( + expected_layers, + cached_hint_labels(editor), + "Markdown editor should not be affected by Rust editor changes" + ); + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!(inlay_cache.version, 1); + }); + + md_editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input("some md change", cx); + }); + cx.foreground().run_until_parked(); + md_editor.update(cx, |editor, cx| { + let expected_layers = vec!["1".to_string()]; + assert_eq!( + expected_layers, + cached_hint_labels(editor), + "Rust editor should not be affected by Markdown editor changes" + ); + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!(inlay_cache.version, 2); + }); + rs_editor.update(cx, |editor, cx| { + let expected_layers = vec!["1".to_string()]; + assert_eq!( + expected_layers, + cached_hint_labels(editor), + "Markdown editor should also change independently" + ); + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!(inlay_cache.version, 2); + }); + } + #[gpui::test] async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) { let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]); @@ -1079,7 +1434,6 @@ mod tests { visible_hint_labels(editor, cx) ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds); assert_eq!( inlay_cache.version, edits_made, "Should not update cache version due to new loaded hints being the same" @@ -1215,7 +1569,6 @@ mod tests { assert!(cached_hint_labels(editor).is_empty()); assert!(visible_hint_labels(editor, cx).is_empty()); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds); assert_eq!( inlay_cache.version, edits_made, "The editor should not update the cache version after /refresh query without updates" @@ -1289,20 +1642,18 @@ mod tests { visible_hint_labels(editor, cx), ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds); assert_eq!(inlay_cache.version, edits_made); }); } #[gpui::test] async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) { - let allowed_hint_kinds = HashSet::from_iter([None]); init_test(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { enabled: true, - show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), - show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)), - show_other_hints: allowed_hint_kinds.contains(&None), + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, }) }); @@ -1370,7 +1721,6 @@ mod tests { ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds); assert_eq!( inlay_cache.version, 1, "Only one update should be registered in the cache after all cancellations" @@ -1417,7 +1767,6 @@ mod tests { ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds); assert_eq!( inlay_cache.version, 2, "Should update the cache version once more, for the new change" @@ -1427,13 +1776,12 @@ mod tests { #[gpui::test] async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) { - let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]); init_test(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { enabled: true, - show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), - show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)), - show_other_hints: allowed_hint_kinds.contains(&None), + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, }) }); @@ -1539,7 +1887,6 @@ mod tests { ); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds); assert_eq!( inlay_cache.version, 2, "Both LSP queries should've bumped the cache version" @@ -1572,7 +1919,6 @@ mod tests { "Should have hints from the new LSP response after edit"); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds); assert_eq!(inlay_cache.version, 5, "Should update the cache for every LSP response with hints added"); }); } @@ -1582,13 +1928,12 @@ mod tests { deterministic: Arc, cx: &mut gpui::TestAppContext, ) { - let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]); init_test(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { enabled: true, - show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), - show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)), - show_other_hints: allowed_hint_kinds.contains(&None), + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, }) }); @@ -1794,7 +2139,6 @@ mod tests { ); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds); assert_eq!(inlay_cache.version, 4, "Every visible excerpt hints should bump the verison"); }); @@ -1826,7 +2170,6 @@ mod tests { "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits"); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds); assert_eq!(inlay_cache.version, 9); }); @@ -1855,7 +2198,6 @@ mod tests { "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched"); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds); assert_eq!(inlay_cache.version, 12); }); @@ -1884,7 +2226,6 @@ mod tests { "After multibuffer was scrolled to the end, further scrolls up should not bring more hints"); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds); assert_eq!(inlay_cache.version, 12, "No updates should happen during scrolling already scolled buffer"); }); @@ -1911,7 +2252,6 @@ mod tests { unedited (2nd) buffer should have the same hint"); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds); assert_eq!(inlay_cache.version, 16); }); } diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 523a0af964..1bd37da52f 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -193,7 +193,11 @@ pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPo }) } -pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint { +pub fn start_of_paragraph( + map: &DisplaySnapshot, + display_point: DisplayPoint, + mut count: usize, +) -> DisplayPoint { let point = display_point.to_point(map); if point.row == 0 { return map.max_point(); @@ -203,7 +207,11 @@ pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> for row in (0..point.row + 1).rev() { let blank = map.buffer_snapshot.is_line_blank(row); if found_non_blank_line && blank { - return Point::new(row, 0).to_display_point(map); + if count <= 1 { + return Point::new(row, 0).to_display_point(map); + } + count -= 1; + found_non_blank_line = false; } found_non_blank_line |= !blank; @@ -212,7 +220,11 @@ pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint::zero() } -pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint { +pub fn end_of_paragraph( + map: &DisplaySnapshot, + display_point: DisplayPoint, + mut count: usize, +) -> DisplayPoint { let point = display_point.to_point(map); if point.row == map.max_buffer_row() { return DisplayPoint::zero(); @@ -222,7 +234,11 @@ pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> D for row in point.row..map.max_buffer_row() + 1 { let blank = map.buffer_snapshot.is_line_blank(row); if found_non_blank_line && blank { - return Point::new(row, 0).to_display_point(map); + if count <= 1 { + return Point::new(row, 0).to_display_point(map); + } + count -= 1; + found_non_blank_line = false; } found_non_blank_line |= !blank; @@ -263,13 +279,13 @@ pub fn find_preceding_boundary( if let Some((prev_ch, prev_point)) = prev { if is_boundary(ch, prev_ch) { - return prev_point; + return map.clip_point(prev_point, Bias::Left); } } prev = Some((ch, point)); } - DisplayPoint::zero() + map.clip_point(DisplayPoint::zero(), Bias::Left) } /// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the @@ -292,7 +308,7 @@ pub fn find_preceding_boundary_in_line( for (ch, point) in map.reverse_chars_at(from) { if let Some((prev_ch, prev_point)) = prev { if is_boundary(ch, prev_ch) { - return prev_point; + return map.clip_point(prev_point, Bias::Left); } } @@ -303,7 +319,7 @@ pub fn find_preceding_boundary_in_line( prev = Some((ch, point)); } - prev.map(|(_, point)| point).unwrap_or(from) + map.clip_point(prev.map(|(_, point)| point).unwrap_or(from), Bias::Left) } /// Scans for a boundary following the given start point until a boundary is found, indicated by the @@ -406,8 +422,12 @@ pub fn split_display_range_by_lines( #[cfg(test)] mod tests { use super::*; - use crate::{test::marked_display_snapshot, Buffer, DisplayMap, ExcerptRange, MultiBuffer}; + use crate::{ + display_map::Inlay, test::marked_display_snapshot, Buffer, DisplayMap, ExcerptRange, + InlayId, MultiBuffer, + }; use settings::SettingsStore; + use util::post_inc; #[gpui::test] fn test_previous_word_start(cx: &mut gpui::AppContext) { @@ -505,6 +525,80 @@ mod tests { }); } + #[gpui::test] + fn test_find_preceding_boundary_with_inlays(cx: &mut gpui::AppContext) { + init_test(cx); + + let input_text = "abcdefghijklmnopqrstuvwxys"; + let family_id = cx + .font_cache() + .load_family(&["Helvetica"], &Default::default()) + .unwrap(); + let font_id = cx + .font_cache() + .select_font(family_id, &Default::default()) + .unwrap(); + let font_size = 14.0; + let buffer = MultiBuffer::build_simple(input_text, cx); + let buffer_snapshot = buffer.read(cx).snapshot(cx); + let display_map = + cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx)); + + // add all kinds of inlays between two word boundaries: we should be able to cross them all, when looking for another boundary + let mut id = 0; + let inlays = (0..buffer_snapshot.len()) + .map(|offset| { + [ + Inlay { + id: InlayId::Suggestion(post_inc(&mut id)), + position: buffer_snapshot.anchor_at(offset, Bias::Left), + text: format!("test").into(), + }, + Inlay { + id: InlayId::Suggestion(post_inc(&mut id)), + position: buffer_snapshot.anchor_at(offset, Bias::Right), + text: format!("test").into(), + }, + Inlay { + id: InlayId::Hint(post_inc(&mut id)), + position: buffer_snapshot.anchor_at(offset, Bias::Left), + text: format!("test").into(), + }, + Inlay { + id: InlayId::Hint(post_inc(&mut id)), + position: buffer_snapshot.anchor_at(offset, Bias::Right), + text: format!("test").into(), + }, + ] + }) + .flatten() + .collect(); + let snapshot = display_map.update(cx, |map, cx| { + map.splice_inlays(Vec::new(), inlays, cx); + map.snapshot(cx) + }); + + assert_eq!( + find_preceding_boundary( + &snapshot, + buffer_snapshot.len().to_display_point(&snapshot), + |left, _| left == 'a', + ), + 0.to_display_point(&snapshot), + "Should not stop at inlays when looking for boundaries" + ); + + assert_eq!( + find_preceding_boundary_in_line( + &snapshot, + buffer_snapshot.len().to_display_point(&snapshot), + |left, _| left == 'a', + ), + 0.to_display_point(&snapshot), + "Should not stop at inlays when looking for boundaries in line" + ); + } + #[gpui::test] fn test_next_word_end(cx: &mut gpui::AppContext) { init_test(cx); diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index 95da7ff297..bac70f139a 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -210,6 +210,10 @@ impl<'a> EditorTestContext<'a> { self.assert_selections(expected_selections, marked_text.to_string()) } + pub fn editor_state(&mut self) -> String { + generate_marked_text(self.buffer_text().as_str(), &self.editor_selections(), true) + } + #[track_caller] pub fn assert_editor_background_highlights(&mut self, marked_text: &str) { let expected_ranges = self.ranges(marked_text); @@ -248,14 +252,8 @@ impl<'a> EditorTestContext<'a> { self.assert_selections(expected_selections, expected_marked_text) } - #[track_caller] - fn assert_selections( - &mut self, - expected_selections: Vec>, - expected_marked_text: String, - ) { - let actual_selections = self - .editor + fn editor_selections(&self) -> Vec> { + self.editor .read_with(self.cx, |editor, cx| editor.selections.all::(cx)) .into_iter() .map(|s| { @@ -265,12 +263,22 @@ impl<'a> EditorTestContext<'a> { s.start..s.end } }) - .collect::>(); + .collect::>() + } + + #[track_caller] + fn assert_selections( + &mut self, + expected_selections: Vec>, + expected_marked_text: String, + ) { + let actual_selections = self.editor_selections(); let actual_marked_text = generate_marked_text(&self.buffer_text(), &actual_selections, true); if expected_selections != actual_selections { panic!( indoc! {" + {}Editor has unexpected selections. Expected selections: diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index cb738f567c..b3ebd224b0 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -31,6 +31,7 @@ serde_derive.workspace = true serde_json.workspace = true log.workspace = true libc = "0.2" +time.workspace = true [dev-dependencies] gpui = { path = "../gpui", features = ["test-support"] } diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 592e6c9a53..ec8a249ff4 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -279,6 +279,9 @@ impl Fs for RealFs { async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> { let buffer_size = text.summary().len.min(10 * 1024); + if let Some(path) = path.parent() { + self.create_dir(path).await?; + } let file = smol::fs::File::create(path).await?; let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file); for chunk in chunks(text, line_ending) { @@ -1077,6 +1080,9 @@ impl Fs for FakeFs { self.simulate_random_delay().await; let path = normalize_path(path); let content = chunks(text, line_ending).collect(); + if let Some(path) = path.parent() { + self.create_dir(path).await?; + } self.write_file_internal(path, content)?; Ok(()) } diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index 488262887f..0e5fd8343f 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -1,6 +1,6 @@ use anyhow::Result; use collections::HashMap; -use git2::ErrorCode; +use git2::{BranchType, ErrorCode}; use parking_lot::Mutex; use rpc::proto; use serde_derive::{Deserialize, Serialize}; @@ -16,6 +16,12 @@ use util::ResultExt; pub use git2::Repository as LibGitRepository; +#[derive(Clone, Debug, Hash, PartialEq)] +pub struct Branch { + pub name: Box, + /// Timestamp of most recent commit, normalized to Unix Epoch format. + pub unix_timestamp: Option, +} #[async_trait::async_trait] pub trait GitRepository: Send { fn reload_index(&self); @@ -27,6 +33,12 @@ pub trait GitRepository: Send { fn statuses(&self) -> Option>; fn status(&self, path: &RepoPath) -> Result>; + fn branches(&self) -> Result> { + Ok(vec![]) + } + fn change_branch(&self, _: &str) -> Result<()> { + Ok(()) + } } impl std::fmt::Debug for dyn GitRepository { @@ -106,6 +118,40 @@ impl GitRepository for LibGitRepository { } } } + fn branches(&self) -> Result> { + let local_branches = self.branches(Some(BranchType::Local))?; + let valid_branches = local_branches + .filter_map(|branch| { + branch.ok().and_then(|(branch, _)| { + let name = branch.name().ok().flatten().map(Box::from)?; + let timestamp = branch.get().peel_to_commit().ok()?.time(); + let unix_timestamp = timestamp.seconds(); + let timezone_offset = timestamp.offset_minutes(); + let utc_offset = + time::UtcOffset::from_whole_seconds(timezone_offset * 60).ok()?; + let unix_timestamp = + time::OffsetDateTime::from_unix_timestamp(unix_timestamp).ok()?; + Some(Branch { + name, + unix_timestamp: Some(unix_timestamp.to_offset(utc_offset).unix_timestamp()), + }) + }) + }) + .collect(); + Ok(valid_branches) + } + fn change_branch(&self, name: &str) -> Result<()> { + let revision = self.find_branch(name, BranchType::Local)?; + let revision = revision.get(); + let as_tree = revision.peel_to_tree()?; + self.checkout_tree(as_tree.as_object(), None)?; + self.set_head( + revision + .name() + .ok_or_else(|| anyhow::anyhow!("Branch name could not be retrieved"))?, + )?; + Ok(()) + } } fn read_status(status: git2::Status) -> Option { diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index 0b41ee6dca..769f2eda55 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -24,6 +24,7 @@ pub struct GoToLine { prev_scroll_position: Option, cursor_point: Point, max_point: Point, + has_focus: bool, } pub enum Event { @@ -57,6 +58,7 @@ impl GoToLine { prev_scroll_position: scroll_position, cursor_point, max_point, + has_focus: false, } } @@ -178,11 +180,20 @@ impl View for GoToLine { } fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { + self.has_focus = true; cx.focus(&self.line_editor); } + + fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext) { + self.has_focus = false; + } } impl Modal for GoToLine { + fn has_focus(&self) -> bool { + self.has_focus + } + fn dismiss_on_event(event: &Self::Event) -> bool { matches!(event, Event::Dismissed) } diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 20043a9093..640614324f 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -2971,14 +2971,12 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> { } pub fn focus(&mut self, handle: &AnyViewHandle) { - self.window_context - .focus(handle.window_id, Some(handle.view_id)); + self.window_context.focus(Some(handle.view_id)); } pub fn focus_self(&mut self) { - let window_id = self.window_id; let view_id = self.view_id; - self.window_context.focus(window_id, Some(view_id)); + self.window_context.focus(Some(view_id)); } pub fn is_self_focused(&self) -> bool { @@ -2997,8 +2995,7 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> { } pub fn blur(&mut self) { - let window_id = self.window_id; - self.window_context.focus(window_id, None); + self.window_context.focus(None); } pub fn on_window_should_close(&mut self, mut callback: F) @@ -3304,11 +3301,15 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> { let region_id = MouseRegionId::new::(self.view_id, region_id); MouseState { hovered: self.window.hovered_region_ids.contains(®ion_id), - clicked: self - .window - .clicked_region_ids - .get(®ion_id) - .and_then(|_| self.window.clicked_button), + clicked: if let Some((clicked_region_id, button)) = self.window.clicked_region { + if region_id == clicked_region_id { + Some(button) + } else { + None + } + } else { + None + }, accessed_hovered: false, accessed_clicked: false, } diff --git a/crates/gpui/src/app/window.rs b/crates/gpui/src/app/window.rs index cffce6c3a6..58d7bb4c40 100644 --- a/crates/gpui/src/app/window.rs +++ b/crates/gpui/src/app/window.rs @@ -8,14 +8,14 @@ use crate::{ MouseButton, MouseMovedEvent, PromptLevel, WindowBounds, }, scene::{ - CursorRegion, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseEvent, MouseHover, - MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut, Scene, + CursorRegion, MouseClick, MouseClickOut, MouseDown, MouseDownOut, MouseDrag, MouseEvent, + MouseHover, MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut, Scene, }, text_layout::TextLayoutCache, util::post_inc, Action, AnyView, AnyViewHandle, AppContext, BorrowAppContext, BorrowWindowContext, Effect, - Element, Entity, Handle, LayoutContext, MouseRegion, MouseRegionId, SceneBuilder, Subscription, - View, ViewContext, ViewHandle, WindowInvalidation, + Element, Entity, Handle, LayoutContext, MouseRegion, MouseRegionId, NoAction, SceneBuilder, + Subscription, View, ViewContext, ViewHandle, WindowInvalidation, }; use anyhow::{anyhow, bail, Result}; use collections::{HashMap, HashSet}; @@ -53,7 +53,7 @@ pub struct Window { last_mouse_moved_event: Option, pub(crate) hovered_region_ids: HashSet, pub(crate) clicked_region_ids: HashSet, - pub(crate) clicked_button: Option, + pub(crate) clicked_region: Option<(MouseRegionId, MouseButton)>, mouse_position: Vector2F, text_layout_cache: TextLayoutCache, } @@ -86,7 +86,7 @@ impl Window { last_mouse_moved_event: None, hovered_region_ids: Default::default(), clicked_region_ids: Default::default(), - clicked_button: None, + clicked_region: None, mouse_position: vec2f(0., 0.), titlebar_height, appearance, @@ -434,7 +434,11 @@ impl<'a> WindowContext<'a> { MatchResult::None => false, MatchResult::Pending => true, MatchResult::Matches(matches) => { + let no_action_id = (NoAction {}).id(); for (view_id, action) in matches { + if action.id() == no_action_id { + return false; + } if self.dispatch_action(Some(*view_id), action.as_ref()) { self.keystroke_matcher.clear_pending(); handled_by = Some(action.boxed_clone()); @@ -480,8 +484,8 @@ impl<'a> WindowContext<'a> { // specific ancestor element that contained both [positions]' // So we need to store the overlapping regions on mouse down. - // If there is already clicked_button stored, don't replace it. - if self.window.clicked_button.is_none() { + // If there is already region being clicked, don't replace it. + if self.window.clicked_region.is_none() { self.window.clicked_region_ids = self .window .mouse_regions @@ -495,7 +499,17 @@ impl<'a> WindowContext<'a> { }) .collect(); - self.window.clicked_button = Some(e.button); + let mut highest_z_index = 0; + let mut clicked_region_id = None; + for (region, z_index) in self.window.mouse_regions.iter() { + if region.bounds.contains_point(e.position) && *z_index >= highest_z_index { + highest_z_index = *z_index; + clicked_region_id = Some(region.id()); + } + } + + self.window.clicked_region = + clicked_region_id.map(|region_id| (region_id, e.button)); } mouse_events.push(MouseEvent::Down(MouseDown { @@ -524,6 +538,10 @@ impl<'a> WindowContext<'a> { region: Default::default(), platform_event: e.clone(), })); + mouse_events.push(MouseEvent::ClickOut(MouseClickOut { + region: Default::default(), + platform_event: e.clone(), + })); } Event::MouseMoved( @@ -556,7 +574,7 @@ impl<'a> WindowContext<'a> { prev_mouse_position: self.window.mouse_position, platform_event: e.clone(), })); - } else if let Some(clicked_button) = self.window.clicked_button { + } else if let Some((_, clicked_button)) = self.window.clicked_region { // Mouse up event happened outside the current window. Simulate mouse up button event let button_event = e.to_button_event(clicked_button); mouse_events.push(MouseEvent::Up(MouseUp { @@ -679,8 +697,8 @@ impl<'a> WindowContext<'a> { // Only raise click events if the released button is the same as the one stored if self .window - .clicked_button - .map(|clicked_button| clicked_button == e.button) + .clicked_region + .map(|(_, clicked_button)| clicked_button == e.button) .unwrap_or(false) { // Clear clicked regions and clicked button @@ -688,7 +706,7 @@ impl<'a> WindowContext<'a> { &mut self.window.clicked_region_ids, Default::default(), ); - self.window.clicked_button = None; + self.window.clicked_region = None; // Find regions which still overlap with the mouse since the last MouseDown happened for (mouse_region, _) in self.window.mouse_regions.iter().rev() { @@ -712,7 +730,10 @@ impl<'a> WindowContext<'a> { } } - MouseEvent::MoveOut(_) | MouseEvent::UpOut(_) | MouseEvent::DownOut(_) => { + MouseEvent::MoveOut(_) + | MouseEvent::UpOut(_) + | MouseEvent::DownOut(_) + | MouseEvent::ClickOut(_) => { for (mouse_region, _) in self.window.mouse_regions.iter().rev() { // NOT contains if !mouse_region @@ -860,18 +881,10 @@ impl<'a> WindowContext<'a> { } for view_id in &invalidation.updated { let titlebar_height = self.window.titlebar_height; - let hovered_region_ids = self.window.hovered_region_ids.clone(); - let clicked_region_ids = self - .window - .clicked_button - .map(|button| (self.window.clicked_region_ids.clone(), button)); - let element = self .render_view(RenderParams { view_id: *view_id, titlebar_height, - hovered_region_ids, - clicked_region_ids, refreshing: false, appearance, }) @@ -1085,6 +1098,10 @@ impl<'a> WindowContext<'a> { self.window.focused_view_id } + pub fn focus(&mut self, view_id: Option) { + self.app_context.focus(self.window_id, view_id); + } + pub fn window_bounds(&self) -> WindowBounds { self.window.platform_window.bounds() } @@ -1176,8 +1193,6 @@ impl<'a> WindowContext<'a> { pub struct RenderParams { pub view_id: usize, pub titlebar_height: f32, - pub hovered_region_ids: HashSet, - pub clicked_region_ids: Option<(HashSet, MouseButton)>, pub refreshing: bool, pub appearance: Appearance, } diff --git a/crates/gpui/src/elements/mouse_event_handler.rs b/crates/gpui/src/elements/mouse_event_handler.rs index 6f2762db66..1b8142d964 100644 --- a/crates/gpui/src/elements/mouse_event_handler.rs +++ b/crates/gpui/src/elements/mouse_event_handler.rs @@ -7,8 +7,8 @@ use crate::{ platform::CursorStyle, platform::MouseButton, scene::{ - CursorRegion, HandlerSet, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseHover, - MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut, + CursorRegion, HandlerSet, MouseClick, MouseClickOut, MouseDown, MouseDownOut, MouseDrag, + MouseHover, MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut, }, AnyElement, Element, EventContext, LayoutContext, MouseRegion, MouseState, SceneBuilder, SizeConstraint, View, ViewContext, @@ -136,6 +136,15 @@ impl MouseEventHandler { self } + pub fn on_click_out( + mut self, + button: MouseButton, + handler: impl Fn(MouseClickOut, &mut V, &mut EventContext) + 'static, + ) -> Self { + self.handlers = self.handlers.on_click_out(button, handler); + self + } + pub fn on_down_out( mut self, button: MouseButton, diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 25d022d8ed..3442934b3a 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -31,3 +31,5 @@ pub use window::{Axis, SizeConstraint, Vector2FExt, WindowContext}; pub use anyhow; pub use serde_json; + +actions!(zed, [NoAction]); diff --git a/crates/gpui/src/platform/event.rs b/crates/gpui/src/platform/event.rs index c39c76dc34..4456db9a51 100644 --- a/crates/gpui/src/platform/event.rs +++ b/crates/gpui/src/platform/event.rs @@ -4,7 +4,7 @@ use pathfinder_geometry::vector::vec2f; use crate::{geometry::vector::Vector2F, keymap_matcher::Keystroke}; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct KeyDownEvent { pub keystroke: Keystroke, pub is_held: bool, diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 3c82538611..381a4fbaaa 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -232,10 +232,6 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C sel!(canBecomeKeyWindow), yes as extern "C" fn(&Object, Sel) -> BOOL, ); - decl.add_method( - sel!(sendEvent:), - send_event as extern "C" fn(&Object, Sel, id), - ); decl.add_method( sel!(windowDidResize:), window_did_resize as extern "C" fn(&Object, Sel, id), @@ -299,7 +295,7 @@ struct WindowState { appearance_changed_callback: Option>, input_handler: Option>, pending_key_down: Option<(KeyDownEvent, Option)>, - performed_key_equivalent: bool, + last_key_equivalent: Option, synthetic_drag_counter: usize, executor: Rc, scene_to_render: Option, @@ -521,7 +517,7 @@ impl Window { appearance_changed_callback: None, input_handler: None, pending_key_down: None, - performed_key_equivalent: false, + last_key_equivalent: None, synthetic_drag_counter: 0, executor, scene_to_render: Default::default(), @@ -965,36 +961,34 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent: let window_height = window_state_borrow.content_size().y(); let event = unsafe { Event::from_native(native_event, Some(window_height)) }; - if let Some(event) = event { + if let Some(Event::KeyDown(event)) = event { + // For certain keystrokes, macOS will first dispatch a "key equivalent" event. + // If that event isn't handled, it will then dispatch a "key down" event. GPUI + // makes no distinction between these two types of events, so we need to ignore + // the "key down" event if we've already just processed its "key equivalent" version. if key_equivalent { - window_state_borrow.performed_key_equivalent = true; - } else if window_state_borrow.performed_key_equivalent { + window_state_borrow.last_key_equivalent = Some(event.clone()); + } else if window_state_borrow.last_key_equivalent.take().as_ref() == Some(&event) { return NO; } - let function_is_held; - window_state_borrow.pending_key_down = match event { - Event::KeyDown(event) => { - let keydown = event.keystroke.clone(); - // Ignore events from held-down keys after some of the initially-pressed keys - // were released. - if event.is_held { - if window_state_borrow.last_fresh_keydown.as_ref() != Some(&keydown) { - return YES; - } - } else { - window_state_borrow.last_fresh_keydown = Some(keydown); - } - function_is_held = event.keystroke.function; - Some((event, None)) + let keydown = event.keystroke.clone(); + let fn_modifier = keydown.function; + // Ignore events from held-down keys after some of the initially-pressed keys + // were released. + if event.is_held { + if window_state_borrow.last_fresh_keydown.as_ref() != Some(&keydown) { + return YES; } - - _ => return NO, - }; - + } else { + window_state_borrow.last_fresh_keydown = Some(keydown); + } + window_state_borrow.pending_key_down = Some((event, None)); drop(window_state_borrow); - if !function_is_held { + // Send the event to the input context for IME handling, unless the `fn` modifier is + // being pressed. + if !fn_modifier { unsafe { let input_context: id = msg_send![this, inputContext]; let _: BOOL = msg_send![input_context, handleEvent: native_event]; @@ -1143,13 +1137,6 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) { } } -extern "C" fn send_event(this: &Object, _: Sel, native_event: id) { - unsafe { - let _: () = msg_send![super(this, class!(NSWindow)), sendEvent: native_event]; - get_window_state(this).borrow_mut().performed_key_equivalent = false; - } -} - extern "C" fn window_did_resize(this: &Object, _: Sel, _: id) { let window_state = unsafe { get_window_state(this) }; window_state.as_ref().borrow().move_traffic_light(); diff --git a/crates/gpui/src/scene/mouse_event.rs b/crates/gpui/src/scene/mouse_event.rs index cf0a08f33e..a492da771b 100644 --- a/crates/gpui/src/scene/mouse_event.rs +++ b/crates/gpui/src/scene/mouse_event.rs @@ -99,6 +99,20 @@ impl Deref for MouseClick { } } +#[derive(Debug, Default, Clone)] +pub struct MouseClickOut { + pub region: RectF, + pub platform_event: MouseButtonEvent, +} + +impl Deref for MouseClickOut { + type Target = MouseButtonEvent; + + fn deref(&self) -> &Self::Target { + &self.platform_event + } +} + #[derive(Debug, Default, Clone)] pub struct MouseDownOut { pub region: RectF, @@ -150,6 +164,7 @@ pub enum MouseEvent { Down(MouseDown), Up(MouseUp), Click(MouseClick), + ClickOut(MouseClickOut), DownOut(MouseDownOut), UpOut(MouseUpOut), ScrollWheel(MouseScrollWheel), @@ -165,6 +180,7 @@ impl MouseEvent { MouseEvent::Down(r) => r.region = region, MouseEvent::Up(r) => r.region = region, MouseEvent::Click(r) => r.region = region, + MouseEvent::ClickOut(r) => r.region = region, MouseEvent::DownOut(r) => r.region = region, MouseEvent::UpOut(r) => r.region = region, MouseEvent::ScrollWheel(r) => r.region = region, @@ -182,6 +198,7 @@ impl MouseEvent { MouseEvent::Down(_) => true, MouseEvent::Up(_) => true, MouseEvent::Click(_) => true, + MouseEvent::ClickOut(_) => true, MouseEvent::DownOut(_) => false, MouseEvent::UpOut(_) => false, MouseEvent::ScrollWheel(_) => true, @@ -222,6 +239,10 @@ impl MouseEvent { discriminant(&MouseEvent::Click(Default::default())) } + pub fn click_out_disc() -> Discriminant { + discriminant(&MouseEvent::ClickOut(Default::default())) + } + pub fn down_out_disc() -> Discriminant { discriminant(&MouseEvent::DownOut(Default::default())) } @@ -239,6 +260,7 @@ impl MouseEvent { MouseEvent::Down(e) => HandlerKey::new(Self::down_disc(), Some(e.button)), MouseEvent::Up(e) => HandlerKey::new(Self::up_disc(), Some(e.button)), MouseEvent::Click(e) => HandlerKey::new(Self::click_disc(), Some(e.button)), + MouseEvent::ClickOut(e) => HandlerKey::new(Self::click_out_disc(), Some(e.button)), MouseEvent::UpOut(e) => HandlerKey::new(Self::up_out_disc(), Some(e.button)), MouseEvent::DownOut(e) => HandlerKey::new(Self::down_out_disc(), Some(e.button)), MouseEvent::ScrollWheel(_) => HandlerKey::new(Self::scroll_wheel_disc(), None), diff --git a/crates/gpui/src/scene/mouse_region.rs b/crates/gpui/src/scene/mouse_region.rs index 0efc794148..ca2cc04b9d 100644 --- a/crates/gpui/src/scene/mouse_region.rs +++ b/crates/gpui/src/scene/mouse_region.rs @@ -14,7 +14,7 @@ use super::{ MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseEvent, MouseHover, MouseMove, MouseUp, MouseUpOut, }, - MouseMoveOut, MouseScrollWheel, + MouseClickOut, MouseMoveOut, MouseScrollWheel, }; #[derive(Clone)] @@ -89,6 +89,15 @@ impl MouseRegion { self } + pub fn on_click_out(mut self, button: MouseButton, handler: F) -> Self + where + V: View, + F: Fn(MouseClickOut, &mut V, &mut EventContext) + 'static, + { + self.handlers = self.handlers.on_click_out(button, handler); + self + } + pub fn on_down_out(mut self, button: MouseButton, handler: F) -> Self where V: View, @@ -246,6 +255,10 @@ impl HandlerSet { HandlerKey::new(MouseEvent::click_disc(), Some(button)), SmallVec::from_buf([Rc::new(|_, _, _, _| true)]), ); + set.insert( + HandlerKey::new(MouseEvent::click_out_disc(), Some(button)), + SmallVec::from_buf([Rc::new(|_, _, _, _| true)]), + ); set.insert( HandlerKey::new(MouseEvent::down_out_disc(), Some(button)), SmallVec::from_buf([Rc::new(|_, _, _, _| true)]), @@ -405,6 +418,28 @@ impl HandlerSet { self } + pub fn on_click_out(mut self, button: MouseButton, handler: F) -> Self + where + V: View, + F: Fn(MouseClickOut, &mut V, &mut EventContext) + 'static, + { + self.insert(MouseEvent::click_out_disc(), Some(button), + Rc::new(move |region_event, view, cx, view_id| { + if let MouseEvent::ClickOut(e) = region_event { + let view = view.downcast_mut().unwrap(); + let mut cx = ViewContext::mutable(cx, view_id); + let mut cx = EventContext::new(&mut cx); + handler(e, view, &mut cx); + cx.handled + } else { + panic!( + "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::ClickOut, found {:?}", + region_event); + } + })); + self + } + pub fn on_down_out(mut self, button: MouseButton, handler: F) -> Self where V: View, diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index 1570baf185..b6431c2286 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -4,7 +4,6 @@ mod syntax_map_tests; use crate::{Grammar, InjectionConfig, Language, LanguageRegistry}; use collections::HashMap; use futures::FutureExt; -use lazy_static::lazy_static; use parking_lot::Mutex; use std::{ borrow::Cow, @@ -25,9 +24,7 @@ thread_local! { static PARSER: RefCell = RefCell::new(Parser::new()); } -lazy_static! { - static ref QUERY_CURSORS: Mutex> = Default::default(); -} +static QUERY_CURSORS: Mutex> = Mutex::new(vec![]); #[derive(Default)] pub struct SyntaxMap { diff --git a/crates/live_kit_client/Cargo.toml b/crates/live_kit_client/Cargo.toml index 36087a42a3..78f435906b 100644 --- a/crates/live_kit_client/Cargo.toml +++ b/crates/live_kit_client/Cargo.toml @@ -17,7 +17,6 @@ test-support = [ "async-trait", "collections/test-support", "gpui/test-support", - "lazy_static", "live_kit_server", "nanoid", ] @@ -38,7 +37,6 @@ parking_lot.workspace = true postage.workspace = true async-trait = { workspace = true, optional = true } -lazy_static = { workspace = true, optional = true } nanoid = { version ="0.4", optional = true} [dev-dependencies] @@ -60,7 +58,6 @@ foreign-types = "0.3" futures.workspace = true hmac = "0.12" jwt = "0.16" -lazy_static.workspace = true objc = "0.2" parking_lot.workspace = true serde.workspace = true diff --git a/crates/live_kit_client/src/test.rs b/crates/live_kit_client/src/test.rs index 3fc046c5a2..ada864fc44 100644 --- a/crates/live_kit_client/src/test.rs +++ b/crates/live_kit_client/src/test.rs @@ -1,18 +1,15 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; -use collections::HashMap; +use collections::{BTreeMap, HashMap}; use futures::Stream; use gpui::executor::Background; -use lazy_static::lazy_static; use live_kit_server::token; use media::core_video::CVImageBuffer; use parking_lot::Mutex; use postage::watch; use std::{future::Future, mem, sync::Arc}; -lazy_static! { - static ref SERVERS: Mutex>> = Default::default(); -} +static SERVERS: Mutex>> = Mutex::new(BTreeMap::new()); pub struct TestServer { pub url: String, diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 33d6e84241..d09de5320c 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -25,6 +25,7 @@ pub struct Picker { theme: Arc theme::Picker>>>, confirmed: bool, pending_update_matches: Task>, + has_focus: bool, } pub trait PickerDelegate: Sized + 'static { @@ -45,10 +46,16 @@ pub trait PickerDelegate: Sized + 'static { fn center_selection_after_match_updates(&self) -> bool { false } - fn render_header(&self, _cx: &AppContext) -> Option>> { + fn render_header( + &self, + _cx: &mut ViewContext>, + ) -> Option>> { None } - fn render_footer(&self, _cx: &AppContext) -> Option>> { + fn render_footer( + &self, + _cx: &mut ViewContext>, + ) -> Option>> { None } } @@ -140,13 +147,22 @@ impl View for Picker { } fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { + self.has_focus = true; if cx.is_self_focused() { cx.focus(&self.query_editor); } } + + fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext) { + self.has_focus = false; + } } impl Modal for Picker { + fn has_focus(&self) -> bool { + self.has_focus + } + fn dismiss_on_event(event: &Self::Event) -> bool { matches!(event, PickerEvent::Dismiss) } @@ -191,6 +207,7 @@ impl Picker { theme, confirmed: false, pending_update_matches: Task::ready(None), + has_focus: false, }; this.update_matches(String::new(), cx); this diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index d6578c87ba..bfe5f89f68 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -64,7 +64,7 @@ itertools = "0.10" [dev-dependencies] ctor.workspace = true env_logger.workspace = true -pretty_assertions = "1.3.0" +pretty_assertions.workspace = true client = { path = "../client", features = ["test-support"] } collections = { path = "../collections", features = ["test-support"] } db = { path = "../db", features = ["test-support"] } diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index a3c6302e29..eec64beb5a 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1822,11 +1822,21 @@ impl LspCommand for InlayHints { async fn response_from_lsp( self, message: Option>, - _: ModelHandle, + project: ModelHandle, buffer: ModelHandle, - _: LanguageServerId, - cx: AsyncAppContext, + server_id: LanguageServerId, + mut cx: AsyncAppContext, ) -> Result> { + let (lsp_adapter, _) = language_server_for_buffer(&project, &buffer, server_id, &mut cx)?; + // `typescript-language-server` adds padding to the left for type hints, turning + // `const foo: boolean` into `const foo : boolean` which looks odd. + // `rust-analyzer` does not have the padding for this case, and we have to accomodate both. + // + // We could trim the whole string, but being pessimistic on par with the situation above, + // there might be a hint with multiple whitespaces at the end(s) which we need to display properly. + // Hence let's use a heuristic first to handle the most awkward case and look for more. + let force_no_type_left_padding = + lsp_adapter.name.0.as_ref() == "typescript-language-server"; cx.read(|cx| { let origin_buffer = buffer.read(cx); Ok(message @@ -1840,6 +1850,12 @@ impl LspCommand for InlayHints { }); let position = origin_buffer .clip_point_utf16(point_from_lsp(lsp_hint.position), Bias::Left); + let padding_left = + if force_no_type_left_padding && kind == Some(InlayHintKind::Type) { + false + } else { + lsp_hint.padding_left.unwrap_or(false) + }; InlayHint { buffer_id: origin_buffer.remote_id(), position: if kind == Some(InlayHintKind::Parameter) { @@ -1847,7 +1863,7 @@ impl LspCommand for InlayHints { } else { origin_buffer.anchor_after(position) }, - padding_left: lsp_hint.padding_left.unwrap_or(false), + padding_left, padding_right: lsp_hint.padding_right.unwrap_or(false), label: match lsp_hint.label { lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s), diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index bbb2064da2..81db0c7ed7 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -777,20 +777,32 @@ impl Project { } let mut language_servers_to_stop = Vec::new(); + let mut language_servers_to_restart = Vec::new(); let languages = self.languages.to_vec(); + let project_settings = settings::get::(cx).clone(); for (worktree_id, started_lsp_name) in self.language_server_ids.keys() { - let language = languages.iter().find(|l| { - l.lsp_adapters() + let language = languages.iter().find_map(|l| { + let adapter = l + .lsp_adapters() .iter() - .any(|adapter| &adapter.name == started_lsp_name) + .find(|adapter| &adapter.name == started_lsp_name)?; + Some((l, adapter)) }); - if let Some(language) = language { + if let Some((language, adapter)) = language { let worktree = self.worktree_for_id(*worktree_id, cx); - let file = worktree.and_then(|tree| { + let file = worktree.as_ref().and_then(|tree| { tree.update(cx, |tree, cx| tree.root_file(cx).map(|f| f as _)) }); if !language_settings(Some(language), file.as_ref(), cx).enable_language_server { language_servers_to_stop.push((*worktree_id, started_lsp_name.clone())); + } else if let Some(worktree) = worktree { + let new_lsp_settings = project_settings + .lsp + .get(&adapter.name.0) + .and_then(|s| s.initialization_options.as_ref()); + if adapter.initialization_options.as_ref() != new_lsp_settings { + language_servers_to_restart.push((worktree, Arc::clone(language))); + } } } } @@ -807,6 +819,11 @@ impl Project { self.start_language_servers(&worktree, worktree_path, language, cx); } + // Restart all language servers with changed initialization options. + for (worktree, language) in language_servers_to_restart { + self.restart_language_servers(worktree, language, cx); + } + if !self.copilot_enabled && Copilot::global(cx).is_some() { self.copilot_enabled = true; for buffer in self.opened_buffers.values() { @@ -3397,6 +3414,7 @@ impl Project { cx: &mut ModelContext, ) { if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { + cx.emit(Event::RefreshInlays); status.pending_work.remove(&token); cx.notify(); } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 20e693770f..2c3c9d5304 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -981,6 +981,19 @@ impl LocalWorktree { }) } + /// Find the lowest path in the worktree's datastructures that is an ancestor + fn lowest_ancestor(&self, path: &Path) -> PathBuf { + let mut lowest_ancestor = None; + for path in path.ancestors() { + if self.entry_for_path(path).is_some() { + lowest_ancestor = Some(path.to_path_buf()); + break; + } + } + + lowest_ancestor.unwrap_or_else(|| PathBuf::from("")) + } + pub fn create_entry( &self, path: impl Into>, @@ -988,6 +1001,7 @@ impl LocalWorktree { cx: &mut ModelContext, ) -> Task> { let path = path.into(); + let lowest_ancestor = self.lowest_ancestor(&path); let abs_path = self.absolutize(&path); let fs = self.fs.clone(); let write = cx.background().spawn(async move { @@ -1001,10 +1015,31 @@ impl LocalWorktree { cx.spawn(|this, mut cx| async move { write.await?; - this.update(&mut cx, |this, cx| { - this.as_local_mut().unwrap().refresh_entry(path, None, cx) - }) - .await + let (result, refreshes) = this.update(&mut cx, |this, cx| { + let mut refreshes = Vec::>>::new(); + let refresh_paths = path.strip_prefix(&lowest_ancestor).unwrap(); + for refresh_path in refresh_paths.ancestors() { + if refresh_path == Path::new("") { + continue; + } + let refresh_full_path = lowest_ancestor.join(refresh_path); + + refreshes.push(this.as_local_mut().unwrap().refresh_entry( + refresh_full_path.into(), + None, + cx, + )); + } + ( + this.as_local_mut().unwrap().refresh_entry(path, None, cx), + refreshes, + ) + }); + for refresh in refreshes { + refresh.await.log_err(); + } + + result.await }) } @@ -2140,6 +2175,7 @@ impl LocalSnapshot { impl BackgroundScannerState { fn should_scan_directory(&self, entry: &Entry) -> bool { (!entry.is_external && !entry.is_ignored) + || entry.path.file_name() == Some(&*DOT_GIT) || self.scanned_dirs.contains(&entry.id) // If we've ever scanned it, keep scanning || self .paths_to_scan @@ -2319,6 +2355,7 @@ impl BackgroundScannerState { .entry_for_id(entry_id) .map(|entry| RepositoryWorkDirectory(entry.path.clone())) else { continue }; + log::info!("reload git repository {:?}", dot_git_dir); let repository = repository.repo_ptr.lock(); let branch = repository.branch_name(); repository.reload_index(); @@ -2359,6 +2396,8 @@ impl BackgroundScannerState { } fn build_repository(&mut self, dot_git_path: Arc, fs: &dyn Fs) -> Option<()> { + log::info!("build git repository {:?}", dot_git_path); + let work_dir_path: Arc = dot_git_path.parent().unwrap().into(); // Guard against repositories inside the repository metadata @@ -3138,8 +3177,6 @@ impl BackgroundScanner { } async fn process_events(&mut self, mut abs_paths: Vec) { - log::debug!("received fs events {:?}", abs_paths); - let root_path = self.state.lock().snapshot.abs_path.clone(); let root_canonical_path = match self.fs.canonicalize(&root_path).await { Ok(path) => path, @@ -3150,7 +3187,6 @@ impl BackgroundScanner { }; let mut relative_paths = Vec::with_capacity(abs_paths.len()); - let mut unloaded_relative_paths = Vec::new(); abs_paths.sort_unstable(); abs_paths.dedup_by(|a, b| a.starts_with(&b)); abs_paths.retain(|abs_path| { @@ -3173,7 +3209,6 @@ impl BackgroundScanner { }); if !parent_dir_is_loaded { log::debug!("ignoring event {relative_path:?} within unloaded directory"); - unloaded_relative_paths.push(relative_path); return false; } @@ -3182,27 +3217,30 @@ impl BackgroundScanner { } }); - if !relative_paths.is_empty() { - let (scan_job_tx, scan_job_rx) = channel::unbounded(); - self.reload_entries_for_paths( - root_path, - root_canonical_path, - &relative_paths, - abs_paths, - Some(scan_job_tx.clone()), - ) - .await; - drop(scan_job_tx); - self.scan_dirs(false, scan_job_rx).await; - - let (scan_job_tx, scan_job_rx) = channel::unbounded(); - self.update_ignore_statuses(scan_job_tx).await; - self.scan_dirs(false, scan_job_rx).await; + if relative_paths.is_empty() { + return; } + log::debug!("received fs events {:?}", relative_paths); + + let (scan_job_tx, scan_job_rx) = channel::unbounded(); + self.reload_entries_for_paths( + root_path, + root_canonical_path, + &relative_paths, + abs_paths, + Some(scan_job_tx.clone()), + ) + .await; + drop(scan_job_tx); + self.scan_dirs(false, scan_job_rx).await; + + let (scan_job_tx, scan_job_rx) = channel::unbounded(); + self.update_ignore_statuses(scan_job_tx).await; + self.scan_dirs(false, scan_job_rx).await; + { let mut state = self.state.lock(); - relative_paths.extend(unloaded_relative_paths); state.reload_repositories(&relative_paths, self.fs.as_ref()); state.snapshot.completed_scan_id = state.snapshot.scan_id; for (_, entry_id) in mem::take(&mut state.removed_entry_ids) { @@ -3610,23 +3648,28 @@ impl BackgroundScanner { } } - let fs_entry = state.insert_entry(fs_entry, self.fs.as_ref()); - - if let Some(scan_queue_tx) = &scan_queue_tx { - let mut ancestor_inodes = state.snapshot.ancestor_inodes_for_path(&path); - if metadata.is_dir && !ancestor_inodes.contains(&metadata.inode) { - ancestor_inodes.insert(metadata.inode); - smol::block_on(scan_queue_tx.send(ScanJob { - abs_path, - path: path.clone(), - ignore_stack, - ancestor_inodes, - is_external: fs_entry.is_external, - scan_queue: scan_queue_tx.clone(), - })) - .unwrap(); + if let (Some(scan_queue_tx), true) = (&scan_queue_tx, fs_entry.is_dir()) { + if state.should_scan_directory(&fs_entry) { + let mut ancestor_inodes = + state.snapshot.ancestor_inodes_for_path(&path); + if !ancestor_inodes.contains(&metadata.inode) { + ancestor_inodes.insert(metadata.inode); + smol::block_on(scan_queue_tx.send(ScanJob { + abs_path, + path: path.clone(), + ignore_stack, + ancestor_inodes, + is_external: fs_entry.is_external, + scan_queue: scan_queue_tx.clone(), + })) + .unwrap(); + } + } else { + fs_entry.kind = EntryKind::UnloadedDir; } } + + state.insert_entry(fs_entry, self.fs.as_ref()); } Ok(None) => { self.remove_repo_path(&path, &mut state.snapshot); diff --git a/crates/project/src/worktree_tests.rs b/crates/project/src/worktree_tests.rs index f908d702eb..6f5b363509 100644 --- a/crates/project/src/worktree_tests.rs +++ b/crates/project/src/worktree_tests.rs @@ -936,6 +936,119 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) { ); } +#[gpui::test] +async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { + let client_fake = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); + + let fs_fake = FakeFs::new(cx.background()); + fs_fake + .insert_tree( + "/root", + json!({ + "a": {}, + }), + ) + .await; + + let tree_fake = Worktree::local( + client_fake, + "/root".as_ref(), + true, + fs_fake, + Default::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + + let entry = tree_fake + .update(cx, |tree, cx| { + tree.as_local_mut() + .unwrap() + .create_entry("a/b/c/d.txt".as_ref(), false, cx) + }) + .await + .unwrap(); + assert!(entry.is_file()); + + cx.foreground().run_until_parked(); + tree_fake.read_with(cx, |tree, _| { + assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file()); + assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir()); + assert!(tree.entry_for_path("a/b/").unwrap().is_dir()); + }); + + let client_real = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); + + let fs_real = Arc::new(RealFs); + let temp_root = temp_tree(json!({ + "a": {} + })); + + let tree_real = Worktree::local( + client_real, + temp_root.path(), + true, + fs_real, + Default::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + + let entry = tree_real + .update(cx, |tree, cx| { + tree.as_local_mut() + .unwrap() + .create_entry("a/b/c/d.txt".as_ref(), false, cx) + }) + .await + .unwrap(); + assert!(entry.is_file()); + + cx.foreground().run_until_parked(); + tree_real.read_with(cx, |tree, _| { + assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file()); + assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir()); + assert!(tree.entry_for_path("a/b/").unwrap().is_dir()); + }); + + // Test smallest change + let entry = tree_real + .update(cx, |tree, cx| { + tree.as_local_mut() + .unwrap() + .create_entry("a/b/c/e.txt".as_ref(), false, cx) + }) + .await + .unwrap(); + assert!(entry.is_file()); + + cx.foreground().run_until_parked(); + tree_real.read_with(cx, |tree, _| { + assert!(tree.entry_for_path("a/b/c/e.txt").unwrap().is_file()); + }); + + // Test largest change + let entry = tree_real + .update(cx, |tree, cx| { + tree.as_local_mut() + .unwrap() + .create_entry("d/e/f/g.txt".as_ref(), false, cx) + }) + .await + .unwrap(); + assert!(entry.is_file()); + + cx.foreground().run_until_parked(); + tree_real.read_with(cx, |tree, _| { + assert!(tree.entry_for_path("d/e/f/g.txt").unwrap().is_file()); + assert!(tree.entry_for_path("d/e/f").unwrap().is_dir()); + assert!(tree.entry_for_path("d/e/").unwrap().is_dir()); + assert!(tree.entry_for_path("d/").unwrap().is_dir()); + }); +} + #[gpui::test(iterations = 100)] async fn test_random_worktree_operations_during_initial_scan( cx: &mut TestAppContext, @@ -1654,6 +1767,23 @@ async fn test_git_status(deterministic: Arc, cx: &mut TestAppCont })); + const A_TXT: &'static str = "a.txt"; + const B_TXT: &'static str = "b.txt"; + const E_TXT: &'static str = "c/d/e.txt"; + const F_TXT: &'static str = "f.txt"; + const DOTGITIGNORE: &'static str = ".gitignore"; + const BUILD_FILE: &'static str = "target/build_file"; + let project_path = Path::new("project"); + + // Set up git repository before creating the worktree. + let work_dir = root.path().join("project"); + let mut repo = git_init(work_dir.as_path()); + repo.add_ignore_rule(IGNORE_RULE).unwrap(); + git_add(A_TXT, &repo); + git_add(E_TXT, &repo); + git_add(DOTGITIGNORE, &repo); + git_commit("Initial commit", &repo); + let tree = Worktree::local( build_client(cx), root.path(), @@ -1665,26 +1795,9 @@ async fn test_git_status(deterministic: Arc, cx: &mut TestAppCont .await .unwrap(); + tree.flush_fs_events(cx).await; cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) .await; - - const A_TXT: &'static str = "a.txt"; - const B_TXT: &'static str = "b.txt"; - const E_TXT: &'static str = "c/d/e.txt"; - const F_TXT: &'static str = "f.txt"; - const DOTGITIGNORE: &'static str = ".gitignore"; - const BUILD_FILE: &'static str = "target/build_file"; - let project_path: &Path = &Path::new("project"); - - let work_dir = root.path().join("project"); - let mut repo = git_init(work_dir.as_path()); - repo.add_ignore_rule(IGNORE_RULE).unwrap(); - git_add(Path::new(A_TXT), &repo); - git_add(Path::new(E_TXT), &repo); - git_add(Path::new(DOTGITIGNORE), &repo); - git_commit("Initial commit", &repo); - - tree.flush_fs_events(cx).await; deterministic.run_until_parked(); // Check that the right git state is observed on startup @@ -1704,39 +1817,39 @@ async fn test_git_status(deterministic: Arc, cx: &mut TestAppCont ); }); + // Modify a file in the working copy. std::fs::write(work_dir.join(A_TXT), "aa").unwrap(); - tree.flush_fs_events(cx).await; deterministic.run_until_parked(); + // The worktree detects that the file's git status has changed. tree.read_with(cx, |tree, _cx| { let snapshot = tree.snapshot(); - assert_eq!( snapshot.status_for_file(project_path.join(A_TXT)), Some(GitFileStatus::Modified) ); }); - git_add(Path::new(A_TXT), &repo); - git_add(Path::new(B_TXT), &repo); + // Create a commit in the git repository. + git_add(A_TXT, &repo); + git_add(B_TXT, &repo); git_commit("Committing modified and added", &repo); tree.flush_fs_events(cx).await; deterministic.run_until_parked(); - // Check that repo only changes are tracked + // The worktree detects that the files' git status have changed. tree.read_with(cx, |tree, _cx| { let snapshot = tree.snapshot(); - assert_eq!( snapshot.status_for_file(project_path.join(F_TXT)), Some(GitFileStatus::Added) ); - assert_eq!(snapshot.status_for_file(project_path.join(B_TXT)), None); assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None); }); + // Modify files in the working copy and perform git operations on other files. git_reset(0, &repo); git_remove_index(Path::new(B_TXT), &repo); git_stash(&mut repo); diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index 55efc09deb..33606fccc4 100644 --- a/crates/project_panel/Cargo.toml +++ b/crates/project_panel/Cargo.toml @@ -27,6 +27,7 @@ serde_derive.workspace = true serde_json.workspace = true anyhow.workspace = true schemars.workspace = true +pretty_assertions.workspace = true unicase = "2.6" [dev-dependencies] diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 3f80e02317..c329ae4e51 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -64,7 +64,7 @@ pub struct ProjectPanel { pending_serialization: Task>, } -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug)] struct Selection { worktree_id: WorktreeId, entry_id: ProjectEntryId, @@ -547,7 +547,7 @@ impl ProjectPanel { worktree_id, entry_id: NEW_ENTRY_ID, }); - let new_path = entry.path.join(&filename); + let new_path = entry.path.join(&filename.trim_start_matches("/")); if path_already_exists(new_path.as_path()) { return None; } @@ -588,6 +588,7 @@ impl ProjectPanel { if selection.entry_id == edited_entry_id { selection.worktree_id = worktree_id; selection.entry_id = new_entry.id; + this.expand_to_selection(cx); } } this.update_visible_entries(None, cx); @@ -965,6 +966,24 @@ impl ProjectPanel { Some((worktree, entry)) } + fn expand_to_selection(&mut self, cx: &mut ViewContext) -> Option<()> { + let (worktree, entry) = self.selected_entry(cx)?; + let expanded_dir_ids = self.expanded_dir_ids.entry(worktree.id()).or_default(); + + for path in entry.path.ancestors() { + let Some(entry) = worktree.entry_for_path(path) else { + continue; + }; + if entry.is_dir() { + if let Err(idx) = expanded_dir_ids.binary_search(&entry.id) { + expanded_dir_ids.insert(idx, entry.id); + } + } + } + + Some(()) + } + fn update_visible_entries( &mut self, new_selected_entry: Option<(WorktreeId, ProjectEntryId)>, @@ -1592,6 +1611,7 @@ impl ClipboardEntry { mod tests { use super::*; use gpui::{TestAppContext, ViewHandle}; + use pretty_assertions::assert_eq; use project::FakeFs; use serde_json::json; use settings::SettingsStore; @@ -2002,6 +2022,133 @@ mod tests { ); } + #[gpui::test(iterations = 30)] + async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/root1", + json!({ + ".dockerignore": "", + ".git": { + "HEAD": "", + }, + "a": { + "0": { "q": "", "r": "", "s": "" }, + "1": { "t": "", "u": "" }, + "2": { "v": "", "w": "", "x": "", "y": "" }, + }, + "b": { + "3": { "Q": "" }, + "4": { "R": "", "S": "", "T": "", "U": "" }, + }, + "C": { + "5": {}, + "6": { "V": "", "W": "" }, + "7": { "X": "" }, + "8": { "Y": {}, "Z": "" } + } + }), + ) + .await; + fs.insert_tree( + "/root2", + json!({ + "d": { + "9": "" + }, + "e": {} + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; + let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + + select_path(&panel, "root1", cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1 <== selected", + " > .git", + " > a", + " > b", + " > C", + " .dockerignore", + "v root2", + " > d", + " > e", + ] + ); + + // Add a file with the root folder selected. The filename editor is placed + // before the first file in the root folder. + panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); + cx.read_window(window_id, |cx| { + let panel = panel.read(cx); + assert!(panel.filename_editor.is_focused(cx)); + }); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > .git", + " > a", + " > b", + " > C", + " [EDITOR: ''] <== selected", + " .dockerignore", + "v root2", + " > d", + " > e", + ] + ); + + let confirm = panel.update(cx, |panel, cx| { + panel.filename_editor.update(cx, |editor, cx| { + editor.set_text("/bdir1/dir2/the-new-filename", cx) + }); + panel.confirm(&Confirm, cx).unwrap() + }); + + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > .git", + " > a", + " > b", + " > C", + " [PROCESSING: '/bdir1/dir2/the-new-filename'] <== selected", + " .dockerignore", + "v root2", + " > d", + " > e", + ] + ); + + confirm.await.unwrap(); + assert_eq!( + visible_entries_as_strings(&panel, 0..13, cx), + &[ + "v root1", + " > .git", + " > a", + " > b", + " v bdir1", + " v dir2", + " the-new-filename <== selected", + " > C", + " .dockerignore", + "v root2", + " > d", + " > e", + ] + ); + } + #[gpui::test] async fn test_copy_paste(cx: &mut gpui::TestAppContext) { init_test(cx); diff --git a/crates/recent_projects/Cargo.toml b/crates/recent_projects/Cargo.toml index 14f8853c9c..51774e8feb 100644 --- a/crates/recent_projects/Cargo.toml +++ b/crates/recent_projects/Cargo.toml @@ -21,6 +21,7 @@ util = { path = "../util"} theme = { path = "../theme" } workspace = { path = "../workspace" } +futures.workspace = true ordered-float.workspace = true postage.workspace = true smol.workspace = true diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index b13f72da0b..4ba6103167 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -48,7 +48,7 @@ fn toggle( let workspace = cx.weak_handle(); cx.add_view(|cx| { RecentProjects::new( - RecentProjectsDelegate::new(workspace, workspace_locations), + RecentProjectsDelegate::new(workspace, workspace_locations, true), cx, ) .with_max_size(800., 1200.) @@ -64,25 +64,40 @@ fn toggle( })) } -type RecentProjects = Picker; +pub fn build_recent_projects( + workspace: WeakViewHandle, + workspaces: Vec, + cx: &mut ViewContext, +) -> RecentProjects { + Picker::new( + RecentProjectsDelegate::new(workspace, workspaces, false), + cx, + ) + .with_theme(|theme| theme.picker.clone()) +} -struct RecentProjectsDelegate { +pub type RecentProjects = Picker; + +pub struct RecentProjectsDelegate { workspace: WeakViewHandle, workspace_locations: Vec, selected_match_index: usize, matches: Vec, + render_paths: bool, } impl RecentProjectsDelegate { fn new( workspace: WeakViewHandle, workspace_locations: Vec, + render_paths: bool, ) -> Self { Self { workspace, workspace_locations, selected_match_index: 0, matches: Default::default(), + render_paths, } } } @@ -188,6 +203,7 @@ impl PickerDelegate for RecentProjectsDelegate { highlighted_location .paths .into_iter() + .filter(|_| self.render_paths) .map(|highlighted_path| highlighted_path.render(style.label.clone())), ) .flex(1., false) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 135194df6a..ebd504d02c 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -675,6 +675,9 @@ impl ProjectSearchView { if match_ranges.is_empty() { self.active_match_index = None; } else { + self.active_match_index = Some(0); + self.select_match(Direction::Next, cx); + self.update_match_index(cx); let prev_search_id = mem::replace(&mut self.search_id, self.model.read(cx).search_id); let is_new_search = self.search_id != prev_search_id; self.results_editor.update(cx, |editor, cx| { diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index b5dc301a5c..06b81a0c61 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -38,5 +38,5 @@ tree-sitter-json = "*" gpui = { path = "../gpui", features = ["test-support"] } fs = { path = "../fs", features = ["test-support"] } indoc.workspace = true -pretty_assertions = "1.3.0" +pretty_assertions.workspace = true unindent.workspace = true diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index d2e656ebe3..93cb2ab3d7 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -1,7 +1,7 @@ use crate::{settings_store::parse_json_with_comments, SettingsAssets}; use anyhow::{anyhow, Context, Result}; use collections::BTreeMap; -use gpui::{keymap_matcher::Binding, AppContext}; +use gpui::{keymap_matcher::Binding, AppContext, NoAction}; use schemars::{ gen::{SchemaGenerator, SchemaSettings}, schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation}, @@ -11,18 +11,18 @@ use serde::Deserialize; use serde_json::Value; use util::{asset_str, ResultExt}; -#[derive(Deserialize, Default, Clone, JsonSchema)] +#[derive(Debug, Deserialize, Default, Clone, JsonSchema)] #[serde(transparent)] pub struct KeymapFile(Vec); -#[derive(Deserialize, Default, Clone, JsonSchema)] +#[derive(Debug, Deserialize, Default, Clone, JsonSchema)] pub struct KeymapBlock { #[serde(default)] context: Option, bindings: BTreeMap, } -#[derive(Deserialize, Default, Clone)] +#[derive(Debug, Deserialize, Default, Clone)] #[serde(transparent)] pub struct KeymapAction(Value); @@ -61,21 +61,22 @@ impl KeymapFile { // We want to deserialize the action data as a `RawValue` so that we can // deserialize the action itself dynamically directly from the JSON // string. But `RawValue` currently does not work inside of an untagged enum. - if let Value::Array(items) = action { - let Ok([name, data]): Result<[serde_json::Value; 2], _> = items.try_into() else { - return Some(Err(anyhow!("Expected array of length 2"))); - }; - let serde_json::Value::String(name) = name else { - return Some(Err(anyhow!("Expected first item in array to be a string."))) - }; - cx.deserialize_action( - &name, - Some(data), - ) - } else if let Value::String(name) = action { - cx.deserialize_action(&name, None) - } else { - return Some(Err(anyhow!("Expected two-element array, got {:?}", action))); + match action { + Value::Array(items) => { + let Ok([name, data]): Result<[serde_json::Value; 2], _> = items.try_into() else { + return Some(Err(anyhow!("Expected array of length 2"))); + }; + let serde_json::Value::String(name) = name else { + return Some(Err(anyhow!("Expected first item in array to be a string."))) + }; + cx.deserialize_action( + &name, + Some(data), + ) + }, + Value::String(name) => cx.deserialize_action(&name, None), + Value::Null => Ok(no_action()), + _ => return Some(Err(anyhow!("Expected two-element array, got {action:?}"))), } .with_context(|| { format!( @@ -115,6 +116,10 @@ impl KeymapFile { instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Array))), ..Default::default() }), + Schema::Object(SchemaObject { + instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Null))), + ..Default::default() + }), ]), ..Default::default() })), @@ -129,6 +134,10 @@ impl KeymapFile { } } +fn no_action() -> Box { + Box::new(NoAction {}) +} + #[cfg(test)] mod tests { use crate::KeymapFile; diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 2f2ff2cdc3..b92059f5d6 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -395,16 +395,17 @@ impl TerminalElement { // Terminal Emulator controlled behavior: region = region // Start selections - .on_down( - MouseButton::Left, - TerminalElement::generic_button_handler( - connection, - origin, - move |terminal, origin, e, _cx| { - terminal.mouse_down(&e, origin); - }, - ), - ) + .on_down(MouseButton::Left, move |event, v: &mut TerminalView, cx| { + cx.focus_parent(); + v.context_menu.update(cx, |menu, _cx| menu.delay_cancel()); + if let Some(conn_handle) = connection.upgrade(cx) { + conn_handle.update(cx, |terminal, cx| { + terminal.mouse_down(&event, origin); + + cx.notify(); + }) + } + }) // Update drag selections .on_drag(MouseButton::Left, move |event, _: &mut TerminalView, cx| { if cx.is_self_focused() { diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 6de6527a26..11f8f7abde 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -87,6 +87,7 @@ impl TerminalPanel { } }) }, + |_, _| {}, None, )) .with_child(Pane::render_tab_bar_button( @@ -100,6 +101,7 @@ impl TerminalPanel { Some(("Toggle Zoom".into(), Some(Box::new(workspace::ToggleZoom)))), cx, move |pane, cx| pane.toggle_zoom(&Default::default(), cx), + |_, _| {}, None, )) .into_any() diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 28c5125de2..7c94f25e1e 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -2489,7 +2489,12 @@ impl ToOffset for Point { impl ToOffset for usize { fn to_offset(&self, snapshot: &BufferSnapshot) -> usize { - assert!(*self <= snapshot.len(), "offset {self} is out of range"); + assert!( + *self <= snapshot.len(), + "offset {} is out of range, max allowed is {}", + self, + snapshot.len() + ); *self } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 01da555e1e..fae7c470e3 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -65,7 +65,6 @@ pub struct Theme { pub assistant: AssistantStyle, pub feedback: FeedbackStyle, pub welcome: WelcomeStyle, - pub color_scheme: ColorScheme, pub titlebar: Titlebar, } @@ -118,8 +117,9 @@ pub struct Titlebar { #[serde(flatten)] pub container: ContainerStyle, pub height: f32, - pub title: TextStyle, - pub highlight_color: Color, + pub project_menu_button: Toggleable>, + pub project_name_divider: ContainedText, + pub git_menu_button: Toggleable>, pub item_spacing: f32, pub face_pile_spacing: f32, pub avatar_ribbon: AvatarRibbon, @@ -585,6 +585,8 @@ pub struct Picker { pub empty_input_editor: FieldEditor, pub no_matches: ContainedLabel, pub item: Toggleable>, + pub header: ContainedLabel, + pub footer: ContainedLabel, } #[derive(Clone, Debug, Deserialize, Default, JsonSchema)] @@ -720,6 +722,7 @@ pub struct Scrollbar { pub width: f32, pub min_height_factor: f32, pub git: GitDiffColors, + pub selections: Color, } #[derive(Clone, Deserialize, Default, JsonSchema)] diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index 57d3821379..47a85f4ed3 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -36,7 +36,6 @@ workspace = { path = "../workspace" } [dev-dependencies] indoc.workspace = true parking_lot.workspace = true -lazy_static.workspace = true editor = { path = "../editor", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index faf69d9473..07b095dd5e 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -31,6 +31,8 @@ pub enum Motion { CurrentLine, StartOfLine, EndOfLine, + StartOfParagraph, + EndOfParagraph, StartOfDocument, EndOfDocument, Matching, @@ -72,6 +74,8 @@ actions!( StartOfLine, EndOfLine, CurrentLine, + StartOfParagraph, + EndOfParagraph, StartOfDocument, EndOfDocument, Matching, @@ -92,6 +96,12 @@ pub fn init(cx: &mut AppContext) { cx.add_action(|_: &mut Workspace, _: &StartOfLine, cx: _| motion(Motion::StartOfLine, cx)); cx.add_action(|_: &mut Workspace, _: &EndOfLine, cx: _| motion(Motion::EndOfLine, cx)); cx.add_action(|_: &mut Workspace, _: &CurrentLine, cx: _| motion(Motion::CurrentLine, cx)); + cx.add_action(|_: &mut Workspace, _: &StartOfParagraph, cx: _| { + motion(Motion::StartOfParagraph, cx) + }); + cx.add_action(|_: &mut Workspace, _: &EndOfParagraph, cx: _| { + motion(Motion::EndOfParagraph, cx) + }); cx.add_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| { motion(Motion::StartOfDocument, cx) }); @@ -142,7 +152,8 @@ impl Motion { pub fn linewise(&self) -> bool { use Motion::*; match self { - Down | Up | StartOfDocument | EndOfDocument | CurrentLine | NextLineStart => true, + Down | Up | StartOfDocument | EndOfDocument | CurrentLine | NextLineStart + | StartOfParagraph | EndOfParagraph => true, EndOfLine | NextWordEnd { .. } | Matching @@ -172,6 +183,8 @@ impl Motion { | Backspace | Right | StartOfLine + | StartOfParagraph + | EndOfParagraph | NextWordStart { .. } | PreviousWordStart { .. } | FirstNonWhitespace @@ -197,6 +210,8 @@ impl Motion { | Backspace | Right | StartOfLine + | StartOfParagraph + | EndOfParagraph | NextWordStart { .. } | PreviousWordStart { .. } | FirstNonWhitespace @@ -235,6 +250,14 @@ impl Motion { FirstNonWhitespace => (first_non_whitespace(map, point), SelectionGoal::None), StartOfLine => (start_of_line(map, point), SelectionGoal::None), EndOfLine => (end_of_line(map, point), SelectionGoal::None), + StartOfParagraph => ( + movement::start_of_paragraph(map, point, times), + SelectionGoal::None, + ), + EndOfParagraph => ( + map.clip_at_line_end(movement::end_of_paragraph(map, point, times)), + SelectionGoal::None, + ), CurrentLine => (end_of_line(map, point), SelectionGoal::None), StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None), EndOfDocument => ( @@ -502,10 +525,13 @@ fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint if line_end == point { line_end = map.max_point().to_point(map); } - line_end.column = line_end.column.saturating_sub(1); let line_range = map.prev_line_boundary(point).0..line_end; - let ranges = map.buffer_snapshot.bracket_ranges(line_range.clone()); + let visible_line_range = + line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1)); + let ranges = map + .buffer_snapshot + .bracket_ranges(visible_line_range.clone()); if let Some(ranges) = ranges { let line_range = line_range.start.to_offset(&map.buffer_snapshot) ..line_range.end.to_offset(&map.buffer_snapshot); @@ -590,3 +616,131 @@ fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> let new_row = (point.row() + times as u32).min(map.max_buffer_row()); map.clip_point(DisplayPoint::new(new_row, 0), Bias::Left) } + +#[cfg(test)] + +mod test { + + use crate::test::NeovimBackedTestContext; + use indoc::indoc; + + #[gpui::test] + async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + let initial_state = indoc! {r"ˇabc + def + + paragraph + the second + + + + third and + final"}; + + // goes down once + cx.set_shared_state(initial_state).await; + cx.simulate_shared_keystrokes(["}"]).await; + cx.assert_shared_state(indoc! {r"abc + def + ˇ + paragraph + the second + + + + third and + final"}) + .await; + + // goes up once + cx.simulate_shared_keystrokes(["{"]).await; + cx.assert_shared_state(initial_state).await; + + // goes down twice + cx.simulate_shared_keystrokes(["2", "}"]).await; + cx.assert_shared_state(indoc! {r"abc + def + + paragraph + the second + ˇ + + + third and + final"}) + .await; + + // goes down over multiple blanks + cx.simulate_shared_keystrokes(["}"]).await; + cx.assert_shared_state(indoc! {r"abc + def + + paragraph + the second + + + + third and + finaˇl"}) + .await; + + // goes up twice + cx.simulate_shared_keystrokes(["2", "{"]).await; + cx.assert_shared_state(indoc! {r"abc + def + ˇ + paragraph + the second + + + + third and + final"}) + .await + } + + #[gpui::test] + async fn test_matching(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! {r"func ˇ(a string) { + do(something(with.and_arrays[0, 2])) + }"}) + .await; + cx.simulate_shared_keystrokes(["%"]).await; + cx.assert_shared_state(indoc! {r"func (a stringˇ) { + do(something(with.and_arrays[0, 2])) + }"}) + .await; + + // test it works on the last character of the line + cx.set_shared_state(indoc! {r"func (a string) ˇ{ + do(something(with.and_arrays[0, 2])) + }"}) + .await; + cx.simulate_shared_keystrokes(["%"]).await; + cx.assert_shared_state(indoc! {r"func (a string) { + do(something(with.and_arrays[0, 2])) + ˇ}"}) + .await; + + // test it works on immediate nesting + cx.set_shared_state("ˇ{()}").await; + cx.simulate_shared_keystrokes(["%"]).await; + cx.assert_shared_state("{()ˇ}").await; + cx.simulate_shared_keystrokes(["%"]).await; + cx.assert_shared_state("ˇ{()}").await; + + // test it works on immediate nesting inside braces + cx.set_shared_state("{\n ˇ{()}\n}").await; + cx.simulate_shared_keystrokes(["%"]).await; + cx.assert_shared_state("{\n {()ˇ}\n}").await; + + // test it jumps to the next paren on a line + cx.set_shared_state("func ˇboop() {\n}").await; + cx.simulate_shared_keystrokes(["%"]).await; + cx.assert_shared_state("func boop(ˇ) {\n}").await; + } +} diff --git a/crates/vim/src/normal/case.rs b/crates/vim/src/normal/case.rs index ba527af0bb..b3e101262d 100644 --- a/crates/vim/src/normal/case.rs +++ b/crates/vim/src/normal/case.rs @@ -1,29 +1,51 @@ +use editor::scroll::autoscroll::Autoscroll; use gpui::ViewContext; -use language::Point; +use language::{Bias, Point}; use workspace::Workspace; -use crate::{motion::Motion, normal::ChangeCase, Vim}; +use crate::{normal::ChangeCase, state::Mode, Vim}; pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { - let count = vim.pop_number_operator(cx); + let count = vim.pop_number_operator(cx).unwrap_or(1) as u32; vim.update_active_editor(cx, |editor, cx| { - editor.set_clip_at_line_ends(false, cx); - editor.transact(cx, |editor, cx| { - editor.change_selections(None, cx, |s| { - s.move_with(|map, selection| { - if selection.start == selection.end { - Motion::Right.expand_selection(map, selection, count, true); + let mut ranges = Vec::new(); + let mut cursor_positions = Vec::new(); + let snapshot = editor.buffer().read(cx).snapshot(cx); + for selection in editor.selections.all::(cx) { + match vim.state.mode { + Mode::Visual { line: true } => { + let start = Point::new(selection.start.row, 0); + let end = + Point::new(selection.end.row, snapshot.line_len(selection.end.row)); + ranges.push(start..end); + cursor_positions.push(start..start); + } + Mode::Visual { line: false } => { + ranges.push(selection.start..selection.end); + cursor_positions.push(selection.start..selection.start); + } + Mode::Insert | Mode::Normal => { + let start = selection.start; + let mut end = start; + for _ in 0..count { + end = snapshot.clip_point(end + Point::new(0, 1), Bias::Right); } - }) - }); - let selections = editor.selections.all::(cx); - for selection in selections.into_iter().rev() { + ranges.push(start..end); + + if end.column == snapshot.line_len(end.row) { + end = snapshot.clip_point(end - Point::new(0, 1), Bias::Left); + } + cursor_positions.push(end..end) + } + } + } + editor.transact(cx, |editor, cx| { + for range in ranges.into_iter().rev() { let snapshot = editor.buffer().read(cx).snapshot(cx); editor.buffer().update(cx, |buffer, cx| { - let range = selection.start..selection.end; let text = snapshot - .text_for_range(selection.start..selection.end) + .text_for_range(range.start..range.end) .flat_map(|s| s.chars()) .flat_map(|c| { if c.is_lowercase() { @@ -37,28 +59,46 @@ pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext", ">"]); cx.assert_editor_state("aa\n b«b\n cˇ»c"); } + +#[gpui::test] +async fn test_escape_command_palette(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.set_state("aˇbc\n", Mode::Normal); + cx.simulate_keystrokes(["i", "cmd-shift-p"]); + + assert!(cx.workspace(|workspace, _| workspace.modal::().is_some())); + cx.simulate_keystroke("escape"); + assert!(!cx.workspace(|workspace, _| workspace.modal::().is_some())); + cx.assert_state("aˇbc\n", Mode::Insert); +} diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index 9b6bf976ca..7f9a84b666 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -1,9 +1,10 @@ -use std::ops::{Deref, DerefMut}; +use indoc::indoc; +use std::ops::{Deref, DerefMut, Range}; use collections::{HashMap, HashSet}; use gpui::ContextHandle; use language::OffsetRangeExt; -use util::test::marked_text_offsets; +use util::test::{generate_marked_text, marked_text_offsets}; use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext}; use crate::state::Mode; @@ -112,6 +113,43 @@ impl<'a> NeovimBackedTestContext<'a> { context_handle } + pub async fn assert_shared_state(&mut self, marked_text: &str) { + let neovim = self.neovim_state().await; + if neovim != marked_text { + panic!( + indoc! {"Test is incorrect (currently expected != neovim state) + + # currently expected: + {} + # neovim state: + {} + # zed state: + {}"}, + marked_text, + neovim, + self.editor_state(), + ) + } + self.assert_editor_state(marked_text) + } + + pub async fn neovim_state(&mut self) -> String { + generate_marked_text( + self.neovim.text().await.as_str(), + &vec![self.neovim_selection().await], + true, + ) + } + + async fn neovim_selection(&mut self) -> Range { + let mut neovim_selection = self.neovim.selection().await; + // Zed selections adjust themselves to make the end point visually make sense + if neovim_selection.start > neovim_selection.end { + neovim_selection.start.column += 1; + } + neovim_selection.to_offset(&self.buffer_snapshot()) + } + pub async fn assert_state_matches(&mut self) { assert_eq!( self.neovim.text().await, @@ -120,13 +158,8 @@ impl<'a> NeovimBackedTestContext<'a> { self.assertion_context() ); - let mut neovim_selection = self.neovim.selection().await; - // Zed selections adjust themselves to make the end point visually make sense - if neovim_selection.start > neovim_selection.end { - neovim_selection.start.column += 1; - } - let neovim_selection = neovim_selection.to_offset(&self.buffer_snapshot()); - self.assert_editor_selections(vec![neovim_selection]); + let selections = vec![self.neovim_selection().await]; + self.assert_editor_selections(selections); if let Some(neovim_mode) = self.neovim.mode().await { assert_eq!(neovim_mode, self.mode(), "{}", self.assertion_context(),); diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index c3916722dd..5bfae4e673 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -11,8 +11,6 @@ use gpui::keymap_matcher::Keystroke; use language::Point; -#[cfg(feature = "neovim")] -use lazy_static::lazy_static; #[cfg(feature = "neovim")] use nvim_rs::{ create::tokio::new_child_cmd, error::LoopError, Handler, Neovim, UiAttachOptions, Value, @@ -32,9 +30,7 @@ use collections::VecDeque; // Neovim doesn't like to be started simultaneously from multiple threads. We use this lock // to ensure we are only constructing one neovim connection at a time. #[cfg(feature = "neovim")] -lazy_static! { - static ref NEOVIM_LOCK: ReentrantMutex<()> = ReentrantMutex::new(()); -} +static NEOVIM_LOCK: ReentrantMutex<()> = ReentrantMutex::new(()); #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub enum NeovimData { @@ -171,15 +167,25 @@ impl NeovimConnection { .await .expect("Could not get neovim window"); - if !selection.is_empty() { - panic!("Setting neovim state with non empty selection not yet supported"); - } let cursor = selection.start; nvim_window .set_cursor((cursor.row as i64 + 1, cursor.column as i64)) .await .expect("Could not set nvim cursor position"); + if !selection.is_empty() { + self.nvim + .input("v") + .await + .expect("could not enter visual mode"); + + let cursor = selection.end; + nvim_window + .set_cursor((cursor.row as i64 + 1, cursor.column as i64)) + .await + .expect("Could not set nvim cursor position"); + } + if let Some(NeovimData::Get { mode, state }) = self.data.back() { if *mode == Some(Mode::Normal) && *state == marked_text { return; diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index 3e66d6bb1c..f9ba577231 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -21,12 +21,14 @@ impl<'a> VimTestContext<'a> { cx.update(|cx| { search::init(cx); crate::init(cx); + command_palette::init(cx); }); cx.update(|cx| { cx.update_global(|store: &mut SettingsStore, cx| { store.update_user_settings::(cx, |s| *s = Some(enabled)); }); + settings::KeymapFile::load_asset("keymaps/default.json", cx).unwrap(); settings::KeymapFile::load_asset("keymaps/vim.json", cx).unwrap(); }); diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index eae8643cf3..2bcc2254ee 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -12,7 +12,7 @@ mod visual; use anyhow::Result; use collections::CommandPaletteFilter; -use editor::{Bias, Cancel, Editor, EditorMode, Event}; +use editor::{Bias, Editor, EditorMode, Event}; use gpui::{ actions, impl_actions, AppContext, Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext, @@ -64,22 +64,6 @@ pub fn init(cx: &mut AppContext) { Vim::update(cx, |vim, cx| vim.push_number(n, cx)); }); - // Editor Actions - cx.add_action(|_: &mut Editor, _: &Cancel, cx| { - // If we are in aren't in normal mode or have an active operator, swap to normal mode - // Otherwise forward cancel on to the editor - let vim = Vim::read(cx); - if vim.state.mode != Mode::Normal || vim.active_operator().is_some() { - WindowContext::defer(cx, |cx| { - Vim::update(cx, |state, cx| { - state.switch_mode(Mode::Normal, false, cx); - }); - }); - } else { - cx.propagate_action(); - } - }); - cx.add_action(|_: &mut Workspace, _: &Tab, cx| { Vim::active_editor_input_ignored(" ".into(), cx) }); @@ -109,10 +93,7 @@ pub fn observe_keystrokes(cx: &mut WindowContext) { cx.observe_keystrokes(|_keystroke, _result, handled_by, cx| { if let Some(handled_by) = handled_by { // Keystroke is handled by the vim system, so continue forward - // Also short circuit if it is the special cancel action - if handled_by.namespace() == "vim" - || (handled_by.namespace() == "editor" && handled_by.name() == "Cancel") - { + if handled_by.namespace() == "vim" { return true; } } diff --git a/crates/vim/test_data/test_change_case.json b/crates/vim/test_data/test_change_case.json new file mode 100644 index 0000000000..1c0cad0b93 --- /dev/null +++ b/crates/vim/test_data/test_change_case.json @@ -0,0 +1,18 @@ +{"Put":{"state":"ˇabC\n"}} +{"Key":"~"} +{"Get":{"state":"AˇbC\n","mode":"Normal"}} +{"Key":"2"} +{"Key":"~"} +{"Get":{"state":"ABˇc\n","mode":"Normal"}} +{"Put":{"state":"a😀C«dÉ1*fˇ»\n"}} +{"Key":"~"} +{"Get":{"state":"a😀CˇDé1*F\n","mode":"Normal"}} +{"Key":"~"} +{"Put":{"state":"aˇC😀é1*F\n"}} +{"Key":"4"} +{"Key":"~"} +{"Get":{"state":"ac😀É1ˇ*F\n","mode":"Normal"}} +{"Put":{"state":"abˇC\n"}} +{"Key":"shift-v"} +{"Key":"~"} +{"Get":{"state":"ˇABc\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_matching.json b/crates/vim/test_data/test_matching.json new file mode 100644 index 0000000000..5c8d7529b9 --- /dev/null +++ b/crates/vim/test_data/test_matching.json @@ -0,0 +1,17 @@ +{"Put":{"state":"func ˇ(a string) {\n do(something(with.and_arrays[0, 2]))\n}"}} +{"Key":"%"} +{"Get":{"state":"func (a stringˇ) {\n do(something(with.and_arrays[0, 2]))\n}","mode":"Normal"}} +{"Put":{"state":"func (a string) ˇ{\ndo(something(with.and_arrays[0, 2]))\n}"}} +{"Key":"%"} +{"Get":{"state":"func (a string) {\ndo(something(with.and_arrays[0, 2]))\nˇ}","mode":"Normal"}} +{"Put":{"state":"ˇ{()}"}} +{"Key":"%"} +{"Get":{"state":"{()ˇ}","mode":"Normal"}} +{"Key":"%"} +{"Get":{"state":"ˇ{()}","mode":"Normal"}} +{"Put":{"state":"{\n ˇ{()}\n}"}} +{"Key":"%"} +{"Get":{"state":"{\n {()ˇ}\n}","mode":"Normal"}} +{"Put":{"state":"func ˇboop() {\n}"}} +{"Key":"%"} +{"Get":{"state":"func boop(ˇ) {\n}","mode":"Normal"}} diff --git a/crates/vim/test_data/test_start_end_of_paragraph.json b/crates/vim/test_data/test_start_end_of_paragraph.json new file mode 100644 index 0000000000..0de4d84f50 --- /dev/null +++ b/crates/vim/test_data/test_start_end_of_paragraph.json @@ -0,0 +1,13 @@ +{"Put":{"state":"ˇabc\ndef\n\nparagraph\nthe second\n\n\n\nthird and\nfinal"}} +{"Key":"}"} +{"Get":{"state":"abc\ndef\nˇ\nparagraph\nthe second\n\n\n\nthird and\nfinal","mode":"Normal"}} +{"Key":"{"} +{"Get":{"state":"ˇabc\ndef\n\nparagraph\nthe second\n\n\n\nthird and\nfinal","mode":"Normal"}} +{"Key":"2"} +{"Key":"}"} +{"Get":{"state":"abc\ndef\n\nparagraph\nthe second\nˇ\n\n\nthird and\nfinal","mode":"Normal"}} +{"Key":"}"} +{"Get":{"state":"abc\ndef\n\nparagraph\nthe second\n\n\n\nthird and\nfinaˇl","mode":"Normal"}} +{"Key":"2"} +{"Key":"{"} +{"Get":{"state":"abc\ndef\nˇ\nparagraph\nthe second\n\n\n\nthird and\nfinal","mode":"Normal"}} diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 9776fede2c..6a20fab9a2 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -273,6 +273,11 @@ impl Pane { Some(("New...".into(), None)), cx, |pane, cx| pane.deploy_new_menu(cx), + |pane, cx| { + pane.tab_bar_context_menu + .handle + .update(cx, |menu, _| menu.delay_cancel()) + }, pane.tab_bar_context_menu .handle_if_kind(TabBarContextMenuKind::New), )) @@ -283,6 +288,11 @@ impl Pane { Some(("Split Pane".into(), None)), cx, |pane, cx| pane.deploy_split_menu(cx), + |pane, cx| { + pane.tab_bar_context_menu + .handle + .update(cx, |menu, _| menu.delay_cancel()) + }, pane.tab_bar_context_menu .handle_if_kind(TabBarContextMenuKind::Split), )) @@ -304,6 +314,7 @@ impl Pane { Some((tooltip_label, Some(Box::new(ToggleZoom)))), cx, move |pane, cx| pane.toggle_zoom(&Default::default(), cx), + move |_, _| {}, None, ) }) @@ -988,7 +999,7 @@ impl Pane { fn deploy_split_menu(&mut self, cx: &mut ViewContext) { self.tab_bar_context_menu.handle.update(cx, |menu, cx| { - menu.show( + menu.toggle( Default::default(), AnchorCorner::TopRight, vec![ @@ -1006,7 +1017,7 @@ impl Pane { fn deploy_new_menu(&mut self, cx: &mut ViewContext) { self.tab_bar_context_menu.handle.update(cx, |menu, cx| { - menu.show( + menu.toggle( Default::default(), AnchorCorner::TopRight, vec![ @@ -1416,13 +1427,17 @@ impl Pane { .into_any() } - pub fn render_tab_bar_button)>( + pub fn render_tab_bar_button< + F1: 'static + Fn(&mut Pane, &mut EventContext), + F2: 'static + Fn(&mut Pane, &mut EventContext), + >( index: usize, icon: &'static str, is_active: bool, tooltip: Option<(String, Option>)>, cx: &mut ViewContext, - on_click: F, + on_click: F1, + on_down: F2, context_menu: Option>, ) -> AnyElement { enum TabBarButton {} @@ -1440,6 +1455,7 @@ impl Pane { .with_height(style.button_width) }) .with_cursor_style(CursorStyle::PointingHand) + .on_down(MouseButton::Left, move |_, pane, cx| on_down(pane, cx)) .on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx)) .into_any(); if let Some((tooltip, action)) = tooltip { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 066ea5f8a6..01d80d141c 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -97,9 +97,25 @@ lazy_static! { } pub trait Modal: View { + fn has_focus(&self) -> bool; fn dismiss_on_event(event: &Self::Event) -> bool; } +trait ModalHandle { + fn as_any(&self) -> &AnyViewHandle; + fn has_focus(&self, cx: &WindowContext) -> bool; +} + +impl ModalHandle for ViewHandle { + fn as_any(&self) -> &AnyViewHandle { + self + } + + fn has_focus(&self, cx: &WindowContext) -> bool { + self.read(cx).has_focus() + } +} + #[derive(Clone, PartialEq)] pub struct RemoveWorktreeFromProject(pub WorktreeId); @@ -466,7 +482,7 @@ pub enum Event { pub struct Workspace { weak_self: WeakViewHandle, remote_entity_subscription: Option, - modal: Option, + modal: Option, zoomed: Option, zoomed_position: Option, center: PaneGroup, @@ -495,6 +511,11 @@ pub struct Workspace { pane_history_timestamp: Arc, } +struct ActiveModal { + view: Box, + previously_focused_view_id: Option, +} + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub struct ViewId { pub creator: PeerId, @@ -1482,8 +1503,10 @@ impl Workspace { cx.notify(); // Whatever modal was visible is getting clobbered. If its the same type as V, then return // it. Otherwise, create a new modal and set it as active. - let already_open_modal = self.modal.take().and_then(|modal| modal.downcast::()); - if let Some(already_open_modal) = already_open_modal { + if let Some(already_open_modal) = self + .dismiss_modal(cx) + .and_then(|modal| modal.downcast::()) + { cx.focus_self(); Some(already_open_modal) } else { @@ -1494,8 +1517,12 @@ impl Workspace { } }) .detach(); + let previously_focused_view_id = cx.focused_view_id(); cx.focus(&modal); - self.modal = Some(modal.into_any()); + self.modal = Some(ActiveModal { + view: Box::new(modal), + previously_focused_view_id, + }); None } } @@ -1503,13 +1530,20 @@ impl Workspace { pub fn modal(&self) -> Option> { self.modal .as_ref() - .and_then(|modal| modal.clone().downcast::()) + .and_then(|modal| modal.view.as_any().clone().downcast::()) } - pub fn dismiss_modal(&mut self, cx: &mut ViewContext) { - if self.modal.take().is_some() { - cx.focus(&self.active_pane); + pub fn dismiss_modal(&mut self, cx: &mut ViewContext) -> Option { + if let Some(modal) = self.modal.take() { + if let Some(previously_focused_view_id) = modal.previously_focused_view_id { + if modal.view.has_focus(cx) { + cx.window_context().focus(Some(previously_focused_view_id)); + } + } cx.notify(); + Some(modal.view.as_any().clone()) + } else { + None } } @@ -3496,7 +3530,7 @@ impl View for Workspace { ) })) .with_children(self.modal.as_ref().map(|modal| { - ChildView::new(modal, cx) + ChildView::new(modal.view.as_any(), cx) .contained() .with_style(theme.workspace.modal) .aligned() @@ -4775,6 +4809,7 @@ mod tests { theme::init((), cx); language::init(cx); crate::init_settings(cx); + Project::init_settings(cx); }); } } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 6434d36951..d016525af4 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.94.0" +version = "0.95.0" publish = false [lib] @@ -16,6 +16,7 @@ name = "Zed" path = "src/main.rs" [dependencies] +audio = { path = "../audio" } activity_indicator = { path = "../activity_indicator" } auto_update = { path = "../auto_update" } breadcrumbs = { path = "../breadcrumbs" } diff --git a/crates/zed/src/assets.rs b/crates/zed/src/assets.rs index 6eb8a44f0f..574016c25d 100644 --- a/crates/zed/src/assets.rs +++ b/crates/zed/src/assets.rs @@ -7,6 +7,7 @@ use rust_embed::RustEmbed; #[include = "fonts/**/*"] #[include = "icons/**/*"] #[include = "themes/**/*"] +#[include = "sounds/**/*"] #[include = "*.md"] #[exclude = "*.DS_Store"] pub struct Assets; diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 31a00cb916..5eed301367 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -57,8 +57,9 @@ use staff_mode::StaffMode; use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt}; use workspace::{item::ItemHandle, notifications::NotifyResultExt, AppState, Workspace}; use zed::{ - assets::Assets, build_window_options, handle_keymap_file_changes, initialize_workspace, - languages, menus, + assets::Assets, + build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus, + only_instance::{ensure_only_instance, IsOnlyInstance}, }; fn main() { @@ -66,6 +67,10 @@ fn main() { init_paths(); init_logger(); + if ensure_only_instance() != IsOnlyInstance::Yes { + return; + } + log::info!("========== starting zed =========="); let mut app = gpui::App::new(Assets).unwrap(); @@ -180,6 +185,8 @@ fn main() { background_actions, }); cx.set_global(Arc::downgrade(&app_state)); + + audio::init(Assets, cx); auto_update::init(http.clone(), client::ZED_SERVER_URL.clone(), cx); workspace::init(app_state.clone(), cx); diff --git a/crates/zed/src/only_instance.rs b/crates/zed/src/only_instance.rs new file mode 100644 index 0000000000..a8c4b30816 --- /dev/null +++ b/crates/zed/src/only_instance.rs @@ -0,0 +1,103 @@ +use std::{ + io::{Read, Write}, + net::{Ipv4Addr, SocketAddr, SocketAddrV4, TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use util::channel::ReleaseChannel; + +const LOCALHOST: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1); +const CONNECT_TIMEOUT: Duration = Duration::from_millis(10); +const RECEIVE_TIMEOUT: Duration = Duration::from_millis(35); +const SEND_TIMEOUT: Duration = Duration::from_millis(20); + +fn address() -> SocketAddr { + let port = match *util::channel::RELEASE_CHANNEL { + ReleaseChannel::Dev => 43737, + ReleaseChannel::Preview => 43738, + ReleaseChannel::Stable => 43739, + }; + + SocketAddr::V4(SocketAddrV4::new(LOCALHOST, port)) +} + +fn instance_handshake() -> &'static str { + match *util::channel::RELEASE_CHANNEL { + ReleaseChannel::Dev => "Zed Editor Dev Instance Running", + ReleaseChannel::Preview => "Zed Editor Preview Instance Running", + ReleaseChannel::Stable => "Zed Editor Stable Instance Running", + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IsOnlyInstance { + Yes, + No, +} + +pub fn ensure_only_instance() -> IsOnlyInstance { + if *db::ZED_STATELESS { + return IsOnlyInstance::Yes; + } + + if check_got_handshake() { + return IsOnlyInstance::No; + } + + let listener = match TcpListener::bind(address()) { + Ok(listener) => listener, + + Err(err) => { + log::warn!("Error binding to single instance port: {err}"); + if check_got_handshake() { + return IsOnlyInstance::No; + } + + // Avoid failing to start when some other application by chance already has + // a claim on the port. This is sub-par as any other instance that gets launched + // will be unable to communicate with this instance and will duplicate + log::warn!("Backup handshake request failed, continuing without handshake"); + return IsOnlyInstance::Yes; + } + }; + + thread::spawn(move || { + for stream in listener.incoming() { + let mut stream = match stream { + Ok(stream) => stream, + Err(_) => return, + }; + + _ = stream.set_nodelay(true); + _ = stream.set_read_timeout(Some(SEND_TIMEOUT)); + _ = stream.write_all(instance_handshake().as_bytes()); + } + }); + + IsOnlyInstance::Yes +} + +fn check_got_handshake() -> bool { + match TcpStream::connect_timeout(&address(), CONNECT_TIMEOUT) { + Ok(mut stream) => { + let mut buf = vec![0u8; instance_handshake().len()]; + + stream.set_read_timeout(Some(RECEIVE_TIMEOUT)).unwrap(); + if let Err(err) = stream.read_exact(&mut buf) { + log::warn!("Connected to single instance port but failed to read: {err}"); + return false; + } + + if buf == instance_handshake().as_bytes() { + log::info!("Got instance handshake"); + return true; + } + + log::warn!("Got wrong instance handshake value"); + false + } + + Err(_) => false, + } +} diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index a7c6956a34..09bdbf65be 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1,6 +1,7 @@ pub mod assets; pub mod languages; pub mod menus; +pub mod only_instance; #[cfg(any(test, feature = "test-support"))] pub mod test; @@ -2074,6 +2075,167 @@ mod tests { line!(), ); + #[track_caller] + fn assert_key_bindings_for<'a>( + window_id: usize, + cx: &TestAppContext, + actions: Vec<(&'static str, &'a dyn Action)>, + line: u32, + ) { + for (key, action) in actions { + // assert that... + assert!( + cx.available_actions(window_id, 0) + .into_iter() + .any(|(_, bound_action, b)| { + // action names match... + bound_action.name() == action.name() + && bound_action.namespace() == action.namespace() + // and key strokes contain the given key + && b.iter() + .any(|binding| binding.keystrokes().iter().any(|k| k.key == key)) + }), + "On {} Failed to find {} with key binding {}", + line, + action.name(), + key + ); + } + } + } + + #[gpui::test] + async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) { + struct TestView; + + impl Entity for TestView { + type Event = (); + } + + impl View for TestView { + fn ui_name() -> &'static str { + "TestView" + } + + fn render(&mut self, _: &mut ViewContext) -> AnyElement { + Empty::new().into_any() + } + } + + let executor = cx.background(); + let fs = FakeFs::new(executor.clone()); + + actions!(test, [A, B]); + // From the Atom keymap + actions!(workspace, [ActivatePreviousPane]); + // From the JetBrains keymap + actions!(pane, [ActivatePrevItem]); + + fs.save( + "/settings.json".as_ref(), + &r#" + { + "base_keymap": "Atom" + } + "# + .into(), + Default::default(), + ) + .await + .unwrap(); + + fs.save( + "/keymap.json".as_ref(), + &r#" + [ + { + "bindings": { + "backspace": "test::A" + } + } + ] + "# + .into(), + Default::default(), + ) + .await + .unwrap(); + + cx.update(|cx| { + cx.set_global(SettingsStore::test(cx)); + theme::init(Assets, cx); + welcome::init(cx); + + cx.add_global_action(|_: &A, _cx| {}); + cx.add_global_action(|_: &B, _cx| {}); + cx.add_global_action(|_: &ActivatePreviousPane, _cx| {}); + cx.add_global_action(|_: &ActivatePrevItem, _cx| {}); + + let settings_rx = watch_config_file( + executor.clone(), + fs.clone(), + PathBuf::from("/settings.json"), + ); + let keymap_rx = + watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json")); + + handle_keymap_file_changes(keymap_rx, cx); + handle_settings_file_changes(settings_rx, cx); + }); + + cx.foreground().run_until_parked(); + + let (window_id, _view) = cx.add_window(|_| TestView); + + // Test loading the keymap base at all + assert_key_bindings_for( + window_id, + cx, + vec![("backspace", &A), ("k", &ActivatePreviousPane)], + line!(), + ); + + // Test disabling the key binding for the base keymap + fs.save( + "/keymap.json".as_ref(), + &r#" + [ + { + "bindings": { + "backspace": null + } + } + ] + "# + .into(), + Default::default(), + ) + .await + .unwrap(); + + cx.foreground().run_until_parked(); + + assert_key_bindings_for(window_id, cx, vec![("k", &ActivatePreviousPane)], line!()); + + // Test modifying the base, while retaining the users keymap + fs.save( + "/settings.json".as_ref(), + &r#" + { + "base_keymap": "JetBrains" + } + "# + .into(), + Default::default(), + ) + .await + .unwrap(); + + cx.foreground().run_until_parked(); + + assert_key_bindings_for(window_id, cx, vec![("[", &ActivatePrevItem)], line!()); + + #[track_caller] fn assert_key_bindings_for<'a>( window_id: usize, cx: &TestAppContext, @@ -2160,6 +2322,7 @@ mod tests { state.initialize_workspace = initialize_workspace; state.build_window_options = build_window_options; theme::init((), cx); + audio::init((), cx); call::init(app_state.client.clone(), app_state.user_store.clone(), cx); workspace::init(app_state.clone(), cx); Project::init_settings(cx); diff --git a/docs/zed/syntax-highlighting.md b/docs/zed/syntax-highlighting.md index 3878fcc6e9..d4331ee367 100644 --- a/docs/zed/syntax-highlighting.md +++ b/docs/zed/syntax-highlighting.md @@ -35,7 +35,7 @@ Match a property identifier and highlight it using the identifier `@property`. I ``` ```ts -function buildDefaultSyntax(colorScheme: ColorScheme): Partial { +function buildDefaultSyntax(colorScheme: Theme): Partial { // ... } ``` diff --git a/styles/package-lock.json b/styles/package-lock.json index 3f73a0b4e5..6fc5f746e5 100644 --- a/styles/package-lock.json +++ b/styles/package-lock.json @@ -27,7 +27,8 @@ "ts-node": "^10.9.1", "typescript": "^5.1.5", "utility-types": "^3.10.0", - "vitest": "^0.32.0" + "vitest": "^0.32.0", + "zustand": "^4.3.8" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -2595,6 +2596,12 @@ "node": ">= 0.8" } }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "peer": true + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -2706,6 +2713,18 @@ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/loupe": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", @@ -3292,6 +3311,18 @@ } ] }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -4025,6 +4056,14 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/utility-types": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz", @@ -4305,6 +4344,29 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zustand": { + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.3.8.tgz", + "integrity": "sha512-4h28KCkHg5ii/wcFFJ5Fp+k1J3gJoasaIbppdgZFO4BPJnsNxL0mQXBSFgOgAdCdBj35aDTPvdAJReTMntFPGg==", + "dependencies": { + "use-sync-external-store": "1.2.0" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "immer": ">=9.0", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } } } } diff --git a/styles/package.json b/styles/package.json index d82bbb7e81..16e95d90d5 100644 --- a/styles/package.json +++ b/styles/package.json @@ -16,21 +16,22 @@ "@tokens-studio/types": "^0.2.3", "@types/chroma-js": "^2.4.0", "@types/node": "^18.14.1", + "@typescript-eslint/eslint-plugin": "^5.60.1", + "@typescript-eslint/parser": "^5.60.1", + "@vitest/coverage-v8": "^0.32.0", "ayu": "^8.0.1", "chroma-js": "^2.4.2", "deepmerge": "^4.3.0", + "eslint": "^8.43.0", + "eslint-import-resolver-typescript": "^3.5.5", + "eslint-plugin-import": "^2.27.5", "json-schema-to-typescript": "^13.0.2", "toml": "^3.0.0", "ts-deepmerge": "^6.0.3", "ts-node": "^10.9.1", + "typescript": "^5.1.5", "utility-types": "^3.10.0", "vitest": "^0.32.0", - "@typescript-eslint/eslint-plugin": "^5.60.1", - "@typescript-eslint/parser": "^5.60.1", - "@vitest/coverage-v8": "^0.32.0", - "eslint": "^8.43.0", - "eslint-import-resolver-typescript": "^3.5.5", - "eslint-plugin-import": "^2.27.5", - "typescript": "^5.1.5" + "zustand": "^4.3.8" } } diff --git a/styles/src/build_themes.ts b/styles/src/build_themes.ts index 5a091719df..17575663a1 100644 --- a/styles/src/build_themes.ts +++ b/styles/src/build_themes.ts @@ -2,8 +2,9 @@ import * as fs from "fs" import { tmpdir } from "os" import * as path from "path" import app from "./style_tree/app" -import { ColorScheme, create_color_scheme } from "./theme/color_scheme" +import { Theme, create_theme } from "./theme/create_theme" import { themes } from "./themes" +import { useThemeStore } from "./theme" const assets_directory = `${__dirname}/../../assets` const temp_directory = fs.mkdtempSync(path.join(tmpdir(), "build-themes")) @@ -20,15 +21,22 @@ function clear_themes(theme_directory: string) { } } -function write_themes(themes: ColorScheme[], output_directory: string) { +const all_themes: Theme[] = themes.map((theme) => + create_theme(theme) +) + +function write_themes(themes: Theme[], output_directory: string) { clear_themes(output_directory) - for (const color_scheme of themes) { - const style_tree = app(color_scheme) + for (const theme of themes) { + const { setTheme } = useThemeStore.getState() + setTheme(theme) + + const style_tree = app() const style_tree_json = JSON.stringify(style_tree, null, 2) - const temp_path = path.join(temp_directory, `${color_scheme.name}.json`) + const temp_path = path.join(temp_directory, `${theme.name}.json`) const out_path = path.join( output_directory, - `${color_scheme.name}.json` + `${theme.name}.json` ) fs.writeFileSync(temp_path, style_tree_json) fs.renameSync(temp_path, out_path) @@ -36,8 +44,4 @@ function write_themes(themes: ColorScheme[], output_directory: string) { } } -const all_themes: ColorScheme[] = themes.map((theme) => - create_color_scheme(theme) -) - write_themes(all_themes, `${assets_directory}/themes`) diff --git a/styles/src/build_tokens.ts b/styles/src/build_tokens.ts index e33c3712e6..fd6aa18ced 100644 --- a/styles/src/build_tokens.ts +++ b/styles/src/build_tokens.ts @@ -1,9 +1,9 @@ import * as fs from "fs" import * as path from "path" -import { ColorScheme, create_color_scheme } from "./common" +import { Theme, create_theme, useThemeStore } from "./common" import { themes } from "./themes" import { slugify } from "./utils/slugify" -import { theme_tokens } from "./theme/tokens/color_scheme" +import { theme_tokens } from "./theme/tokens/theme" const TOKENS_DIRECTORY = path.join(__dirname, "..", "target", "tokens") const TOKENS_FILE = path.join(TOKENS_DIRECTORY, "$themes.json") @@ -27,7 +27,7 @@ type TokenSet = { selected_token_sets: { [key: string]: "enabled" } } -function build_token_set_order(theme: ColorScheme[]): { +function build_token_set_order(theme: Theme[]): { token_set_order: string[] } { const token_set_order: string[] = theme.map((scheme) => @@ -36,7 +36,7 @@ function build_token_set_order(theme: ColorScheme[]): { return { token_set_order } } -function build_themes_index(theme: ColorScheme[]): TokenSet[] { +function build_themes_index(theme: Theme[]): TokenSet[] { const themes_index: TokenSet[] = theme.map((scheme, index) => { const id = `${scheme.is_light ? "light" : "dark"}_${scheme.name .toLowerCase() @@ -55,12 +55,15 @@ function build_themes_index(theme: ColorScheme[]): TokenSet[] { return themes_index } -function write_tokens(themes: ColorScheme[], tokens_directory: string) { +function write_tokens(themes: Theme[], tokens_directory: string) { clear_tokens(tokens_directory) for (const theme of themes) { + const { setTheme } = useThemeStore.getState() + setTheme(theme) + const file_name = slugify(theme.name) + ".json" - const tokens = theme_tokens(theme) + const tokens = theme_tokens() const tokens_json = JSON.stringify(tokens, null, 2) const out_path = path.join(tokens_directory, file_name) fs.writeFileSync(out_path, tokens_json, { mode: 0o644 }) @@ -80,8 +83,8 @@ function write_tokens(themes: ColorScheme[], tokens_directory: string) { console.log(`- ${METADATA_FILE} created`) } -const all_themes: ColorScheme[] = themes.map((theme) => - create_color_scheme(theme) +const all_themes: Theme[] = themes.map((theme) => + create_theme(theme) ) write_tokens(all_themes, TOKENS_DIRECTORY) diff --git a/styles/src/component/icon_button.ts b/styles/src/component/icon_button.ts index 79891c2477..6887fc7c30 100644 --- a/styles/src/component/icon_button.ts +++ b/styles/src/component/icon_button.ts @@ -1,6 +1,6 @@ import { interactive, toggleable } from "../element" import { background, foreground } from "../style_tree/components" -import { ColorScheme } from "../theme/color_scheme" +import { useTheme, Theme } from "../theme" export type Margin = { top: number @@ -11,21 +11,20 @@ export type Margin = { interface IconButtonOptions { layer?: - | ColorScheme["lowest"] - | ColorScheme["middle"] - | ColorScheme["highest"] - color?: keyof ColorScheme["lowest"] + | Theme["lowest"] + | Theme["middle"] + | Theme["highest"] + color?: keyof Theme["lowest"] margin?: Partial } type ToggleableIconButtonOptions = IconButtonOptions & { - active_color?: keyof ColorScheme["lowest"] + active_color?: keyof Theme["lowest"] } -export function icon_button( - theme: ColorScheme, - { color, margin, layer }: IconButtonOptions -) { +export function icon_button({ color, margin, layer }: IconButtonOptions) { + const theme = useTheme() + if (!color) color = "base" const m = { @@ -68,15 +67,15 @@ export function icon_button( } export function toggleable_icon_button( - theme: ColorScheme, + theme: Theme, { color, active_color, margin }: ToggleableIconButtonOptions ) { if (!color) color = "base" return toggleable({ state: { - inactive: icon_button(theme, { color, margin }), - active: icon_button(theme, { + inactive: icon_button({ color, margin }), + active: icon_button({ color: active_color ? active_color : color, margin, layer: theme.middle, diff --git a/styles/src/component/text_button.ts b/styles/src/component/text_button.ts index 477c2515e3..58b2a1cbf2 100644 --- a/styles/src/component/text_button.ts +++ b/styles/src/component/text_button.ts @@ -5,27 +5,30 @@ import { foreground, text, } from "../style_tree/components" -import { ColorScheme } from "../theme/color_scheme" +import { useTheme, Theme } from "../theme" import { Margin } from "./icon_button" interface TextButtonOptions { layer?: - | ColorScheme["lowest"] - | ColorScheme["middle"] - | ColorScheme["highest"] - color?: keyof ColorScheme["lowest"] + | Theme["lowest"] + | Theme["middle"] + | Theme["highest"] + color?: keyof Theme["lowest"] margin?: Partial text_properties?: TextProperties } type ToggleableTextButtonOptions = TextButtonOptions & { - active_color?: keyof ColorScheme["lowest"] + active_color?: keyof Theme["lowest"] } -export function text_button( - theme: ColorScheme, - { color, layer, margin, text_properties }: TextButtonOptions -) { +export function text_button({ + color, + layer, + margin, + text_properties, +}: TextButtonOptions) { + const theme = useTheme() if (!color) color = "base" const text_options: TextProperties = { @@ -72,15 +75,15 @@ export function text_button( } export function toggleable_text_button( - theme: ColorScheme, + theme: Theme, { color, active_color, margin }: ToggleableTextButtonOptions ) { if (!color) color = "base" return toggleable({ state: { - inactive: text_button(theme, { color, margin }), - active: text_button(theme, { + inactive: text_button({ color, margin }), + active: text_button({ color: active_color ? active_color : color, margin, layer: theme.middle, diff --git a/styles/src/styleTree/editor.ts b/styles/src/styleTree/editor.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/styles/src/style_tree/app.ts b/styles/src/style_tree/app.ts index 684614d073..ccfdd60a98 100644 --- a/styles/src/style_tree/app.ts +++ b/styles/src/style_tree/app.ts @@ -17,59 +17,46 @@ import terminal from "./terminal" import contact_list from "./contact_list" import toolbar_dropdown_menu from "./toolbar_dropdown_menu" import incoming_call_notification from "./incoming_call_notification" -import { ColorScheme } from "../theme/color_scheme" import welcome from "./welcome" import copilot from "./copilot" import assistant from "./assistant" import { titlebar } from "./titlebar" import editor from "./editor" import feedback from "./feedback" +import { useTheme } from "../common" + +export default function app(): any { + const theme = useTheme() -export default function app(theme: ColorScheme): any { return { meta: { name: theme.name, is_light: theme.is_light, }, - command_palette: command_palette(theme), - contact_notification: contact_notification(theme), - project_shared_notification: project_shared_notification(theme), - incoming_call_notification: incoming_call_notification(theme), - picker: picker(theme), - workspace: workspace(theme), - titlebar: titlebar(theme), - copilot: copilot(theme), - welcome: welcome(theme), - context_menu: context_menu(theme), - editor: editor(theme), - project_diagnostics: project_diagnostics(theme), - project_panel: project_panel(theme), - contacts_popover: contacts_popover(theme), - contact_finder: contact_finder(theme), - contact_list: contact_list(theme), - toolbar_dropdown_menu: toolbar_dropdown_menu(theme), - search: search(theme), - shared_screen: shared_screen(theme), - update_notification: update_notification(theme), - simple_message_notification: simple_message_notification(theme), - tooltip: tooltip(theme), - terminal: terminal(theme), - assistant: assistant(theme), - feedback: feedback(theme), - color_scheme: { - ...theme, - players: Object.values(theme.players), - ramps: { - neutral: theme.ramps.neutral.colors(100, "hex"), - red: theme.ramps.red.colors(100, "hex"), - orange: theme.ramps.orange.colors(100, "hex"), - yellow: theme.ramps.yellow.colors(100, "hex"), - green: theme.ramps.green.colors(100, "hex"), - cyan: theme.ramps.cyan.colors(100, "hex"), - blue: theme.ramps.blue.colors(100, "hex"), - violet: theme.ramps.violet.colors(100, "hex"), - magenta: theme.ramps.magenta.colors(100, "hex"), - }, - }, + command_palette: command_palette(), + contact_notification: contact_notification(), + project_shared_notification: project_shared_notification(), + incoming_call_notification: incoming_call_notification(), + picker: picker(), + workspace: workspace(), + titlebar: titlebar(), + copilot: copilot(), + welcome: welcome(), + context_menu: context_menu(), + editor: editor(), + project_diagnostics: project_diagnostics(), + project_panel: project_panel(), + contacts_popover: contacts_popover(), + contact_finder: contact_finder(), + contact_list: contact_list(), + toolbar_dropdown_menu: toolbar_dropdown_menu(), + search: search(), + shared_screen: shared_screen(), + update_notification: update_notification(), + simple_message_notification: simple_message_notification(), + tooltip: tooltip(), + terminal: terminal(), + assistant: assistant(), + feedback: feedback() } } diff --git a/styles/src/style_tree/assistant.ts b/styles/src/style_tree/assistant.ts index 3205268f53..adec3dee62 100644 --- a/styles/src/style_tree/assistant.ts +++ b/styles/src/style_tree/assistant.ts @@ -1,11 +1,7 @@ -import { ColorScheme, StyleSets } from "../theme/color_scheme" import { text, border, background, foreground, TextStyle } from "./components" import { Interactive, interactive } from "../element" import { tab_bar_button } from "../component/tab_bar_button" - -interface ToolbarButtonOptions { - icon: string -} +import { StyleSets, useTheme } from "../theme" type RoleCycleButton = TextStyle & { background?: string @@ -23,7 +19,8 @@ type RemainingTokens = TextStyle & { corner_radius: number, } -export default function assistant(theme: ColorScheme): any { +export default function assistant(): any { + const theme = useTheme() const interactive_role = (color: StyleSets): Interactive => { return ( diff --git a/styles/src/style_tree/command_palette.ts b/styles/src/style_tree/command_palette.ts index 289deef54b..2f7404c8d4 100644 --- a/styles/src/style_tree/command_palette.ts +++ b/styles/src/style_tree/command_palette.ts @@ -1,9 +1,11 @@ -import { ColorScheme } from "../theme/color_scheme" import { with_opacity } from "../theme/color" import { text, background } from "./components" import { toggleable } from "../element" +import { useTheme } from "../theme" + +export default function command_palette(): any { + const theme = useTheme() -export default function command_palette(theme: ColorScheme): any { const key = toggleable({ base: { text: text(theme.highest, "mono", "variant", "default", { diff --git a/styles/src/style_tree/components.ts b/styles/src/style_tree/components.ts index db32712f41..43a5fa9d28 100644 --- a/styles/src/style_tree/components.ts +++ b/styles/src/style_tree/components.ts @@ -1,5 +1,5 @@ import { font_families, font_sizes, FontWeight } from "../common" -import { Layer, Styles, StyleSets, Style } from "../theme/color_scheme" +import { Layer, Styles, StyleSets, Style } from "../theme/create_theme" function is_style_set(key: any): key is StyleSets { return [ diff --git a/styles/src/style_tree/contact_finder.ts b/styles/src/style_tree/contact_finder.ts index e61d100264..aa88a9f26a 100644 --- a/styles/src/style_tree/contact_finder.ts +++ b/styles/src/style_tree/contact_finder.ts @@ -1,8 +1,10 @@ import picker from "./picker" -import { ColorScheme } from "../theme/color_scheme" import { background, border, foreground, text } from "./components" +import { useTheme } from "../theme" + +export default function contact_finder(): any { + const theme = useTheme() -export default function contact_finder(theme: ColorScheme): any { const side_margin = 6 const contact_button = { background: background(theme.middle, "variant"), @@ -12,7 +14,7 @@ export default function contact_finder(theme: ColorScheme): any { corner_radius: 8, } - const picker_style = picker(theme) + const picker_style = picker() const picker_input = { background: background(theme.middle, "on"), corner_radius: 6, @@ -44,6 +46,8 @@ export default function contact_finder(theme: ColorScheme): any { no_matches: picker_style.no_matches, input_editor: picker_input, empty_input_editor: picker_input, + header: picker_style.header, + footer: picker_style.footer, }, row_height: 28, contact_avatar: { diff --git a/styles/src/style_tree/contact_list.ts b/styles/src/style_tree/contact_list.ts index 93f88e2d4a..1955231f59 100644 --- a/styles/src/style_tree/contact_list.ts +++ b/styles/src/style_tree/contact_list.ts @@ -1,4 +1,3 @@ -import { ColorScheme } from "../theme/color_scheme" import { background, border, @@ -7,7 +6,10 @@ import { text, } from "./components" import { interactive, toggleable } from "../element" -export default function contacts_panel(theme: ColorScheme): any { +import { useTheme } from "../theme" +export default function contacts_panel(): any { + const theme = useTheme() + const name_margin = 8 const side_padding = 12 diff --git a/styles/src/style_tree/contact_notification.ts b/styles/src/style_tree/contact_notification.ts index 8de5496d91..365e3a646d 100644 --- a/styles/src/style_tree/contact_notification.ts +++ b/styles/src/style_tree/contact_notification.ts @@ -1,8 +1,10 @@ -import { ColorScheme } from "../theme/color_scheme" import { background, foreground, text } from "./components" import { interactive } from "../element" +import { useTheme } from "../theme" + +export default function contact_notification(): any { + const theme = useTheme() -export default function contact_notification(theme: ColorScheme): any { const avatar_size = 12 const header_padding = 8 diff --git a/styles/src/style_tree/contacts_popover.ts b/styles/src/style_tree/contacts_popover.ts index 4e3f8899e0..0ce63d088a 100644 --- a/styles/src/style_tree/contacts_popover.ts +++ b/styles/src/style_tree/contacts_popover.ts @@ -1,7 +1,9 @@ -import { ColorScheme } from "../theme/color_scheme" +import { useTheme } from "../theme" import { background, border } from "./components" -export default function contacts_popover(theme: ColorScheme): any { +export default function contacts_popover(): any { + const theme = useTheme() + return { background: background(theme.middle), corner_radius: 6, diff --git a/styles/src/style_tree/context_menu.ts b/styles/src/style_tree/context_menu.ts index af45942d2d..d4266a71fe 100644 --- a/styles/src/style_tree/context_menu.ts +++ b/styles/src/style_tree/context_menu.ts @@ -1,8 +1,10 @@ -import { ColorScheme } from "../theme/color_scheme" import { background, border, border_color, text } from "./components" import { interactive, toggleable } from "../element" +import { useTheme } from "../theme" + +export default function context_menu(): any { + const theme = useTheme() -export default function context_menu(theme: ColorScheme): any { return { background: background(theme.middle), corner_radius: 10, diff --git a/styles/src/style_tree/copilot.ts b/styles/src/style_tree/copilot.ts index eee6880e0c..f002db5ef5 100644 --- a/styles/src/style_tree/copilot.ts +++ b/styles/src/style_tree/copilot.ts @@ -1,7 +1,9 @@ -import { ColorScheme } from "../theme/color_scheme" import { background, border, foreground, svg, text } from "./components" import { interactive } from "../element" -export default function copilot(theme: ColorScheme): any { +import { useTheme } from "../theme" +export default function copilot(): any { + const theme = useTheme() + const content_width = 264 const cta_button = diff --git a/styles/src/style_tree/editor.ts b/styles/src/style_tree/editor.ts index af58276d16..48a2fd33e2 100644 --- a/styles/src/style_tree/editor.ts +++ b/styles/src/style_tree/editor.ts @@ -1,5 +1,5 @@ import { with_opacity } from "../theme/color" -import { ColorScheme, Layer, StyleSets } from "../theme/color_scheme" +import { Layer, StyleSets } from "../theme/create_theme" import { background, border, @@ -11,8 +11,11 @@ import hover_popover from "./hover_popover" import { build_syntax } from "../theme/syntax" import { interactive, toggleable } from "../element" +import { useTheme } from "../theme" + +export default function editor(): any { + const theme = useTheme() -export default function editor(theme: ColorScheme): any { const { is_light } = theme const layer = theme.highest @@ -45,7 +48,7 @@ export default function editor(theme: ColorScheme): any { } } - const syntax = build_syntax(theme) + const syntax = build_syntax() return { text_color: syntax.primary.color, @@ -248,7 +251,7 @@ export default function editor(theme: ColorScheme): any { invalid_hint_diagnostic: diagnostic(theme.middle, "base"), invalid_information_diagnostic: diagnostic(theme.middle, "base"), invalid_warning_diagnostic: diagnostic(theme.middle, "base"), - hover_popover: hover_popover(theme), + hover_popover: hover_popover(), link_definition: { color: syntax.link_uri.color, underline: syntax.link_uri.underline, @@ -301,6 +304,7 @@ export default function editor(theme: ColorScheme): any { ? with_opacity(theme.ramps.green(0.5).hex(), 0.8) : with_opacity(theme.ramps.green(0.4).hex(), 0.8), }, + selections: foreground(layer, "accent") }, composition_mark: { underline: { diff --git a/styles/src/style_tree/feedback.ts b/styles/src/style_tree/feedback.ts index 9217b60929..2bb63e951b 100644 --- a/styles/src/style_tree/feedback.ts +++ b/styles/src/style_tree/feedback.ts @@ -1,8 +1,10 @@ -import { ColorScheme } from "../theme/color_scheme" import { background, border, text } from "./components" import { interactive } from "../element" +import { useTheme } from "../theme" + +export default function feedback(): any { + const theme = useTheme() -export default function feedback(theme: ColorScheme): any { return { submit_button: interactive({ base: { diff --git a/styles/src/style_tree/hover_popover.ts b/styles/src/style_tree/hover_popover.ts index f469505741..80f2250349 100644 --- a/styles/src/style_tree/hover_popover.ts +++ b/styles/src/style_tree/hover_popover.ts @@ -1,7 +1,9 @@ -import { ColorScheme } from "../theme/color_scheme" +import { useTheme } from "../theme" import { background, border, foreground, text } from "./components" -export default function hover_popover(theme: ColorScheme): any { +export default function hover_popover(): any { + const theme = useTheme() + const base_container = { background: background(theme.middle), corner_radius: 8, diff --git a/styles/src/style_tree/incoming_call_notification.ts b/styles/src/style_tree/incoming_call_notification.ts index ca46596e57..294ec00a73 100644 --- a/styles/src/style_tree/incoming_call_notification.ts +++ b/styles/src/style_tree/incoming_call_notification.ts @@ -1,9 +1,9 @@ -import { ColorScheme } from "../theme/color_scheme" +import { useTheme } from "../theme" import { background, border, text } from "./components" -export default function incoming_call_notification( - theme: ColorScheme -): unknown { +export default function incoming_call_notification(): unknown { + const theme = useTheme() + const avatar_size = 48 return { window_height: 74, diff --git a/styles/src/style_tree/picker.ts b/styles/src/style_tree/picker.ts index 7ca76cd85f..bbd664397f 100644 --- a/styles/src/style_tree/picker.ts +++ b/styles/src/style_tree/picker.ts @@ -1,9 +1,11 @@ -import { ColorScheme } from "../theme/color_scheme" import { with_opacity } from "../theme/color" import { background, border, text } from "./components" import { interactive, toggleable } from "../element" +import { useTheme } from "../theme" + +export default function picker(): any { + const theme = useTheme() -export default function picker(theme: ColorScheme): any { const container = { background: background(theme.lowest), border: border(theme.lowest), @@ -108,5 +110,23 @@ export default function picker(theme: ColorScheme): any { top: 8, }, }, + header: { + text: text(theme.lowest, "sans", "variant", { size: "xs" }), + + margin: { + top: 1, + left: 8, + right: 8, + }, + }, + footer: { + text: text(theme.lowest, "sans", "variant", { size: "xs" }), + margin: { + top: 1, + left: 8, + right: 8, + }, + + } } } diff --git a/styles/src/style_tree/project_diagnostics.ts b/styles/src/style_tree/project_diagnostics.ts index 5962b98a26..1c13b31a4a 100644 --- a/styles/src/style_tree/project_diagnostics.ts +++ b/styles/src/style_tree/project_diagnostics.ts @@ -1,7 +1,9 @@ -import { ColorScheme } from "../theme/color_scheme" +import { useTheme } from "../theme" import { background, text } from "./components" -export default function project_diagnostics(theme: ColorScheme): any { +export default function project_diagnostics(): any { + const theme = useTheme() + return { background: background(theme.highest), tab_icon_spacing: 4, diff --git a/styles/src/style_tree/project_panel.ts b/styles/src/style_tree/project_panel.ts index d1024778f1..af997d0a6e 100644 --- a/styles/src/style_tree/project_panel.ts +++ b/styles/src/style_tree/project_panel.ts @@ -1,4 +1,3 @@ -import { ColorScheme } from "../theme/color_scheme" import { with_opacity } from "../theme/color" import { Border, @@ -10,7 +9,10 @@ import { } from "./components" import { interactive, toggleable } from "../element" import merge from "ts-deepmerge" -export default function project_panel(theme: ColorScheme): any { +import { useTheme } from "../theme" +export default function project_panel(): any { + const theme = useTheme() + const { is_light } = theme type EntryStateProps = { @@ -65,13 +67,12 @@ export default function project_panel(theme: ColorScheme): any { const unselected_hovered_style = merge( base_properties, { background: background(theme.middle, "hovered") }, - unselected?.hovered ?? {}, + unselected?.hovered ?? {} ) const unselected_clicked_style = merge( base_properties, - { background: background(theme.middle, "pressed"), } - , - unselected?.clicked ?? {}, + { background: background(theme.middle, "pressed") }, + unselected?.clicked ?? {} ) const selected_default_style = merge( base_properties, @@ -79,18 +80,15 @@ export default function project_panel(theme: ColorScheme): any { background: background(theme.lowest), text: text(theme.lowest, "sans", { size: "sm" }), }, - selected_style?.default ?? {}, - + selected_style?.default ?? {} ) const selected_hovered_style = merge( base_properties, { background: background(theme.lowest, "hovered"), text: text(theme.lowest, "sans", { size: "sm" }), - }, - selected_style?.hovered ?? {}, - + selected_style?.hovered ?? {} ) const selected_clicked_style = merge( base_properties, @@ -98,8 +96,7 @@ export default function project_panel(theme: ColorScheme): any { background: background(theme.lowest, "pressed"), text: text(theme.lowest, "sans", { size: "sm" }), }, - selected_style?.clicked ?? {}, - + selected_style?.clicked ?? {} ) return toggleable({ diff --git a/styles/src/style_tree/project_shared_notification.ts b/styles/src/style_tree/project_shared_notification.ts index ffda0f4b70..e7c1dcedd5 100644 --- a/styles/src/style_tree/project_shared_notification.ts +++ b/styles/src/style_tree/project_shared_notification.ts @@ -1,9 +1,9 @@ -import { ColorScheme } from "../theme/color_scheme" +import { useTheme } from "../theme" import { background, border, text } from "./components" -export default function project_shared_notification( - theme: ColorScheme -): unknown { +export default function project_shared_notification(): unknown { + const theme = useTheme() + const avatar_size = 48 return { window_height: 74, diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index df260f95b7..5c16d03233 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -1,9 +1,11 @@ -import { ColorScheme } from "../theme/color_scheme" import { with_opacity } from "../theme/color" import { background, border, foreground, text } from "./components" import { interactive, toggleable } from "../element" +import { useTheme } from "../theme" + +export default function search(): any { + const theme = useTheme() -export default function search(theme: ColorScheme): any { // Search input const editor = { background: background(theme.highest), diff --git a/styles/src/style_tree/shared_screen.ts b/styles/src/style_tree/shared_screen.ts index b57c483f1c..aca7fd7f07 100644 --- a/styles/src/style_tree/shared_screen.ts +++ b/styles/src/style_tree/shared_screen.ts @@ -1,7 +1,9 @@ -import { ColorScheme } from "../theme/color_scheme" +import { useTheme } from "../theme" import { background } from "./components" -export default function sharedScreen(theme: ColorScheme) { +export default function sharedScreen() { + const theme = useTheme() + return { background: background(theme.highest), } diff --git a/styles/src/style_tree/simple_message_notification.ts b/styles/src/style_tree/simple_message_notification.ts index 0b5c1e0c29..35133f04a2 100644 --- a/styles/src/style_tree/simple_message_notification.ts +++ b/styles/src/style_tree/simple_message_notification.ts @@ -1,8 +1,10 @@ -import { ColorScheme } from "../theme/color_scheme" import { background, border, foreground, text } from "./components" import { interactive } from "../element" +import { useTheme } from "../theme" + +export default function simple_message_notification(): any { + const theme = useTheme() -export default function simple_message_notification(theme: ColorScheme): any { const header_padding = 8 return { diff --git a/styles/src/style_tree/status_bar.ts b/styles/src/style_tree/status_bar.ts index bde40d570c..9aeea866f3 100644 --- a/styles/src/style_tree/status_bar.ts +++ b/styles/src/style_tree/status_bar.ts @@ -1,7 +1,9 @@ -import { ColorScheme } from "../theme/color_scheme" import { background, border, foreground, text } from "./components" import { interactive, toggleable } from "../element" -export default function status_bar(theme: ColorScheme): any { +import { useTheme } from "../common" +export default function status_bar(): any { + const theme = useTheme() + const layer = theme.lowest const status_container = { diff --git a/styles/src/style_tree/tab_bar.ts b/styles/src/style_tree/tab_bar.ts index 55fd2c314a..29769f9bae 100644 --- a/styles/src/style_tree/tab_bar.ts +++ b/styles/src/style_tree/tab_bar.ts @@ -1,9 +1,11 @@ -import { ColorScheme } from "../theme/color_scheme" import { with_opacity } from "../theme/color" import { text, border, background, foreground } from "./components" import { interactive, toggleable } from "../element" +import { useTheme } from "../common" + +export default function tab_bar(): any { + const theme = useTheme() -export default function tab_bar(theme: ColorScheme): any { const height = 32 const active_layer = theme.highest diff --git a/styles/src/style_tree/terminal.ts b/styles/src/style_tree/terminal.ts index e902aa4264..5b98eebfcd 100644 --- a/styles/src/style_tree/terminal.ts +++ b/styles/src/style_tree/terminal.ts @@ -1,6 +1,8 @@ -import { ColorScheme } from "../theme/color_scheme" +import { useTheme } from "../theme" + +export default function terminal() { + const theme = useTheme() -export default function terminal(theme: ColorScheme) { /** * Colors are controlled per-cell in the terminal grid. * Cells can be set to any of these more 'theme-capable' colors diff --git a/styles/src/style_tree/titlebar.ts b/styles/src/style_tree/titlebar.ts index 067d619bb5..60894b08f6 100644 --- a/styles/src/style_tree/titlebar.ts +++ b/styles/src/style_tree/titlebar.ts @@ -1,7 +1,7 @@ -import { ColorScheme } from "../common" import { icon_button, toggleable_icon_button } from "../component/icon_button" import { toggleable_text_button } from "../component/text_button" import { interactive, toggleable } from "../element" +import { useTheme } from "../theme" import { with_opacity } from "../theme/color" import { background, border, foreground, text } from "./components" @@ -22,7 +22,9 @@ function build_spacing( } } -function call_controls(theme: ColorScheme) { +function call_controls() { + const theme = useTheme() + const button_height = 18 const space = build_spacing(TITLEBAR_HEIGHT, button_height, ITEM_SPACING) @@ -69,7 +71,9 @@ function call_controls(theme: ColorScheme) { * When logged in shows the user's avatar and a chevron, * When logged out only shows a chevron. */ -function user_menu(theme: ColorScheme) { +function user_menu() { + const theme = useTheme() + const button_height = 18 const space = build_spacing(TITLEBAR_HEIGHT, button_height, ITEM_SPACING) @@ -155,7 +159,9 @@ function user_menu(theme: ColorScheme) { } } -export function titlebar(theme: ColorScheme): any { +export function titlebar(): any { + const theme = useTheme() + const avatar_width = 15 const avatar_outer_width = avatar_width + 4 const follower_avatar_width = 14 @@ -173,8 +179,14 @@ export function titlebar(theme: ColorScheme): any { }, // Project - title: text(theme.lowest, "sans", "variant"), - highlight_color: text(theme.lowest, "sans", "active").color, + project_name_divider: text(theme.lowest, "sans", "variant"), + + project_menu_button: toggleable_text_button(theme, { + color: 'base', + }), + git_menu_button: toggleable_text_button(theme, { + color: 'variant', + }), // Collaborators leader_avatar: { @@ -237,14 +249,14 @@ export function titlebar(theme: ColorScheme): any { corner_radius: 6, }, - leave_call_button: icon_button(theme, { + leave_call_button: icon_button({ margin: { left: ITEM_SPACING / 2, right: ITEM_SPACING, }, }), - ...call_controls(theme), + ...call_controls(), toggle_contacts_button: toggleable_icon_button(theme, { margin: { @@ -261,6 +273,6 @@ export function titlebar(theme: ColorScheme): any { background: foreground(theme.lowest, "accent"), }, share_button: toggleable_text_button(theme, {}), - user_menu: user_menu(theme), + user_menu: user_menu(), } } diff --git a/styles/src/style_tree/toolbar_dropdown_menu.ts b/styles/src/style_tree/toolbar_dropdown_menu.ts index dc22ac73cf..97f29ab18c 100644 --- a/styles/src/style_tree/toolbar_dropdown_menu.ts +++ b/styles/src/style_tree/toolbar_dropdown_menu.ts @@ -1,7 +1,9 @@ -import { ColorScheme } from "../theme/color_scheme" import { background, border, text } from "./components" import { interactive, toggleable } from "../element" -export default function dropdown_menu(theme: ColorScheme): any { +import { useTheme } from "../theme" +export default function dropdown_menu(): any { + const theme = useTheme() + return { row_height: 30, background: background(theme.middle), diff --git a/styles/src/style_tree/tooltip.ts b/styles/src/style_tree/tooltip.ts index 2fa5db04d4..54a2d7b78d 100644 --- a/styles/src/style_tree/tooltip.ts +++ b/styles/src/style_tree/tooltip.ts @@ -1,7 +1,9 @@ -import { ColorScheme } from "../theme/color_scheme" +import { useTheme } from "../theme" import { background, border, text } from "./components" -export default function tooltip(theme: ColorScheme): any { +export default function tooltip(): any { + const theme = useTheme() + return { background: background(theme.middle), border: border(theme.middle), diff --git a/styles/src/style_tree/update_notification.ts b/styles/src/style_tree/update_notification.ts index d14e840450..2d0c36d74c 100644 --- a/styles/src/style_tree/update_notification.ts +++ b/styles/src/style_tree/update_notification.ts @@ -1,8 +1,10 @@ -import { ColorScheme } from "../theme/color_scheme" import { foreground, text } from "./components" import { interactive } from "../element" +import { useTheme } from "../theme" + +export default function update_notification(): any { + const theme = useTheme() -export default function update_notification(theme: ColorScheme): any { const header_padding = 8 return { diff --git a/styles/src/style_tree/welcome.ts b/styles/src/style_tree/welcome.ts index fad8dfe235..8ff15d5d26 100644 --- a/styles/src/style_tree/welcome.ts +++ b/styles/src/style_tree/welcome.ts @@ -1,4 +1,3 @@ -import { ColorScheme } from "../theme/color_scheme" import { with_opacity } from "../theme/color" import { border, @@ -9,8 +8,11 @@ import { svg, } from "./components" import { interactive } from "../element" +import { useTheme } from "../theme" + +export default function welcome(): any { + const theme = useTheme() -export default function welcome(theme: ColorScheme): any { const checkbox_base = { corner_radius: 4, padding: { diff --git a/styles/src/style_tree/workspace.ts b/styles/src/style_tree/workspace.ts index 0326de414a..5aee3c987d 100644 --- a/styles/src/style_tree/workspace.ts +++ b/styles/src/style_tree/workspace.ts @@ -1,4 +1,3 @@ -import { ColorScheme } from "../theme/color_scheme" import { with_opacity } from "../theme/color" import { background, @@ -11,9 +10,12 @@ import { import statusBar from "./status_bar" import tabBar from "./tab_bar" import { interactive } from "../element" - import { titlebar } from "./titlebar" -export default function workspace(theme: ColorScheme): any { +import { useTheme } from "../theme" + +export default function workspace(): any { + const theme = useTheme() + const { is_light } = theme return { @@ -85,7 +87,7 @@ export default function workspace(theme: ColorScheme): any { }, leader_border_opacity: 0.7, leader_border_width: 2.0, - tab_bar: tabBar(theme), + tab_bar: tabBar(), modal: { margin: { bottom: 52, @@ -123,8 +125,8 @@ export default function workspace(theme: ColorScheme): any { color: border_color(theme.lowest), width: 1, }, - status_bar: statusBar(theme), - titlebar: titlebar(theme), + status_bar: statusBar(), + titlebar: titlebar(), toolbar: { height: 34, background: background(theme.highest), diff --git a/styles/src/theme/color_scheme.ts b/styles/src/theme/create_theme.ts similarity index 98% rename from styles/src/theme/color_scheme.ts rename to styles/src/theme/create_theme.ts index c64be95184..d2701f8341 100644 --- a/styles/src/theme/color_scheme.ts +++ b/styles/src/theme/create_theme.ts @@ -8,7 +8,7 @@ import { } from "./theme_config" import { get_ramps } from "./ramps" -export interface ColorScheme { +export interface Theme { name: string is_light: boolean @@ -114,7 +114,7 @@ export interface Style { foreground: string } -export function create_color_scheme(theme: ThemeConfig): ColorScheme { +export function create_theme(theme: ThemeConfig): Theme { const { name, appearance, diff --git a/styles/src/theme/index.ts b/styles/src/theme/index.ts index 22287bf669..ca8aaa461f 100644 --- a/styles/src/theme/index.ts +++ b/styles/src/theme/index.ts @@ -1,4 +1,25 @@ -export * from "./color_scheme" +import { create } from "zustand" +import { Theme } from "./create_theme" + +type ThemeState = { + theme: Theme | undefined + setTheme: (theme: Theme) => void +} + +export const useThemeStore = create((set) => ({ + theme: undefined, + setTheme: (theme) => set(() => ({ theme })), +})) + +export const useTheme = (): Theme => { + const { theme } = useThemeStore.getState() + + if (!theme) throw new Error("Tried to use theme before it was loaded") + + return theme +} + +export * from "./create_theme" export * from "./ramps" export * from "./syntax" export * from "./theme_config" diff --git a/styles/src/theme/ramps.ts b/styles/src/theme/ramps.ts index 118d0c7274..c5b915a8c5 100644 --- a/styles/src/theme/ramps.ts +++ b/styles/src/theme/ramps.ts @@ -1,5 +1,5 @@ import chroma, { Color, Scale } from "chroma-js" -import { RampSet } from "./color_scheme" +import { RampSet } from "./create_theme" import { ThemeConfigInputColors, ThemeConfigInputColorsKeys, diff --git a/styles/src/theme/syntax.ts b/styles/src/theme/syntax.ts index c0d68e418e..540a1d0ff9 100644 --- a/styles/src/theme/syntax.ts +++ b/styles/src/theme/syntax.ts @@ -1,6 +1,5 @@ import deepmerge from "deepmerge" -import { FontWeight, font_weights } from "../common" -import { ColorScheme } from "./color_scheme" +import { FontWeight, font_weights, useTheme } from "../common" import chroma from "chroma-js" export interface SyntaxHighlightStyle { @@ -123,7 +122,9 @@ const default_syntax_highlight_style: Omit = { italic: false, } -function build_default_syntax(color_scheme: ColorScheme): Syntax { +function build_default_syntax(): Syntax { + const theme = useTheme() + // Make a temporary object that is allowed to be missing // the "color" property for each style const syntax: { @@ -141,8 +142,8 @@ function build_default_syntax(color_scheme: ColorScheme): Syntax { // predictive color distinct from any other color in the theme const predictive = chroma .mix( - color_scheme.ramps.neutral(0.4).hex(), - color_scheme.ramps.blue(0.4).hex(), + theme.ramps.neutral(0.4).hex(), + theme.ramps.blue(0.4).hex(), 0.45, "lch" ) @@ -151,32 +152,32 @@ function build_default_syntax(color_scheme: ColorScheme): Syntax { // hint color distinct from any other color in the theme const hint = chroma .mix( - color_scheme.ramps.neutral(0.6).hex(), - color_scheme.ramps.blue(0.4).hex(), + theme.ramps.neutral(0.6).hex(), + theme.ramps.blue(0.4).hex(), 0.45, "lch" ) .hex() const color = { - primary: color_scheme.ramps.neutral(1).hex(), - comment: color_scheme.ramps.neutral(0.71).hex(), - punctuation: color_scheme.ramps.neutral(0.86).hex(), + primary: theme.ramps.neutral(1).hex(), + comment: theme.ramps.neutral(0.71).hex(), + punctuation: theme.ramps.neutral(0.86).hex(), predictive: predictive, hint: hint, - emphasis: color_scheme.ramps.blue(0.5).hex(), - string: color_scheme.ramps.orange(0.5).hex(), - function: color_scheme.ramps.yellow(0.5).hex(), - type: color_scheme.ramps.cyan(0.5).hex(), - constructor: color_scheme.ramps.blue(0.5).hex(), - variant: color_scheme.ramps.blue(0.5).hex(), - property: color_scheme.ramps.blue(0.5).hex(), - enum: color_scheme.ramps.orange(0.5).hex(), - operator: color_scheme.ramps.orange(0.5).hex(), - number: color_scheme.ramps.green(0.5).hex(), - boolean: color_scheme.ramps.green(0.5).hex(), - constant: color_scheme.ramps.green(0.5).hex(), - keyword: color_scheme.ramps.blue(0.5).hex(), + emphasis: theme.ramps.blue(0.5).hex(), + string: theme.ramps.orange(0.5).hex(), + function: theme.ramps.yellow(0.5).hex(), + type: theme.ramps.cyan(0.5).hex(), + constructor: theme.ramps.blue(0.5).hex(), + variant: theme.ramps.blue(0.5).hex(), + property: theme.ramps.blue(0.5).hex(), + enum: theme.ramps.orange(0.5).hex(), + operator: theme.ramps.orange(0.5).hex(), + number: theme.ramps.green(0.5).hex(), + boolean: theme.ramps.green(0.5).hex(), + constant: theme.ramps.green(0.5).hex(), + keyword: theme.ramps.blue(0.5).hex(), } // Then assign colors and use Syntax to enforce each style getting it's own color @@ -211,11 +212,11 @@ function build_default_syntax(color_scheme: ColorScheme): Syntax { weight: font_weights.bold, }, link_uri: { - color: color_scheme.ramps.green(0.5).hex(), + color: theme.ramps.green(0.5).hex(), underline: true, }, link_text: { - color: color_scheme.ramps.orange(0.5).hex(), + color: theme.ramps.orange(0.5).hex(), italic: true, }, "text.literal": { @@ -231,7 +232,7 @@ function build_default_syntax(color_scheme: ColorScheme): Syntax { color: color.punctuation, }, "punctuation.special": { - color: color_scheme.ramps.neutral(0.86).hex(), + color: theme.ramps.neutral(0.86).hex(), }, "punctuation.list_marker": { color: color.punctuation, @@ -252,10 +253,10 @@ function build_default_syntax(color_scheme: ColorScheme): Syntax { color: color.string, }, constructor: { - color: color_scheme.ramps.blue(0.5).hex(), + color: theme.ramps.blue(0.5).hex(), }, variant: { - color: color_scheme.ramps.blue(0.5).hex(), + color: theme.ramps.blue(0.5).hex(), }, type: { color: color.type, @@ -264,16 +265,16 @@ function build_default_syntax(color_scheme: ColorScheme): Syntax { color: color.primary, }, label: { - color: color_scheme.ramps.blue(0.5).hex(), + color: theme.ramps.blue(0.5).hex(), }, tag: { - color: color_scheme.ramps.blue(0.5).hex(), + color: theme.ramps.blue(0.5).hex(), }, attribute: { - color: color_scheme.ramps.blue(0.5).hex(), + color: theme.ramps.blue(0.5).hex(), }, property: { - color: color_scheme.ramps.blue(0.5).hex(), + color: theme.ramps.blue(0.5).hex(), }, constant: { color: color.constant, @@ -307,17 +308,18 @@ function build_default_syntax(color_scheme: ColorScheme): Syntax { return default_syntax } -function merge_syntax( - default_syntax: Syntax, - color_scheme: ColorScheme -): Syntax { - if (!color_scheme.syntax) { +export function build_syntax(): Syntax { + const theme = useTheme() + + const default_syntax: Syntax = build_default_syntax() + + if (!theme.syntax) { return default_syntax } - return deepmerge>( + const syntax = deepmerge>( default_syntax, - color_scheme.syntax, + theme.syntax, { arrayMerge: (destinationArray, sourceArray) => [ ...destinationArray, @@ -325,12 +327,6 @@ function merge_syntax( ], } ) -} - -export function build_syntax(color_scheme: ColorScheme): Syntax { - const default_syntax: Syntax = build_default_syntax(color_scheme) - - const syntax = merge_syntax(default_syntax, color_scheme) return syntax } diff --git a/styles/src/theme/theme_config.ts b/styles/src/theme/theme_config.ts index 26462bee6d..bc8f07425f 100644 --- a/styles/src/theme/theme_config.ts +++ b/styles/src/theme/theme_config.ts @@ -66,35 +66,10 @@ type ThemeConfigProperties = ThemeMeta & { override: ThemeConfigOverrides } -// This should be the format a theme is defined as export type ThemeConfig = { [K in keyof ThemeConfigProperties]: ThemeConfigProperties[K] } -interface ThemeColors { - neutral: string[] - red: string[] - orange: string[] - yellow: string[] - green: string[] - cyan: string[] - blue: string[] - violet: string[] - magenta: string[] -} - -type ThemeSyntax = Required - -export type ThemeProperties = ThemeMeta & { - color: ThemeColors - syntax: ThemeSyntax -} - -// This should be a theme after all its properties have been resolved -export type Theme = { - [K in keyof ThemeProperties]: ThemeProperties[K] -} - export enum ThemeAppearance { Light = "light", Dark = "dark", @@ -104,45 +79,3 @@ export enum ThemeLicenseType { MIT = "MIT", Apache2 = "Apache License 2.0", } - -export type ThemeFamilyItem = - | ThemeConfig - | { light: ThemeConfig; dark: ThemeConfig } - -type ThemeFamilyProperties = Partial> & { - name: string - default: ThemeFamilyItem - variants: { - [key: string]: ThemeFamilyItem - } -} - -// Idea: A theme family is a collection of themes that share the same name -// For example, a theme family could be `One Dark` and have a `light` and `dark` variant -// The Ayu family could have `light`, `mirage`, and `dark` variants - -type ThemeFamily = { - [K in keyof ThemeFamilyProperties]: ThemeFamilyProperties[K] -} - -/** The collection of all themes - * - * Example: - * ```ts - * { - * one_dark, - * one_light, - * ayu: { - * name: 'Ayu', - * default: 'ayu_mirage', - * variants: { - * light: 'ayu_light', - * mirage: 'ayu_mirage', - * dark: 'ayu_dark', - * }, - * }, - * ... - * } - * ``` - */ -export type ThemeIndex = Record diff --git a/styles/src/theme/tokens/layer.ts b/styles/src/theme/tokens/layer.ts index a2e539092e..6b4e1d79f7 100644 --- a/styles/src/theme/tokens/layer.ts +++ b/styles/src/theme/tokens/layer.ts @@ -1,5 +1,5 @@ import { SingleColorToken } from "@tokens-studio/types" -import { Layer, Style, StyleSet } from "../color_scheme" +import { Layer, Style, StyleSet } from "../create_theme" import { color_token } from "./token" interface StyleToken { diff --git a/styles/src/theme/tokens/players.ts b/styles/src/theme/tokens/players.ts index 545a712ff1..4bf605aa93 100644 --- a/styles/src/theme/tokens/players.ts +++ b/styles/src/theme/tokens/players.ts @@ -1,12 +1,14 @@ import { SingleColorToken } from "@tokens-studio/types" import { color_token } from "./token" -import { ColorScheme, Players } from "../color_scheme" +import { Players } from "../create_theme" +import { useTheme } from "../../../src/common" export type PlayerToken = Record<"selection" | "cursor", SingleColorToken> export type PlayersToken = Record -function build_player_token(theme: ColorScheme, index: number): PlayerToken { +function build_player_token(index: number): PlayerToken { + const theme = useTheme() const player_number = index.toString() as keyof Players return { @@ -21,13 +23,15 @@ function build_player_token(theme: ColorScheme, index: number): PlayerToken { } } -export const players_token = (theme: ColorScheme): PlayersToken => ({ - "0": build_player_token(theme, 0), - "1": build_player_token(theme, 1), - "2": build_player_token(theme, 2), - "3": build_player_token(theme, 3), - "4": build_player_token(theme, 4), - "5": build_player_token(theme, 5), - "6": build_player_token(theme, 6), - "7": build_player_token(theme, 7), -}) +export const players_token = (): PlayersToken => { + return { + "0": build_player_token(0), + "1": build_player_token(1), + "2": build_player_token(2), + "3": build_player_token(3), + "4": build_player_token(4), + "5": build_player_token(5), + "6": build_player_token(6), + "7": build_player_token(7), + } +} diff --git a/styles/src/theme/tokens/color_scheme.ts b/styles/src/theme/tokens/theme.ts similarity index 81% rename from styles/src/theme/tokens/color_scheme.ts rename to styles/src/theme/tokens/theme.ts index a8ce4ec2d2..f759bc8139 100644 --- a/styles/src/theme/tokens/color_scheme.ts +++ b/styles/src/theme/tokens/theme.ts @@ -5,18 +5,18 @@ import { TokenTypes, } from "@tokens-studio/types" import { - ColorScheme, Shadow, SyntaxHighlightStyle, ThemeSyntax, -} from "../color_scheme" +} from "../create_theme" import { LayerToken, layer_token } from "./layer" import { PlayersToken, players_token } from "./players" import { color_token } from "./token" import { Syntax } from "../syntax" import editor from "../../style_tree/editor" +import { useTheme } from "../../../src/common" -interface ColorSchemeTokens { +interface ThemeTokens { name: SingleOtherToken appearance: SingleOtherToken lowest: LayerToken @@ -39,12 +39,14 @@ const create_shadow_token = ( } } -const popover_shadow_token = (theme: ColorScheme): SingleBoxShadowToken => { +const popover_shadow_token = (): SingleBoxShadowToken => { + const theme = useTheme() const shadow = theme.popover_shadow return create_shadow_token(shadow, "popover_shadow") } -const modal_shadow_token = (theme: ColorScheme): SingleBoxShadowToken => { +const modal_shadow_token = (): SingleBoxShadowToken => { + const theme = useTheme() const shadow = theme.modal_shadow return create_shadow_token(shadow, "modal_shadow") } @@ -68,13 +70,15 @@ function syntax_highlight_style_color_tokens( }, {} as ThemeSyntaxColorTokens) } -const syntax_tokens = (theme: ColorScheme): ColorSchemeTokens["syntax"] => { - const syntax = editor(theme).syntax +const syntax_tokens = (): ThemeTokens["syntax"] => { + const syntax = editor().syntax return syntax_highlight_style_color_tokens(syntax) } -export function theme_tokens(theme: ColorScheme): ColorSchemeTokens { +export function theme_tokens(): ThemeTokens { + const theme = useTheme() + return { name: { name: "themeName", @@ -89,9 +93,9 @@ export function theme_tokens(theme: ColorScheme): ColorSchemeTokens { lowest: layer_token(theme.lowest, "lowest"), middle: layer_token(theme.middle, "middle"), highest: layer_token(theme.highest, "highest"), - popover_shadow: popover_shadow_token(theme), - modal_shadow: modal_shadow_token(theme), - players: players_token(theme), - syntax: syntax_tokens(theme), + popover_shadow: popover_shadow_token(), + modal_shadow: modal_shadow_token(), + players: players_token(), + syntax: syntax_tokens(), } } diff --git a/styles/tsconfig.json b/styles/tsconfig.json index 925935ebb5..281bd74b21 100644 --- a/styles/tsconfig.json +++ b/styles/tsconfig.json @@ -22,17 +22,9 @@ "strictPropertyInitialization": false, "skipLibCheck": true, "useUnknownInCatchVariables": false, - "baseUrl": ".", - "paths": { - "@/*": ["./*"], - "@element/*": ["./src/element/*"], - "@component/*": ["./src/component/*"], - "@styleTree/*": ["./src/styleTree/*"], - "@theme/*": ["./src/theme/*"], - "@types/*": ["./src/util/*"], - "@themes/*": ["./src/themes/*"], - "@util/*": ["./src/util/*"] - } + "baseUrl": "." }, - "exclude": ["node_modules"] + "exclude": [ + "node_modules" + ] }