diff --git a/.github/workflows/publish_collab_image.yml b/.github/workflows/publish_collab_image.yml index 3421409287dfdf146e745d9c34873d6f4a4e045e..b012e65841a1ab5f2e45ff0be05394f16247a64f 100644 --- a/.github/workflows/publish_collab_image.yml +++ b/.github/workflows/publish_collab_image.yml @@ -11,7 +11,7 @@ env: jobs: publish: - name: Publish collab server image + name: Publish collab server image runs-on: - self-hosted - deploy @@ -22,6 +22,9 @@ jobs: - name: Sign into DigitalOcean docker registry run: doctl registry login + - name: Prune Docker system + run: docker system prune + - name: Checkout repo uses: actions/checkout@v3 with: @@ -41,6 +44,6 @@ jobs: - name: Build docker image run: docker build . --tag registry.digitalocean.com/zed/collab:v${COLLAB_VERSION} - + - name: Publish docker image run: docker push registry.digitalocean.com/zed/collab:v${COLLAB_VERSION} diff --git a/Cargo.lock b/Cargo.lock index 4c31069461b3d8519477bd1745ff6ba66a27afa9..8c02500b53ddbd1c174df6aeccde9bf0bf714ae0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -36,11 +36,11 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ - "gimli 0.27.3", + "gimli 0.28.0", ] [[package]] @@ -88,9 +88,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a" dependencies = [ "memchr", ] @@ -140,7 +140,7 @@ source = "git+https://github.com/zed-industries/alacritty?rev=33306142195b354ef3 dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -151,7 +151,7 @@ dependencies = [ "alacritty_config", "alacritty_config_derive", "base64 0.13.1", - "bitflags 2.3.3", + "bitflags 2.4.0", "home", "libc", "log", @@ -244,9 +244,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" +checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea" [[package]] name = "anstyle-parse" @@ -268,9 +268,9 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +checksum = "c677ab05e09154296dd37acecd46420c17b9713e8366facafa8fc0885167cf4c" dependencies = [ "anstyle", "windows-sys", @@ -278,9 +278,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.72" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "arrayref" @@ -337,7 +337,7 @@ dependencies = [ "futures-core", "futures-io", "once_cell", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tokio", ] @@ -351,7 +351,7 @@ dependencies = [ "futures-core", "futures-io", "memchr", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", ] [[package]] @@ -411,15 +411,15 @@ dependencies = [ "polling", "rustix 0.37.23", "slab", - "socket2", + "socket2 0.4.9", "waker-fn", ] [[package]] name = "async-lock" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" dependencies = [ "event-listener", ] @@ -482,7 +482,7 @@ checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -505,7 +505,7 @@ dependencies = [ "log", "memchr", "once_cell", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "pin-utils", "slab", "wasm-bindgen-futures", @@ -519,7 +519,7 @@ checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" dependencies = [ "async-stream-impl", "futures-core", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", ] [[package]] @@ -530,7 +530,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -567,13 +567,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.72" +version = "0.1.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" +checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -586,7 +586,7 @@ dependencies = [ "futures-io", "futures-util", "log", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tungstenite 0.16.0", ] @@ -681,12 +681,12 @@ dependencies = [ "http", "http-body", "hyper", - "itoa 1.0.9", + "itoa", "matchit", "memchr", "mime", "percent-encoding", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "serde", "serde_json", "serde_urlencoded", @@ -727,7 +727,7 @@ dependencies = [ "futures-util", "http", "mime", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "serde", "serde_json", "tokio", @@ -739,16 +739,16 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ - "addr2line 0.20.0", + "addr2line 0.21.0", "cc", "cfg-if 1.0.0", "libc", "miniz_oxide 0.7.1", - "object 0.31.1", + "object 0.32.0", "rustc-demangle", ] @@ -831,7 +831,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.28", + "syn 2.0.29", "which", ] @@ -858,9 +858,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" dependencies = [ "serde", ] @@ -996,7 +996,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05" dependencies = [ "memchr", - "regex-automata 0.3.4", + "regex-automata 0.3.6", "serde", ] @@ -1063,6 +1063,7 @@ dependencies = [ "anyhow", "async-broadcast", "audio", + "channel", "client", "collections", "fs", @@ -1156,11 +1157,12 @@ checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6" [[package]] name = "cc" -version = "1.0.79" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "jobserver", + "libc", ] [[package]] @@ -1190,6 +1192,41 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "channel" +version = "0.1.0" +dependencies = [ + "anyhow", + "client", + "collections", + "db", + "feature_flags", + "futures 0.3.28", + "gpui", + "image", + "language", + "lazy_static", + "log", + "parking_lot 0.11.2", + "postage", + "rand 0.8.5", + "rpc", + "schemars", + "serde", + "serde_derive", + "settings", + "smol", + "sum_tree", + "tempfile", + "text", + "thiserror", + "time 0.3.27", + "tiny_http", + "url", + "util", + "uuid 1.4.1", +] + [[package]] name = "chrono" version = "0.4.26" @@ -1251,9 +1288,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.3.19" +version = "4.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd304a20bff958a57f04c4e96a2e7594cc4490a0e809cbd48bb6437edaa452d" +checksum = "fb690e81c7840c0d7aade59f242ea3b41b9bc27bcd5997890e7702ae4b32e487" dependencies = [ "clap_builder", "clap_derive 4.3.12", @@ -1262,9 +1299,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.3.19" +version = "4.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c6a3f08f1fe5662a35cfe393aec09c4df95f60ee93b7556505260f75eee9e1" +checksum = "5ed2e96bc16d8d740f6f48d663eddf4b8a0983e79210fd55479b7bcd0a69860e" dependencies = [ "anstream", "anstyle", @@ -1294,7 +1331,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -1337,6 +1374,7 @@ dependencies = [ "async-tungstenite", "collections", "db", + "feature_flags", "futures 0.3.28", "gpui", "image", @@ -1351,11 +1389,11 @@ dependencies = [ "serde_derive", "settings", "smol", - "staff_mode", "sum_tree", "tempfile", + "text", "thiserror", - "time 0.3.24", + "time 0.3.27", "tiny_http", "url", "util", @@ -1409,7 +1447,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.17.0" +version = "0.18.0" dependencies = [ "anyhow", "async-tungstenite", @@ -1418,8 +1456,11 @@ dependencies = [ "axum-extra", "base64 0.13.1", "call", + "channel", "clap 3.2.25", "client", + "clock", + "collab_ui", "collections", "ctor", "dashmap", @@ -1444,6 +1485,7 @@ dependencies = [ "pretty_assertions", "project", "prometheus", + "prost 0.8.0", "rand 0.8.5", "reqwest", "rpc", @@ -1456,8 +1498,9 @@ dependencies = [ "settings", "sha-1 0.9.8", "sqlx", + "text", "theme", - "time 0.3.24", + "time 0.3.27", "tokio", "tokio-tungstenite", "toml 0.5.11", @@ -1478,16 +1521,19 @@ dependencies = [ "anyhow", "auto_update", "call", + "channel", "client", "clock", "collections", "context_menu", "db", "editor", + "feature_flags", "feedback", "futures 0.3.28", "fuzzy", "gpui", + "language", "log", "menu", "picker", @@ -1498,7 +1544,6 @@ dependencies = [ "serde", "serde_derive", "settings", - "staff_mode", "theme", "theme_selector", "util", @@ -1556,6 +1601,19 @@ dependencies = [ "workspace", ] +[[package]] +name = "component_test" +version = "0.1.0" +dependencies = [ + "anyhow", + "gpui", + "project", + "settings", + "theme", + "util", + "workspace", +] + [[package]] name = "concurrent-queue" version = "2.2.0" @@ -1984,7 +2042,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "socket2", + "socket2 0.4.9", "winapi 0.3.9", ] @@ -2005,9 +2063,9 @@ dependencies = [ [[package]] name = "dashmap" -version = "5.5.0" +version = "5.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6943ae99c34386c84a470c499d3414f66502a41340aa895406e0d2e4a207b91d" +checksum = "edd72493923899c6f10c641bdbdeddc7183d6396641d99c1a0d1597f37f92e28" dependencies = [ "cfg-if 1.0.0", "hashbrown 0.14.0", @@ -2065,9 +2123,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.6" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8810e7e2cf385b1e9b50d68264908ec367ba642c96d02edfe61c39e88e2a3c01" +checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" dependencies = [ "serde", ] @@ -2255,9 +2313,9 @@ dependencies = [ [[package]] name = "dyn-clone" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "304e6508efa593091e97a9abbc10f90aa7ca635b6d2784feff3c89d41dd12272" +checksum = "bbfc4744c1b8f2a09adc0e55242f60b1af195d88596bd8700be74418c056c555" [[package]] name = "editor" @@ -2320,9 +2378,9 @@ checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "encoding_rs" -version = "0.8.32" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" dependencies = [ "cfg-if 1.0.0", ] @@ -2370,9 +2428,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "erased-serde" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da96524cc884f6558f1769b6c46686af2fe8e8b4cd253bd5a3cdba8181b8e070" +checksum = "fc978899517288e3ebbd1a3bfc1d9537dbb87eeab149e53ea490e63bcdff561a" dependencies = [ "serde", ] @@ -2471,6 +2529,14 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" +[[package]] +name = "feature_flags" +version = "0.1.0" +dependencies = [ + "anyhow", + "gpui", +] + [[package]] name = "feedback" version = "0.1.0" @@ -2535,13 +2601,13 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.21" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" +checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall 0.2.16", + "redox_syscall 0.3.5", "windows-sys", ] @@ -2553,9 +2619,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" dependencies = [ "crc32fast", "miniz_oxide 0.7.1", @@ -2696,7 +2762,7 @@ dependencies = [ "smol", "sum_tree", "tempfile", - "time 0.3.24", + "time 0.3.27", "util", ] @@ -2834,7 +2900,7 @@ dependencies = [ "futures-io", "memchr", "parking", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "waker-fn", ] @@ -2846,7 +2912,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -2875,7 +2941,7 @@ dependencies = [ "futures-sink", "futures-task", "memchr", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "pin-utils", "slab", "tokio-io", @@ -2953,9 +3019,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.27.3" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" [[package]] name = "git" @@ -2998,11 +3064,11 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "globset" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aca8bbd8e0707c1887a8bbb7e6b40e228f251ff5d62c8220a4a7a53c73aff006" +checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d" dependencies = [ - "aho-corasick 1.0.2", + "aho-corasick 1.0.4", "bstr", "fnv", "log", @@ -3089,7 +3155,7 @@ dependencies = [ "sqlez", "sum_tree", "taffy", - "time 0.3.24", + "time 0.3.27", "tiny-skia", "usvg", "util", @@ -3115,9 +3181,9 @@ checksum = "eec1c01eb1de97451ee0d60de7d81cf1e72aabefb021616027f3d1c3ec1c723c" [[package]] name = "h2" -version = "0.3.20" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" dependencies = [ "bytes 1.4.0", "fnv", @@ -3311,7 +3377,7 @@ checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" dependencies = [ "bytes 1.4.0", "fnv", - "itoa 1.0.9", + "itoa", ] [[package]] @@ -3322,7 +3388,7 @@ checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes 1.4.0", "http", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", ] [[package]] @@ -3339,9 +3405,9 @@ checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "human_bytes" @@ -3370,9 +3436,9 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 1.0.9", - "pin-project-lite 0.2.10", - "socket2", + "itoa", + "pin-project-lite 0.2.12", + "socket2 0.4.9", "tokio", "tower-service", "tracing", @@ -3386,7 +3452,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ "hyper", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tokio", "tokio-io-timeout", ] @@ -3604,7 +3670,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi 0.3.2", - "rustix 0.38.4", + "rustix 0.38.8", "windows-sys", ] @@ -3644,12 +3710,6 @@ dependencies = [ "either", ] -[[package]] -name = "itoa" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" - [[package]] name = "itoa" version = "1.0.9" @@ -4076,9 +4136,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" dependencies = [ "serde", "value-bag", @@ -4109,9 +4169,9 @@ dependencies = [ [[package]] name = "lsp-types" -version = "0.94.0" +version = "0.94.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b63735a13a1f9cd4f4835223d828ed9c2e35c8c5e61837774399f558b6a1237" +checksum = "c66bfd44a06ae10647fe3f8214762e9369fd4248df1350924b4ef9e770a85ea1" dependencies = [ "bitflags 1.3.2", "serde", @@ -4209,9 +4269,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "76fc44e2588d5b436dbc3c6cf62aef290f90dab6235744a93dfe1cc18f451e2c" [[package]] name = "memfd" @@ -4567,9 +4627,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" dependencies = [ "autocfg", "num-integer", @@ -4725,9 +4785,9 @@ dependencies = [ [[package]] name = "object" -version = "0.31.1" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +checksum = "77ac5bbd07aea88c60a577a1ce218075ffd59208b2d7ca97adf9bfc5aeb21ebe" dependencies = [ "memchr", ] @@ -4769,9 +4829,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.55" +version = "0.10.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" +checksum = "729b745ad4a5575dd06a3e1af1414bd330ee561c01b3899eb584baeaa8def17e" dependencies = [ "bitflags 1.3.2", "cfg-if 1.0.0", @@ -4790,7 +4850,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -4801,9 +4861,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.90" +version = "0.9.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" +checksum = "866b5f16f90776b9bb8dc1e1802ac6f0513de3a7a7465867bfbc563dc737faac" dependencies = [ "cc", "libc", @@ -4938,7 +4998,7 @@ dependencies = [ "libc", "redox_syscall 0.3.5", "smallvec", - "windows-targets 0.48.1", + "windows-targets 0.48.5", ] [[package]] @@ -5030,12 +5090,12 @@ dependencies = [ [[package]] name = "petgraph" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ "fixedbitset", - "indexmap 1.9.3", + "indexmap 2.0.0", ] [[package]] @@ -5063,22 +5123,22 @@ checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468" [[package]] name = "pin-project" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -5089,9 +5149,9 @@ checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" [[package]] name = "pin-project-lite" -version = "0.2.10" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" +checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05" [[package]] name = "pin-utils" @@ -5142,7 +5202,7 @@ dependencies = [ "line-wrap", "quick-xml", "serde", - "time 0.3.24", + "time 0.3.27", ] [[package]] @@ -5207,7 +5267,7 @@ dependencies = [ "concurrent-queue", "libc", "log", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "windows-sys", ] @@ -5257,7 +5317,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c64d9ba0963cdcea2e1b2230fbae2bab30eb25a174be395c41e764bfb65dd62" dependencies = [ "proc-macro2", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -5597,9 +5657,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.32" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -5832,13 +5892,13 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.1" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" dependencies = [ - "aho-corasick 1.0.2", + "aho-corasick 1.0.4", "memchr", - "regex-automata 0.3.4", + "regex-automata 0.3.6", "regex-syntax 0.7.4", ] @@ -5853,11 +5913,11 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.4" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7b6d6190b7594385f61bd3911cd1be99dfddcfc365a4160cc2ab5bff4aed294" +checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" dependencies = [ - "aho-corasick 1.0.2", + "aho-corasick 1.0.4", "memchr", "regex-syntax 0.7.4", ] @@ -5906,9 +5966,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.18" +version = "0.11.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +checksum = "20b9b67e2ca7dd9e9f9285b759de30ff538aab981abaaf7bc9bd90b84a0126c3" dependencies = [ "base64 0.21.2", "bytes 1.4.0", @@ -5927,7 +5987,7 @@ dependencies = [ "native-tls", "once_cell", "percent-encoding", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "serde", "serde_json", "serde_urlencoded", @@ -6129,9 +6189,9 @@ dependencies = [ [[package]] name = "rust-embed" -version = "6.8.1" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a36224c3276f8c4ebc8c20f158eca7ca4359c8db89991c4925132aaaf6702661" +checksum = "b1e7d90385b59f0a6bf3d3b757f3ca4ece2048265d70db20a2016043d4509a40" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -6140,22 +6200,22 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "6.8.1" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b94b81e5b2c284684141a2fb9e2a31be90638caf040bf9afbc5a0416afe1ac" +checksum = "3c3d8c6fd84090ae348e63a84336b112b5c3918b3bf0493a581f7bd8ee623c29" dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.28", + "syn 2.0.29", "walkdir", ] [[package]] name = "rust-embed-utils" -version = "7.8.1" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d38ff6bf570dc3bb7100fce9f7b60c33fa71d80e88da3f2580df4ff2bdded74" +checksum = "873feff8cb7bf86fdf0a71bb21c95159f4e4a37dd7a4bd1855a940909b583ada" dependencies = [ "globset", "sha2 0.10.7", @@ -6164,13 +6224,12 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.31.0" +version = "1.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a2ab0025103a60ecaaf3abf24db1db240a4e1c15837090d2c32f625ac98abea" +checksum = "a4c4216490d5a413bc6d10fa4742bd7d4955941d062c0ef873141d6b0e7b30fd" dependencies = [ "arrayvec 0.7.4", "borsh", - "byteorder", "bytes 1.4.0", "num-traits", "rand 0.8.5", @@ -6218,7 +6277,7 @@ dependencies = [ "bitflags 1.3.2", "errno 0.2.8", "io-lifetimes 0.5.3", - "itoa 1.0.9", + "itoa", "libc", "linux-raw-sys 0.0.42", "once_cell", @@ -6241,11 +6300,11 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.4" +version = "0.38.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" +checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" dependencies = [ - "bitflags 2.3.3", + "bitflags 2.4.0", "errno 0.3.2", "libc", "linux-raw-sys 0.4.5", @@ -6447,7 +6506,7 @@ dependencies = [ "serde_json", "sqlx", "thiserror", - "time 0.3.24", + "time 0.3.27", "tracing", "url", "uuid 1.4.1", @@ -6475,7 +6534,7 @@ dependencies = [ "rust_decimal", "sea-query-derive", "serde_json", - "time 0.3.24", + "time 0.3.27", "uuid 1.4.1", ] @@ -6490,7 +6549,7 @@ dependencies = [ "sea-query", "serde_json", "sqlx", - "time 0.3.24", + "time 0.3.27", "uuid 1.4.1", ] @@ -6618,10 +6677,11 @@ dependencies = [ "serde", "serde_json", "settings", + "sha1", "smol", "tempdir", "theme", - "tiktoken-rs 0.5.0", + "tiktoken-rs 0.5.1", "tree-sitter", "tree-sitter-cpp", "tree-sitter-elixir", @@ -6669,22 +6729,22 @@ checksum = "5a9f47faea3cad316faa914d013d24f471cd90bfca1a0c70f05a3f42c6441e99" [[package]] name = "serde" -version = "1.0.180" +version = "1.0.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea67f183f058fe88a4e3ec6e2788e003840893b91bac4559cabedd00863b3ed" +checksum = "be9b6f69f1dfd54c3b568ffa45c310d6973a5e5148fd40cf515acaf38cf5bc31" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.180" +version = "1.0.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24e744d7782b686ab3b73267ef05697159cc0e5abbed3f47f9933165e5219036" +checksum = "dc59dfdcbad1437773485e0367fea4b090a2e0a16d9ffc46af47764536a298ec" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -6709,24 +6769,24 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.104" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" +checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" dependencies = [ "indexmap 2.0.0", - "itoa 1.0.9", + "itoa", "ryu", "serde", ] [[package]] name = "serde_json_lenient" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d7b9ce5b0a63c6269b9623ed828b39259545a6ec0d8a35d6135ad6af6232add" +checksum = "29591aaa3a13f5ad0f2dd1a8a21bcddab11eaae7c3522b20ade2e85e9df52206" dependencies = [ - "indexmap 1.9.3", - "itoa 0.4.8", + "indexmap 2.0.0", + "itoa", "ryu", "serde", ] @@ -6739,7 +6799,7 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -6758,7 +6818,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.9", + "itoa", "ryu", "serde", ] @@ -6781,6 +6841,7 @@ version = "0.1.0" dependencies = [ "anyhow", "collections", + "feature_flags", "fs", "futures 0.3.28", "gpui", @@ -6796,7 +6857,6 @@ dependencies = [ "serde_json_lenient", "smallvec", "sqlez", - "staff_mode", "toml 0.5.11", "tree-sitter", "tree-sitter-json 0.19.0", @@ -6970,9 +7030,9 @@ checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" [[package]] name = "slab" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] @@ -7054,6 +7114,16 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "socket2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "spin" version = "0.5.2" @@ -7153,7 +7223,7 @@ dependencies = [ "hkdf", "hmac 0.12.1", "indexmap 1.9.3", - "itoa 1.0.9", + "itoa", "libc", "libsqlite3-sys", "log", @@ -7176,7 +7246,7 @@ dependencies = [ "sqlx-rt", "stringprep", "thiserror", - "time 0.3.24", + "time 0.3.27", "tokio-stream", "url", "uuid 1.4.1", @@ -7221,14 +7291,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" -[[package]] -name = "staff_mode" -version = "0.1.0" -dependencies = [ - "anyhow", - "gpui", -] - [[package]] name = "static_assertions" version = "1.1.0" @@ -7299,7 +7361,7 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dc09e9364c2045ab5fa38f7b04d077b3359d30c4c2b3ec4bae67a358bd64326" dependencies = [ - "itoa 1.0.9", + "itoa", "ryu", "sval", ] @@ -7310,7 +7372,7 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ada6f627e38cbb8860283649509d87bc4a5771141daa41c78fd31f2b9485888d" dependencies = [ - "itoa 1.0.9", + "itoa", "ryu", "sval", ] @@ -7375,9 +7437,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.28" +version = "2.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" +checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" dependencies = [ "proc-macro2", "quote", @@ -7472,14 +7534,14 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.7.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5486094ee78b2e5038a6382ed7645bc084dc2ec433426ca4c3cb61e2007b8998" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ "cfg-if 1.0.0", "fastrand 2.0.0", "redox_syscall 0.3.5", - "rustix 0.38.4", + "rustix 0.38.8", "windows-sys", ] @@ -7609,6 +7671,7 @@ name = "theme_selector" version = "0.1.0" dependencies = [ "editor", + "feature_flags", "fs", "fuzzy", "gpui", @@ -7618,7 +7681,6 @@ dependencies = [ "postage", "settings", "smol", - "staff_mode", "theme", "util", "workspace", @@ -7626,22 +7688,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.44" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" +checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.44" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" +checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -7688,9 +7750,9 @@ dependencies = [ [[package]] name = "tiktoken-rs" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a99d843674a3468b4a9200a565bbe909a0152f95e82a52feae71e6bf2d4b49d" +checksum = "2bf14cb08d8fda6e484c75ec2bfb6bcef48347d47abcd011fa9d56ee995a3da0" dependencies = [ "anyhow", "base64 0.21.2", @@ -7714,12 +7776,12 @@ dependencies = [ [[package]] name = "time" -version = "0.3.24" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b79eabcd964882a646b3584543ccabeae7869e9ac32a46f6f22b7a5bd405308b" +checksum = "0bb39ee79a6d8de55f48f2293a830e040392f1c5f16e336bdd1788cd0aadce07" dependencies = [ "deranged", - "itoa 1.0.9", + "itoa", "serde", "time-core", "time-macros", @@ -7733,9 +7795,9 @@ checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.11" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb71511c991639bb078fd5bf97757e03914361c48100d52878b8e52b46fb92cd" +checksum = "733d258752e9303d392b94b75230d07b0b9c489350c69b851fc6c065fde3e8f9" dependencies = [ "time-core", ] @@ -7784,20 +7846,19 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.29.1" +version = "1.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" dependencies = [ - "autocfg", "backtrace", "bytes 1.4.0", "libc", "mio 0.8.8", "num_cpus", "parking_lot 0.12.1", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "signal-hook-registry", - "socket2", + "socket2 0.5.3", "tokio-macros", "windows-sys", ] @@ -7819,7 +7880,7 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" dependencies = [ - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tokio", ] @@ -7831,7 +7892,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -7862,7 +7923,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" dependencies = [ "futures-core", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tokio", ] @@ -7888,7 +7949,7 @@ dependencies = [ "futures-core", "futures-sink", "log", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tokio", ] @@ -7902,7 +7963,7 @@ dependencies = [ "futures-core", "futures-io", "futures-sink", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tokio", "tracing", ] @@ -7991,7 +8052,7 @@ dependencies = [ "futures-util", "indexmap 1.9.3", "pin-project", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "rand 0.8.5", "slab", "tokio", @@ -8014,7 +8075,7 @@ dependencies = [ "http", "http-body", "http-range-header", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tower", "tower-layer", "tower-service", @@ -8040,7 +8101,7 @@ checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if 1.0.0", "log", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tracing-attributes", "tracing-core", ] @@ -8053,7 +8114,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -8138,9 +8199,9 @@ dependencies = [ [[package]] name = "tree-sitter-c" -version = "0.20.4" +version = "0.20.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa1bb73a4101c88775e4fefcd0543ee25e192034484a5bd45cb99eefb997dca9" +checksum = "30b03bdf218020057abee831581a74bff8c298323d6c6cd1a70556430ded9f4b" dependencies = [ "cc", "tree-sitter", @@ -8287,9 +8348,9 @@ dependencies = [ [[package]] name = "tree-sitter-python" -version = "0.20.3" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f47ebd9cac632764b2f4389b08517bf2ef895431dd163eb562e3d2062cc23a14" +checksum = "e6c93b1b1fbd0d399db3445f51fd3058e43d0b4dcff62ddbdb46e66550978aa5" dependencies = [ "cc", "tree-sitter", @@ -8316,9 +8377,9 @@ dependencies = [ [[package]] name = "tree-sitter-rust" -version = "0.20.3" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "797842733e252dc11ae5d403a18060bf337b822fc2ae5ddfaa6ff4d9cc20bda6" +checksum = "b0832309b0b2b6d33760ce5c0e818cb47e1d72b468516bfe4134408926fa7594" dependencies = [ "cc", "tree-sitter", @@ -8439,9 +8500,9 @@ checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" [[package]] name = "unicase" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" dependencies = [ "version_check", ] @@ -8847,7 +8908,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", "wasm-bindgen-shared", ] @@ -8881,7 +8942,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -9118,9 +9179,9 @@ dependencies = [ [[package]] name = "wast" -version = "62.0.1" +version = "63.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8ae06f09dbe377b889fbd620ff8fa21e1d49d1d9d364983c0cdbf9870cb9f1f" +checksum = "2560471f60a48b77fccefaf40796fda61c97ce1e790b59dfcec9dc3995c9f63a" dependencies = [ "leb128", "memchr", @@ -9130,11 +9191,11 @@ dependencies = [ [[package]] name = "wat" -version = "1.0.69" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "842e15861d203fb4a96d314b0751cdeaf0f6f8b35e8d81d2953af2af5e44e637" +checksum = "3bdc306c2c4c2f2bf2ba69e083731d0d2a77437fc6a350a19db139636e7e416c" dependencies = [ - "wast 62.0.1", + "wast 63.0.0", ] [[package]] @@ -9336,7 +9397,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets 0.48.1", + "windows-targets 0.48.5", ] [[package]] @@ -9345,7 +9406,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.1", + "windows-targets 0.48.5", ] [[package]] @@ -9365,17 +9426,17 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -9386,9 +9447,9 @@ checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_msvc" @@ -9398,9 +9459,9 @@ checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_i686_gnu" @@ -9410,9 +9471,9 @@ checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_msvc" @@ -9422,9 +9483,9 @@ checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_x86_64_gnu" @@ -9434,9 +9495,9 @@ checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnullvm" @@ -9446,9 +9507,9 @@ checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" @@ -9458,26 +9519,27 @@ checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "winnow" -version = "0.5.2" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bd122eb777186e60c3fdf765a58ac76e41c582f1f535fbf3314434c6b58f3f7" +checksum = "d09770118a7eb1ccaf4a594a221334119a44a814fcb0d31c5b85e83e97227a97" dependencies = [ "memchr", ] [[package]] name = "winreg" -version = "0.10.1" +version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "winapi 0.3.9", + "cfg-if 1.0.0", + "windows-sys", ] [[package]] @@ -9520,6 +9582,7 @@ dependencies = [ "async-recursion 1.0.4", "bincode", "call", + "channel", "client", "collections", "context_menu", @@ -9596,7 +9659,7 @@ name = "xtask" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.3.19", + "clap 4.3.24", "schemars", "serde_json", "theme", @@ -9631,7 +9694,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.101.0" +version = "0.102.0" dependencies = [ "activity_indicator", "ai", @@ -9645,6 +9708,7 @@ dependencies = [ "backtrace", "breadcrumbs", "call", + "channel", "chrono", "cli", "client", @@ -9652,6 +9716,7 @@ dependencies = [ "collab_ui", "collections", "command_palette", + "component_test", "context_menu", "copilot", "copilot_button", @@ -9660,6 +9725,7 @@ dependencies = [ "diagnostics", "editor", "env_logger 0.9.3", + "feature_flags", "feedback", "file_finder", "fs", @@ -9706,7 +9772,6 @@ dependencies = [ "simplelog", "smallvec", "smol", - "staff_mode", "sum_tree", "tempdir", "terminal_view", @@ -9777,7 +9842,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index a60b4eb610c07d4de6c5ea7a6a948486dae7eceb..5938ecb40240765844c1849662b082afeae07a3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "crates/auto_update", "crates/breadcrumbs", "crates/call", + "crates/channel", "crates/cli", "crates/client", "crates/clock", @@ -13,6 +14,7 @@ members = [ "crates/collab_ui", "crates/collections", "crates/command_palette", + "crates/component_test", "crates/context_menu", "crates/copilot", "crates/copilot_button", @@ -60,7 +62,7 @@ members = [ "crates/snippet", "crates/sqlez", "crates/sqlez_macros", - "crates/staff_mode", + "crates/feature_flags", "crates/sum_tree", "crates/terminal", "crates/text", @@ -95,10 +97,11 @@ log = { version = "0.4.16", features = ["kv_unstable_serde"] } ordered-float = { version = "2.1.1" } parking_lot = { version = "0.11.1" } postage = { version = "0.5", features = ["futures-traits"] } +prost = { version = "0.8" } rand = { version = "0.8.5" } refineable = { path = "./crates/refineable" } regex = { version = "1.5" } -rust-embed = { version = "6.3", features = ["include-exclude"] } +rust-embed = { version = "8.0", features = ["include-exclude"] } schemars = { version = "0.8" } serde = { version = "1.0", features = ["derive", "rc"] } serde_derive = { version = "1.0", features = ["deserialize_in_place"] } diff --git a/Dockerfile b/Dockerfile index 77d011490e5821f282240af7d387b19f67a0edbe..208700f7fb5f25d19dc5e5cfd1477f11219c4391 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax = docker/dockerfile:1.2 -FROM rust:1.71-bullseye as builder +FROM rust:1.72-bullseye as builder WORKDIR app COPY . . diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 1bd973e83b8a8bd193b963f50148a9e603f6640c..a1c771da02c156bbb389a1952ac85782225cb9eb 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -521,7 +521,8 @@ "ctrl-alt-cmd-f": "workspace::FollowNextCollaborator", // TODO: Move this to a dock open action "cmd-shift-c": "collab_panel::ToggleFocus", - "cmd-alt-i": "zed::DebugElements" + "cmd-alt-i": "zed::DebugElements", + "ctrl-shift-:": "editor::ToggleInlayHints", } }, { diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index c0de3420f222566220d5f7e487554b3ee5bc33d5..c7e6199f444f6ce09cc1e1d6b39703b91cbd8fd2 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -137,10 +137,67 @@ "partialWord": true } ], + "g j": [ + "vim::Down", + { + "displayLines": true + } + ], + "g down": [ + "vim::Down", + { + "displayLines": true + } + ], + "g k": [ + "vim::Up", + { + "displayLines": true + } + ], + "g up": [ + "vim::Up", + { + "displayLines": true + } + ], + "g $": [ + "vim::EndOfLine", + { + "displayLines": true + } + ], + "g end": [ + "vim::EndOfLine", + { + "displayLines": true + } + ], + "g 0": [ + "vim::StartOfLine", + { + "displayLines": true + } + ], + "g home": [ + "vim::StartOfLine", + { + "displayLines": true + } + ], + "g ^": [ + "vim::FirstNonWhitespace", + { + "displayLines": true + } + ], // z commands "z t": "editor::ScrollCursorTop", "z z": "editor::ScrollCursorCenter", "z b": "editor::ScrollCursorBottom", + "z c": "editor::Fold", + "z o": "editor::UnfoldLines", + "z f": "editor::FoldSelectedRanges", // Count support "1": [ "vim::Number", diff --git a/assets/settings/default.json b/assets/settings/default.json index 24412b883bf0be12cb2639dd54dec7f70adf6882..6739819e713f38f9d0628eaf061bdd2ff509da69 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -98,6 +98,7 @@ // Whether to show selections in the scrollbar. "selections": true }, + "relative_line_numbers": false, // Inlay hint related settings "inlay_hints": { // Global switch to toggle hints on and off, switched off by default. @@ -284,8 +285,6 @@ // "directory": "~/zed/projects/" // } // } - // - // "working_directory": "current_project_directory", // Set the cursor blinking behavior in the terminal. // May take 4 values: @@ -334,13 +333,32 @@ // "line_height": { // "custom": 2 // }, - "line_height": "comfortable" + "line_height": "comfortable", + // Activate the python virtual environment, if one is found, in the + // terminal's working directory (as resolved by the working_directory + // setting). Set this to "off" to disable this behavior. + "detect_venv": { + "on": { + // Default directories to search for virtual environments, relative + // to the current working directory. We recommend overriding this + // in your project's settings, rather than globally. + "directories": [ + ".env", + "env", + ".venv", + "venv" + ], + // Can also be 'csh' and 'fish' + "activate_script": "default" + } + } // Set the terminal's font size. If this option is not included, // the terminal will default to matching the buffer's font size. - // "font_size": "15" + // "font_size": "15", // Set the terminal's font family. If this option is not included, // the terminal will default to matching the buffer's font family. - // "font_family": "Zed Mono" + // "font_family": "Zed Mono", + // --- }, // Difference settings for semantic_index "semantic_index": { diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 81299bbdc26f5001f407901893b7c8d3e0f1b166..3c561b0e03944c5ac1c673704110fbf749617b5c 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -855,14 +855,14 @@ impl Conversation { ) -> Self { let markdown = language_registry.language_for_name("Markdown"); let buffer = cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "", cx); + let mut buffer = Buffer::new(0, cx.model_id() as u64, ""); buffer.set_language_registry(language_registry); cx.spawn_weak(|buffer, mut cx| async move { let markdown = markdown.await?; let buffer = buffer .upgrade(&cx) .ok_or_else(|| anyhow!("buffer was dropped"))?; - buffer.update(&mut cx, |buffer, cx| { + buffer.update(&mut cx, |buffer: &mut Buffer, cx| { buffer.set_language(Some(markdown), cx) }); anyhow::Ok(()) @@ -944,7 +944,7 @@ impl Conversation { let mut message_anchors = Vec::new(); let mut next_message_id = MessageId(0); let buffer = cx.add_model(|cx| { - let mut buffer = Buffer::new(0, saved_conversation.text, cx); + let mut buffer = Buffer::new(0, cx.model_id() as u64, saved_conversation.text); for message in saved_conversation.messages { message_anchors.push(MessageAnchor { id: message.id, @@ -958,7 +958,7 @@ impl Conversation { let buffer = buffer .upgrade(&cx) .ok_or_else(|| anyhow!("buffer was dropped"))?; - buffer.update(&mut cx, |buffer, cx| { + buffer.update(&mut cx, |buffer: &mut Buffer, cx| { buffer.set_language(Some(markdown), cx) }); anyhow::Ok(()) @@ -1128,7 +1128,9 @@ impl Conversation { stream: true, }; - let Some(api_key) = self.api_key.borrow().clone() else { continue }; + let Some(api_key) = self.api_key.borrow().clone() else { + continue; + }; let stream = stream_completion(api_key, cx.background().clone(), request); let assistant_message = self .insert_message_after( @@ -1484,7 +1486,9 @@ impl Conversation { }) { current_message = messages.next(); } - let Some(message) = current_message.as_ref() else { break }; + let Some(message) = current_message.as_ref() else { + break; + }; // Skip offsets that are in the same message. while offsets.peek().map_or(false, |offset| { @@ -1921,7 +1925,10 @@ impl ConversationEditor { let Some(panel) = workspace.panel::(cx) else { return; }; - let Some(editor) = workspace.active_item(cx).and_then(|item| item.act_as::(cx)) else { + let Some(editor) = workspace + .active_item(cx) + .and_then(|item| item.act_as::(cx)) + else { return; }; diff --git a/crates/call/Cargo.toml b/crates/call/Cargo.toml index eb448d8d8d089369c724f49e5911a8946598f8a4..b4e94fe56c3b12533d232eacf30c5edd633d5a03 100644 --- a/crates/call/Cargo.toml +++ b/crates/call/Cargo.toml @@ -20,6 +20,7 @@ test-support = [ [dependencies] audio = { path = "../audio" } +channel = { path = "../channel" } client = { path = "../client" } collections = { path = "../collections" } gpui = { path = "../gpui" } diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 5fef53fa814c00b3d88c05861620b557f9f1802f..5af094df05977c4e72cd36b98445588686f4e896 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -7,9 +7,8 @@ use std::sync::Arc; use anyhow::{anyhow, Result}; use audio::Audio; use call_settings::CallSettings; -use client::{ - proto, ChannelId, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore, -}; +use channel::ChannelId; +use client::{proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore}; use collections::HashSet; use futures::{future::Shared, FutureExt}; use postage::watch; diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 6f01b1d75789ce61d537be2d780f0dbb5960ad17..cc7445dbcc74ff620968c9ff5a2a99686bd800d9 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -644,7 +644,9 @@ impl Room { if let Some(participants) = remote_participants.log_err() { for (participant, user) in room.participants.into_iter().zip(participants) { - let Some(peer_id) = participant.peer_id else { continue }; + let Some(peer_id) = participant.peer_id else { + continue; + }; this.participant_user_ids.insert(participant.user_id); let old_projects = this diff --git a/crates/channel/Cargo.toml b/crates/channel/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..c2191fdfa3edaaf0824e5e59ed974a7c53030ccd --- /dev/null +++ b/crates/channel/Cargo.toml @@ -0,0 +1,51 @@ +[package] +name = "channel" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/channel.rs" +doctest = false + +[features] +test-support = ["collections/test-support", "gpui/test-support", "rpc/test-support"] + +[dependencies] +client = { path = "../client" } +collections = { path = "../collections" } +db = { path = "../db" } +gpui = { path = "../gpui" } +util = { path = "../util" } +rpc = { path = "../rpc" } +text = { path = "../text" } +language = { path = "../language" } +settings = { path = "../settings" } +feature_flags = { path = "../feature_flags" } +sum_tree = { path = "../sum_tree" } + +anyhow.workspace = true +futures.workspace = true +image = "0.23" +lazy_static.workspace = true +log.workspace = true +parking_lot.workspace = true +postage.workspace = true +rand.workspace = true +schemars.workspace = true +smol.workspace = true +thiserror.workspace = true +time.workspace = true +tiny_http = "0.8" +uuid = { version = "1.1.2", features = ["v4"] } +url = "2.2" +serde.workspace = true +serde_derive.workspace = true +tempfile = "3" + +[dev-dependencies] +collections = { path = "../collections", features = ["test-support"] } +gpui = { path = "../gpui", features = ["test-support"] } +rpc = { path = "../rpc", features = ["test-support"] } +settings = { path = "../settings", features = ["test-support"] } +util = { path = "../util", features = ["test-support"] } diff --git a/crates/channel/src/channel.rs b/crates/channel/src/channel.rs new file mode 100644 index 0000000000000000000000000000000000000000..15631b7dd312f36126ec1e13b2413fc01e5ca8af --- /dev/null +++ b/crates/channel/src/channel.rs @@ -0,0 +1,14 @@ +mod channel_store; + +pub mod channel_buffer; +use std::sync::Arc; + +pub use channel_store::*; +use client::Client; + +#[cfg(test)] +mod channel_store_tests; + +pub fn init(client: &Arc) { + channel_buffer::init(client); +} diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs new file mode 100644 index 0000000000000000000000000000000000000000..29f4d3493c6d0fe9e2fc041695f40fe48225c76f --- /dev/null +++ b/crates/channel/src/channel_buffer.rs @@ -0,0 +1,197 @@ +use crate::Channel; +use anyhow::Result; +use client::Client; +use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle}; +use rpc::{proto, TypedEnvelope}; +use std::sync::Arc; +use util::ResultExt; + +pub(crate) fn init(client: &Arc) { + client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer); + client.add_model_message_handler(ChannelBuffer::handle_add_channel_buffer_collaborator); + client.add_model_message_handler(ChannelBuffer::handle_remove_channel_buffer_collaborator); +} + +pub struct ChannelBuffer { + pub(crate) channel: Arc, + connected: bool, + collaborators: Vec, + buffer: ModelHandle, + client: Arc, + subscription: Option, +} + +pub enum Event { + CollaboratorsChanged, + Disconnected, +} + +impl Entity for ChannelBuffer { + type Event = Event; + + fn release(&mut self, _: &mut AppContext) { + if self.connected { + self.client + .send(proto::LeaveChannelBuffer { + channel_id: self.channel.id, + }) + .log_err(); + } + } +} + +impl ChannelBuffer { + pub(crate) async fn new( + channel: Arc, + client: Arc, + mut cx: AsyncAppContext, + ) -> Result> { + let response = client + .request(proto::JoinChannelBuffer { + channel_id: channel.id, + }) + .await?; + + let base_text = response.base_text; + let operations = response + .operations + .into_iter() + .map(language::proto::deserialize_operation) + .collect::, _>>()?; + + let collaborators = response.collaborators; + + let buffer = cx.add_model(|_| { + language::Buffer::remote(response.buffer_id, response.replica_id as u16, base_text) + }); + buffer.update(&mut cx, |buffer, cx| buffer.apply_ops(operations, cx))?; + + let subscription = client.subscribe_to_entity(channel.id)?; + + anyhow::Ok(cx.add_model(|cx| { + cx.subscribe(&buffer, Self::on_buffer_update).detach(); + + Self { + buffer, + client, + connected: true, + collaborators, + channel, + subscription: Some(subscription.set_model(&cx.handle(), &mut cx.to_async())), + } + })) + } + + async fn handle_update_channel_buffer( + this: ModelHandle, + update_channel_buffer: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + let ops = update_channel_buffer + .payload + .operations + .into_iter() + .map(language::proto::deserialize_operation) + .collect::, _>>()?; + + this.update(&mut cx, |this, cx| { + cx.notify(); + this.buffer + .update(cx, |buffer, cx| buffer.apply_ops(ops, cx)) + })?; + + Ok(()) + } + + async fn handle_add_channel_buffer_collaborator( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + let collaborator = envelope.payload.collaborator.ok_or_else(|| { + anyhow::anyhow!( + "Should have gotten a collaborator in the AddChannelBufferCollaborator message" + ) + })?; + + this.update(&mut cx, |this, cx| { + this.collaborators.push(collaborator); + cx.emit(Event::CollaboratorsChanged); + cx.notify(); + }); + + Ok(()) + } + + async fn handle_remove_channel_buffer_collaborator( + this: ModelHandle, + message: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, cx| { + this.collaborators.retain(|collaborator| { + if collaborator.peer_id == message.payload.peer_id { + this.buffer.update(cx, |buffer, cx| { + buffer.remove_peer(collaborator.replica_id as u16, cx) + }); + false + } else { + true + } + }); + cx.emit(Event::CollaboratorsChanged); + cx.notify(); + }); + + Ok(()) + } + + fn on_buffer_update( + &mut self, + _: ModelHandle, + event: &language::Event, + _: &mut ModelContext, + ) { + if let language::Event::Operation(operation) = event { + let operation = language::proto::serialize_operation(operation); + self.client + .send(proto::UpdateChannelBuffer { + channel_id: self.channel.id, + operations: vec![operation], + }) + .log_err(); + } + } + + pub fn buffer(&self) -> ModelHandle { + self.buffer.clone() + } + + pub fn collaborators(&self) -> &[proto::Collaborator] { + &self.collaborators + } + + pub fn channel(&self) -> Arc { + self.channel.clone() + } + + pub(crate) fn disconnect(&mut self, cx: &mut ModelContext) { + if self.connected { + self.connected = false; + self.subscription.take(); + cx.emit(Event::Disconnected); + cx.notify() + } + } + + pub fn is_connected(&self) -> bool { + self.connected + } + + pub fn replica_id(&self, cx: &AppContext) -> u16 { + self.buffer.read(cx).replica_id() + } +} diff --git a/crates/client/src/channel_store.rs b/crates/channel/src/channel_store.rs similarity index 71% rename from crates/client/src/channel_store.rs rename to crates/channel/src/channel_store.rs index 03d334a9defcc05255874c75349f894d1b9bc1f7..861f731331ca6337ed7d798162ceb3321ad170fe 100644 --- a/crates/client/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -1,19 +1,14 @@ -use crate::Status; -use crate::{Client, Subscription, User, UserStore}; -use anyhow::anyhow; -use anyhow::Result; -use collections::HashMap; -use collections::HashSet; -use futures::channel::mpsc; -use futures::Future; -use futures::StreamExt; -use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, Task}; +use crate::channel_buffer::ChannelBuffer; +use anyhow::{anyhow, Result}; +use client::{Client, Status, Subscription, User, UserId, UserStore}; +use collections::{hash_map, HashMap, HashSet}; +use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt}; +use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle}; use rpc::{proto, TypedEnvelope}; use std::sync::Arc; use util::ResultExt; pub type ChannelId = u64; -pub type UserId = u64; pub struct ChannelStore { channels_by_id: HashMap>, @@ -23,6 +18,7 @@ pub struct ChannelStore { channels_with_admin_privileges: HashSet, outgoing_invites: HashSet<(ChannelId, UserId)>, update_channels_tx: mpsc::UnboundedSender, + opened_buffers: HashMap, client: Arc, user_store: ModelHandle, _rpc_subscription: Subscription, @@ -57,6 +53,11 @@ pub enum ChannelMemberStatus { NotMember, } +enum OpenedChannelBuffer { + Open(WeakModelHandle), + Loading(Shared, Arc>>>), +} + impl ChannelStore { pub fn new( client: Arc, @@ -70,16 +71,14 @@ impl ChannelStore { let mut connection_status = client.status(); let watch_connection_status = cx.spawn_weak(|this, mut cx| async move { while let Some(status) = connection_status.next().await { - if matches!(status, Status::ConnectionLost | Status::SignedOut) { + if !status.is_connected() { if let Some(this) = this.upgrade(&cx) { this.update(&mut cx, |this, cx| { - this.channels_by_id.clear(); - this.channel_invitations.clear(); - this.channel_participants.clear(); - this.channels_with_admin_privileges.clear(); - this.channel_paths.clear(); - this.outgoing_invites.clear(); - cx.notify(); + if matches!(status, Status::ConnectionLost | Status::SignedOut) { + this.handle_disconnect(cx); + } else { + this.disconnect_buffers(cx); + } }); } else { break; @@ -87,6 +86,7 @@ impl ChannelStore { } } }); + Self { channels_by_id: HashMap::default(), channel_invitations: Vec::default(), @@ -94,6 +94,7 @@ impl ChannelStore { channel_participants: Default::default(), channels_with_admin_privileges: Default::default(), outgoing_invites: Default::default(), + opened_buffers: Default::default(), update_channels_tx, client, user_store, @@ -114,6 +115,16 @@ impl ChannelStore { } } + pub fn has_children(&self, channel_id: ChannelId) -> bool { + self.channel_paths.iter().any(|path| { + if let Some(ix) = path.iter().position(|id| *id == channel_id) { + path.len() > ix + 1 + } else { + false + } + }) + } + pub fn channel_count(&self) -> usize { self.channel_paths.len() } @@ -141,6 +152,74 @@ impl ChannelStore { self.channels_by_id.get(&channel_id) } + pub fn open_channel_buffer( + &mut self, + channel_id: ChannelId, + cx: &mut ModelContext, + ) -> Task>> { + // Make sure that a given channel buffer is only opened once per + // app instance, even if this method is called multiple times + // with the same channel id while the first task is still running. + let task = loop { + match self.opened_buffers.entry(channel_id) { + hash_map::Entry::Occupied(e) => match e.get() { + OpenedChannelBuffer::Open(buffer) => { + if let Some(buffer) = buffer.upgrade(cx) { + break Task::ready(Ok(buffer)).shared(); + } else { + self.opened_buffers.remove(&channel_id); + continue; + } + } + OpenedChannelBuffer::Loading(task) => break task.clone(), + }, + hash_map::Entry::Vacant(e) => { + let client = self.client.clone(); + let task = cx + .spawn(|this, cx| async move { + let channel = this.read_with(&cx, |this, _| { + this.channel_for_id(channel_id).cloned().ok_or_else(|| { + Arc::new(anyhow!("no channel for id: {}", channel_id)) + }) + })?; + + ChannelBuffer::new(channel, client, cx) + .await + .map_err(Arc::new) + }) + .shared(); + e.insert(OpenedChannelBuffer::Loading(task.clone())); + cx.spawn({ + let task = task.clone(); + |this, mut cx| async move { + let result = task.await; + this.update(&mut cx, |this, cx| match result { + Ok(buffer) => { + cx.observe_release(&buffer, move |this, _, _| { + this.opened_buffers.remove(&channel_id); + }) + .detach(); + this.opened_buffers.insert( + channel_id, + OpenedChannelBuffer::Open(buffer.downgrade()), + ); + } + Err(error) => { + log::error!("failed to open channel buffer {error:?}"); + this.opened_buffers.remove(&channel_id); + } + }); + } + }) + .detach(); + break task; + } + } + }; + cx.foreground() + .spawn(async move { task.await.map_err(|error| anyhow!("{}", error)) }) + } + pub fn is_user_admin(&self, channel_id: ChannelId) -> bool { self.channel_paths.iter().any(|path| { if let Some(ix) = path.iter().position(|id| *id == channel_id) { @@ -403,6 +482,27 @@ impl ChannelStore { Ok(()) } + fn handle_disconnect(&mut self, cx: &mut ModelContext<'_, ChannelStore>) { + self.disconnect_buffers(cx); + self.channels_by_id.clear(); + self.channel_invitations.clear(); + self.channel_participants.clear(); + self.channels_with_admin_privileges.clear(); + self.channel_paths.clear(); + self.outgoing_invites.clear(); + cx.notify(); + } + + fn disconnect_buffers(&mut self, cx: &mut ModelContext) { + for (_, buffer) in self.opened_buffers.drain() { + if let OpenedChannelBuffer::Open(buffer) = buffer { + if let Some(buffer) = buffer.upgrade(cx) { + buffer.update(cx, |buffer, cx| buffer.disconnect(cx)); + } + } + } + } + pub(crate) fn update_channels( &mut self, payload: proto::UpdateChannels, @@ -437,38 +537,44 @@ impl ChannelStore { .retain(|channel_id, _| !payload.remove_channels.contains(channel_id)); self.channels_with_admin_privileges .retain(|channel_id| !payload.remove_channels.contains(channel_id)); - } - for channel in payload.channels { - if let Some(existing_channel) = self.channels_by_id.get_mut(&channel.id) { - // FIXME: We may be missing a path for this existing channel in certain cases - let existing_channel = Arc::make_mut(existing_channel); - existing_channel.name = channel.name; - continue; + for channel_id in &payload.remove_channels { + let channel_id = *channel_id; + if let Some(OpenedChannelBuffer::Open(buffer)) = + self.opened_buffers.remove(&channel_id) + { + if let Some(buffer) = buffer.upgrade(cx) { + buffer.update(cx, ChannelBuffer::disconnect); + } + } } + } - self.channels_by_id.insert( - channel.id, - Arc::new(Channel { - id: channel.id, - name: channel.name, - }), - ); - - if let Some(parent_id) = channel.parent_id { - let mut ix = 0; - while ix < self.channel_paths.len() { - let path = &self.channel_paths[ix]; - if path.ends_with(&[parent_id]) { - let mut new_path = path.clone(); - new_path.push(channel.id); - self.channel_paths.insert(ix + 1, new_path); + for channel_proto in payload.channels { + if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) { + Arc::make_mut(existing_channel).name = channel_proto.name; + } else { + let channel = Arc::new(Channel { + id: channel_proto.id, + name: channel_proto.name, + }); + self.channels_by_id.insert(channel.id, channel.clone()); + + if let Some(parent_id) = channel_proto.parent_id { + let mut ix = 0; + while ix < self.channel_paths.len() { + let path = &self.channel_paths[ix]; + if path.ends_with(&[parent_id]) { + let mut new_path = path.clone(); + new_path.push(channel.id); + self.channel_paths.insert(ix + 1, new_path); + ix += 1; + } ix += 1; } - ix += 1; + } else { + self.channel_paths.push(vec![channel.id]); } - } else { - self.channel_paths.push(vec![channel.id]); } } diff --git a/crates/client/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs similarity index 98% rename from crates/client/src/channel_store_tests.rs rename to crates/channel/src/channel_store_tests.rs index 51e819349e7c665976d4ff0af5f20c7bb32eaff2..18894b1f472f907d3b54ad35df57d78e5e974565 100644 --- a/crates/client/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -1,4 +1,7 @@ use super::*; +use client::{Client, UserStore}; +use gpui::{AppContext, ModelHandle}; +use rpc::proto; use util::http::FakeHttpClient; #[gpui::test] diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 3ecc51598696cd9ec5965c35d346bda069418086..e3038e5bcc49bd41b756062b676e00f4f355867a 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -17,8 +17,9 @@ db = { path = "../db" } gpui = { path = "../gpui" } util = { path = "../util" } rpc = { path = "../rpc" } +text = { path = "../text" } settings = { path = "../settings" } -staff_mode = { path = "../staff_mode" } +feature_flags = { path = "../feature_flags" } sum_tree = { path = "../sum_tree" } anyhow.workspace = true diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 8ef3e32ea8f98b47a744e148f881289934fae215..a32c415f7e9d1b9699b51582f05bee3af06792e2 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1,10 +1,6 @@ #[cfg(any(test, feature = "test-support"))] pub mod test; -#[cfg(test)] -mod channel_store_tests; - -pub mod channel_store; pub mod telemetry; pub mod user; @@ -48,7 +44,6 @@ use util::channel::ReleaseChannel; use util::http::HttpClient; use util::{ResultExt, TryFutureExt}; -pub use channel_store::*; pub use rpc::*; pub use telemetry::ClickhouseEvent; pub use user::*; diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 48886377ba56ad046027da5cf7a754bdc22cea72..9cc5d13af0c72d84bdad54c88d97e2e9ce2586df 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -135,8 +135,6 @@ impl Telemetry { } } - /// This method takes the entire TelemetrySettings struct in order to force client code - /// to pull the struct out of the settings global. Do not remove! pub fn set_authenticated_user_info( self: &Arc, metrics_id: Option, diff --git a/crates/client/src/test.rs b/crates/client/src/test.rs index 4c12a205660f7932a6a7b412c6ee686a6199372c..00e7cd1508613c60a05ddbba8cabff86bbaf1d14 100644 --- a/crates/client/src/test.rs +++ b/crates/client/src/test.rs @@ -168,6 +168,7 @@ impl FakeServer { GetPrivateUserInfoResponse { metrics_id: "the-metrics-id".into(), staff: false, + flags: Default::default(), }, ) .await; diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index be11d1fb442b8d69228f867156b3b7e6c08b0b66..5f13aa40acee9063bfd90c10b43044ff40952db2 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -1,18 +1,20 @@ use super::{proto, Client, Status, TypedEnvelope}; use anyhow::{anyhow, Context, Result}; use collections::{hash_map::Entry, HashMap, HashSet}; +use feature_flags::FeatureFlagAppExt; use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt}; use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task}; use postage::{sink::Sink, watch}; use rpc::proto::{RequestMessage, UsersResponse}; -use staff_mode::StaffMode; use std::sync::{Arc, Weak}; use util::http::HttpClient; use util::TryFutureExt as _; +pub type UserId = u64; + #[derive(Default, Debug)] pub struct User { - pub id: u64, + pub id: UserId, pub github_login: String, pub avatar: Option>, } @@ -143,26 +145,23 @@ impl UserStore { let fetch_metrics_id = client.request(proto::GetPrivateUserInfo {}).log_err(); let (user, info) = futures::join!(fetch_user, fetch_metrics_id); - cx.read(|cx| { - client.telemetry.set_authenticated_user_info( - info.as_ref().map(|info| info.metrics_id.clone()), - info.as_ref().map(|info| info.staff).unwrap_or(false), - cx, - ) - }); - cx.update(|cx| { - cx.update_default_global(|staff_mode: &mut StaffMode, _| { - if !staff_mode.0 { - *staff_mode = StaffMode( - info.as_ref() - .map(|info| info.staff) - .unwrap_or_default(), - ) - } - () + if let Some(info) = info { + cx.update(|cx| { + cx.update_flags(info.staff, info.flags); + client.telemetry.set_authenticated_user_info( + Some(info.metrics_id.clone()), + info.staff, + cx, + ) }); - }); + } else { + cx.read(|cx| { + client + .telemetry + .set_authenticated_user_info(None, false, cx) + }); + } current_user_tx.send(user).await.ok(); diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index fc8c1644cd7d127876f168ab2a32c17f5e2b114c..8adc38615c3cab87d36a348323e0c3674f555d6c 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.17.0" +version = "0.18.0" publish = false [[bin]] @@ -14,8 +14,10 @@ name = "seed" required-features = ["seed-support"] [dependencies] +clock = { path = "../clock" } collections = { path = "../collections" } live_kit_server = { path = "../live_kit_server" } +text = { path = "../text" } rpc = { path = "../rpc" } util = { path = "../util" } @@ -35,6 +37,7 @@ log.workspace = true nanoid = "0.4" parking_lot.workspace = true prometheus = "0.13" +prost.workspace = true rand.workspace = true reqwest = { version = "0.11", features = ["json"], optional = true } scrypt = "0.7" @@ -62,6 +65,7 @@ collections = { path = "../collections", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } call = { path = "../call", features = ["test-support"] } client = { path = "../client", features = ["test-support"] } +channel = { path = "../channel" } editor = { path = "../editor", features = ["test-support"] } language = { path = "../language", features = ["test-support"] } fs = { path = "../fs", features = ["test-support"] } @@ -74,6 +78,7 @@ rpc = { path = "../rpc", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"] } theme = { path = "../theme" } workspace = { path = "../workspace", features = ["test-support"] } +collab_ui = { path = "../collab_ui", features = ["test-support"] } ctor.workspace = true env_logger.workspace = true diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 3dceaecef4e15a3fcbc221102110ee441b876832..80477dcb3c3b9f4fc1efd25622243b59901cf4fc 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -208,3 +208,63 @@ CREATE TABLE "channel_members" ( ); CREATE UNIQUE INDEX "index_channel_members_on_channel_id_and_user_id" ON "channel_members" ("channel_id", "user_id"); + +CREATE TABLE "buffers" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, + "epoch" INTEGER NOT NULL DEFAULT 0 +); + +CREATE INDEX "index_buffers_on_channel_id" ON "buffers" ("channel_id"); + +CREATE TABLE "buffer_operations" ( + "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, + "epoch" INTEGER NOT NULL, + "replica_id" INTEGER NOT NULL, + "lamport_timestamp" INTEGER NOT NULL, + "value" BLOB NOT NULL, + PRIMARY KEY(buffer_id, epoch, lamport_timestamp, replica_id) +); + +CREATE TABLE "buffer_snapshots" ( + "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, + "epoch" INTEGER NOT NULL, + "text" TEXT NOT NULL, + "operation_serialization_version" INTEGER NOT NULL, + PRIMARY KEY(buffer_id, epoch) +); + +CREATE TABLE "channel_buffer_collaborators" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, + "connection_id" INTEGER NOT NULL, + "connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE, + "connection_lost" BOOLEAN NOT NULL DEFAULT false, + "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, + "replica_id" INTEGER NOT NULL +); + +CREATE INDEX "index_channel_buffer_collaborators_on_channel_id" ON "channel_buffer_collaborators" ("channel_id"); +CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_and_replica_id" ON "channel_buffer_collaborators" ("channel_id", "replica_id"); +CREATE INDEX "index_channel_buffer_collaborators_on_connection_server_id" ON "channel_buffer_collaborators" ("connection_server_id"); +CREATE INDEX "index_channel_buffer_collaborators_on_connection_id" ON "channel_buffer_collaborators" ("connection_id"); +CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_connection_id_and_server_id" ON "channel_buffer_collaborators" ("channel_id", "connection_id", "connection_server_id"); + + +CREATE TABLE "feature_flags" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "flag" TEXT NOT NULL UNIQUE +); + +CREATE INDEX "index_feature_flags" ON "feature_flags" ("id"); + + +CREATE TABLE "user_features" ( + "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, + "feature_id" INTEGER NOT NULL REFERENCES feature_flags (id) ON DELETE CASCADE, + PRIMARY KEY (user_id, feature_id) +); + +CREATE UNIQUE INDEX "index_user_features_user_id_and_feature_id" ON "user_features" ("user_id", "feature_id"); +CREATE INDEX "index_user_features_on_user_id" ON "user_features" ("user_id"); +CREATE INDEX "index_user_features_on_feature_id" ON "user_features" ("feature_id"); diff --git a/crates/collab/migrations/20230819154600_add_channel_buffers.sql b/crates/collab/migrations/20230819154600_add_channel_buffers.sql new file mode 100644 index 0000000000000000000000000000000000000000..5e6e7ce3393a628c86cbcdabf2349ebfa6667bd6 --- /dev/null +++ b/crates/collab/migrations/20230819154600_add_channel_buffers.sql @@ -0,0 +1,40 @@ +CREATE TABLE "buffers" ( + "id" SERIAL PRIMARY KEY, + "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, + "epoch" INTEGER NOT NULL DEFAULT 0 +); + +CREATE INDEX "index_buffers_on_channel_id" ON "buffers" ("channel_id"); + +CREATE TABLE "buffer_operations" ( + "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, + "epoch" INTEGER NOT NULL, + "replica_id" INTEGER NOT NULL, + "lamport_timestamp" INTEGER NOT NULL, + "value" BYTEA NOT NULL, + PRIMARY KEY(buffer_id, epoch, lamport_timestamp, replica_id) +); + +CREATE TABLE "buffer_snapshots" ( + "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, + "epoch" INTEGER NOT NULL, + "text" TEXT NOT NULL, + "operation_serialization_version" INTEGER NOT NULL, + PRIMARY KEY(buffer_id, epoch) +); + +CREATE TABLE "channel_buffer_collaborators" ( + "id" SERIAL PRIMARY KEY, + "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, + "connection_id" INTEGER NOT NULL, + "connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE, + "connection_lost" BOOLEAN NOT NULL DEFAULT FALSE, + "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, + "replica_id" INTEGER NOT NULL +); + +CREATE INDEX "index_channel_buffer_collaborators_on_channel_id" ON "channel_buffer_collaborators" ("channel_id"); +CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_and_replica_id" ON "channel_buffer_collaborators" ("channel_id", "replica_id"); +CREATE INDEX "index_channel_buffer_collaborators_on_connection_server_id" ON "channel_buffer_collaborators" ("connection_server_id"); +CREATE INDEX "index_channel_buffer_collaborators_on_connection_id" ON "channel_buffer_collaborators" ("connection_id"); +CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_connection_id_and_server_id" ON "channel_buffer_collaborators" ("channel_id", "connection_id", "connection_server_id"); diff --git a/crates/collab/migrations/20230825190322_add_server_feature_flags.sql b/crates/collab/migrations/20230825190322_add_server_feature_flags.sql new file mode 100644 index 0000000000000000000000000000000000000000..fffde54a20e4869ccbef2093de4e7fe5044132e2 --- /dev/null +++ b/crates/collab/migrations/20230825190322_add_server_feature_flags.sql @@ -0,0 +1,16 @@ +CREATE TABLE "feature_flags" ( + "id" SERIAL PRIMARY KEY, + "flag" VARCHAR(255) NOT NULL UNIQUE +); + +CREATE UNIQUE INDEX "index_feature_flags" ON "feature_flags" ("id"); + +CREATE TABLE "user_features" ( + "user_id" INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + "feature_id" INTEGER NOT NULL REFERENCES feature_flags(id) ON DELETE CASCADE, + PRIMARY KEY (user_id, feature_id) +); + +CREATE UNIQUE INDEX "index_user_features_user_id_and_feature_id" ON "user_features" ("user_id", "feature_id"); +CREATE INDEX "index_user_features_on_user_id" ON "user_features" ("user_id"); +CREATE INDEX "index_user_features_on_feature_id" ON "user_features" ("feature_id"); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index d322b0358936932f6df8222a558cf9e4e34272a0..888158188f35d82443e6a88b2237793d2dcdc016 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1,7 +1,8 @@ #[cfg(test)] -mod db_tests; +pub mod tests; + #[cfg(test)] -pub mod test_db; +pub use tests::TestDb; mod ids; mod queries; @@ -52,6 +53,8 @@ pub struct Database { runtime: Option, } +// The `Database` type has so many methods that its impl blocks are split into +// separate files in the `queries` folder. impl Database { pub async fn new(options: ConnectOptions, executor: Executor) -> Result { Ok(Self { @@ -246,7 +249,9 @@ impl Database { let mut tx = Arc::new(Some(tx)); let result = f(TransactionHandle(tx.clone())).await; let Some(tx) = Arc::get_mut(&mut tx).and_then(|tx| tx.take()) else { - return Err(anyhow!("couldn't complete transaction because it's still in use"))?; + return Err(anyhow!( + "couldn't complete transaction because it's still in use" + ))?; }; Ok((tx, result)) diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index 514c973dad9ea5e42423de3ebdf0df271f949f78..b33ea57183b8771792ea50c6b3ab2b2631971194 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -110,6 +110,7 @@ fn value_to_integer(v: Value) -> Result { } } +id_type!(BufferId); id_type!(AccessTokenId); id_type!(ChannelId); id_type!(ChannelMemberId); @@ -123,3 +124,5 @@ id_type!(ReplicaId); id_type!(ServerId); id_type!(SignupId); id_type!(UserId); +id_type!(ChannelBufferCollaboratorId); +id_type!(FlagId); diff --git a/crates/collab/src/db/queries.rs b/crates/collab/src/db/queries.rs index f67bde30b8a795efc51aadaa0248571798a60710..09a8f073b469f72773a0220750f5d65cf85629af 100644 --- a/crates/collab/src/db/queries.rs +++ b/crates/collab/src/db/queries.rs @@ -1,6 +1,7 @@ use super::*; pub mod access_tokens; +pub mod buffers; pub mod channels; pub mod contacts; pub mod projects; diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs new file mode 100644 index 0000000000000000000000000000000000000000..354accc01a237057a6ea3bbeb9b4c1986b4ea391 --- /dev/null +++ b/crates/collab/src/db/queries/buffers.rs @@ -0,0 +1,588 @@ +use super::*; +use prost::Message; +use text::{EditOperation, InsertionTimestamp, UndoOperation}; + +impl Database { + pub async fn join_channel_buffer( + &self, + channel_id: ChannelId, + user_id: UserId, + connection: ConnectionId, + ) -> Result { + self.transaction(|tx| async move { + let tx = tx; + + self.check_user_is_channel_member(channel_id, user_id, &tx) + .await?; + + let buffer = channel::Model { + id: channel_id, + ..Default::default() + } + .find_related(buffer::Entity) + .one(&*tx) + .await?; + + let buffer = if let Some(buffer) = buffer { + buffer + } else { + let buffer = buffer::ActiveModel { + channel_id: ActiveValue::Set(channel_id), + ..Default::default() + } + .insert(&*tx) + .await?; + buffer_snapshot::ActiveModel { + buffer_id: ActiveValue::Set(buffer.id), + epoch: ActiveValue::Set(0), + text: ActiveValue::Set(String::new()), + operation_serialization_version: ActiveValue::Set( + storage::SERIALIZATION_VERSION, + ), + } + .insert(&*tx) + .await?; + buffer + }; + + // Join the collaborators + let mut collaborators = channel_buffer_collaborator::Entity::find() + .filter(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)) + .all(&*tx) + .await?; + let replica_ids = collaborators + .iter() + .map(|c| c.replica_id) + .collect::>(); + let mut replica_id = ReplicaId(0); + while replica_ids.contains(&replica_id) { + replica_id.0 += 1; + } + let collaborator = channel_buffer_collaborator::ActiveModel { + channel_id: ActiveValue::Set(channel_id), + connection_id: ActiveValue::Set(connection.id as i32), + connection_server_id: ActiveValue::Set(ServerId(connection.owner_id as i32)), + user_id: ActiveValue::Set(user_id), + replica_id: ActiveValue::Set(replica_id), + ..Default::default() + } + .insert(&*tx) + .await?; + collaborators.push(collaborator); + + // Assemble the buffer state + let (base_text, operations) = self.get_buffer_state(&buffer, &tx).await?; + + Ok(proto::JoinChannelBufferResponse { + buffer_id: buffer.id.to_proto(), + replica_id: replica_id.to_proto() as u32, + base_text, + operations, + collaborators: collaborators + .into_iter() + .map(|collaborator| proto::Collaborator { + peer_id: Some(collaborator.connection().into()), + user_id: collaborator.user_id.to_proto(), + replica_id: collaborator.replica_id.0 as u32, + }) + .collect(), + }) + }) + .await + } + + pub async fn leave_channel_buffer( + &self, + channel_id: ChannelId, + connection: ConnectionId, + ) -> Result> { + self.transaction(|tx| async move { + self.leave_channel_buffer_internal(channel_id, connection, &*tx) + .await + }) + .await + } + + pub async fn leave_channel_buffer_internal( + &self, + channel_id: ChannelId, + connection: ConnectionId, + tx: &DatabaseTransaction, + ) -> Result> { + let result = channel_buffer_collaborator::Entity::delete_many() + .filter( + Condition::all() + .add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)) + .add(channel_buffer_collaborator::Column::ConnectionId.eq(connection.id as i32)) + .add( + channel_buffer_collaborator::Column::ConnectionServerId + .eq(connection.owner_id as i32), + ), + ) + .exec(&*tx) + .await?; + if result.rows_affected == 0 { + Err(anyhow!("not a collaborator on this project"))?; + } + + let mut connections = Vec::new(); + let mut rows = channel_buffer_collaborator::Entity::find() + .filter( + Condition::all().add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)), + ) + .stream(&*tx) + .await?; + while let Some(row) = rows.next().await { + let row = row?; + connections.push(ConnectionId { + id: row.connection_id as u32, + owner_id: row.connection_server_id.0 as u32, + }); + } + + drop(rows); + + if connections.is_empty() { + self.snapshot_buffer(channel_id, &tx).await?; + } + + Ok(connections) + } + + pub async fn leave_channel_buffers( + &self, + connection: ConnectionId, + ) -> Result)>> { + self.transaction(|tx| async move { + #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)] + enum QueryChannelIds { + ChannelId, + } + + let channel_ids: Vec = channel_buffer_collaborator::Entity::find() + .select_only() + .column(channel_buffer_collaborator::Column::ChannelId) + .filter(Condition::all().add( + channel_buffer_collaborator::Column::ConnectionId.eq(connection.id as i32), + )) + .into_values::<_, QueryChannelIds>() + .all(&*tx) + .await?; + + let mut result = Vec::new(); + for channel_id in channel_ids { + let collaborators = self + .leave_channel_buffer_internal(channel_id, connection, &*tx) + .await?; + result.push((channel_id, collaborators)); + } + + Ok(result) + }) + .await + } + + #[cfg(debug_assertions)] + pub async fn get_channel_buffer_collaborators( + &self, + channel_id: ChannelId, + ) -> Result> { + self.transaction(|tx| async move { + #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)] + enum QueryUserIds { + UserId, + } + + let users: Vec = channel_buffer_collaborator::Entity::find() + .select_only() + .column(channel_buffer_collaborator::Column::UserId) + .filter( + Condition::all() + .add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)), + ) + .into_values::<_, QueryUserIds>() + .all(&*tx) + .await?; + + Ok(users) + }) + .await + } + + pub async fn update_channel_buffer( + &self, + channel_id: ChannelId, + user: UserId, + operations: &[proto::Operation], + ) -> Result> { + self.transaction(move |tx| async move { + self.check_user_is_channel_member(channel_id, user, &*tx) + .await?; + + let buffer = buffer::Entity::find() + .filter(buffer::Column::ChannelId.eq(channel_id)) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such buffer"))?; + + #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)] + enum QueryVersion { + OperationSerializationVersion, + } + + let serialization_version: i32 = buffer + .find_related(buffer_snapshot::Entity) + .select_only() + .column(buffer_snapshot::Column::OperationSerializationVersion) + .filter(buffer_snapshot::Column::Epoch.eq(buffer.epoch)) + .into_values::<_, QueryVersion>() + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("missing buffer snapshot"))?; + + let operations = operations + .iter() + .filter_map(|op| operation_to_storage(op, &buffer, serialization_version)) + .collect::>(); + if !operations.is_empty() { + buffer_operation::Entity::insert_many(operations) + .exec(&*tx) + .await?; + } + + let mut connections = Vec::new(); + let mut rows = channel_buffer_collaborator::Entity::find() + .filter( + Condition::all() + .add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)), + ) + .stream(&*tx) + .await?; + while let Some(row) = rows.next().await { + let row = row?; + connections.push(ConnectionId { + id: row.connection_id as u32, + owner_id: row.connection_server_id.0 as u32, + }); + } + + Ok(connections) + }) + .await + } + + async fn get_buffer_state( + &self, + buffer: &buffer::Model, + tx: &DatabaseTransaction, + ) -> Result<(String, Vec)> { + let id = buffer.id; + let (base_text, version) = if buffer.epoch > 0 { + let snapshot = buffer_snapshot::Entity::find() + .filter( + buffer_snapshot::Column::BufferId + .eq(id) + .and(buffer_snapshot::Column::Epoch.eq(buffer.epoch)), + ) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such snapshot"))?; + + let version = snapshot.operation_serialization_version; + (snapshot.text, version) + } else { + (String::new(), storage::SERIALIZATION_VERSION) + }; + + let mut rows = buffer_operation::Entity::find() + .filter( + buffer_operation::Column::BufferId + .eq(id) + .and(buffer_operation::Column::Epoch.eq(buffer.epoch)), + ) + .stream(&*tx) + .await?; + let mut operations = Vec::new(); + while let Some(row) = rows.next().await { + let row = row?; + + let operation = operation_from_storage(row, version)?; + operations.push(proto::Operation { + variant: Some(operation), + }) + } + + Ok((base_text, operations)) + } + + async fn snapshot_buffer(&self, channel_id: ChannelId, tx: &DatabaseTransaction) -> Result<()> { + let buffer = channel::Model { + id: channel_id, + ..Default::default() + } + .find_related(buffer::Entity) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such buffer"))?; + + let (base_text, operations) = self.get_buffer_state(&buffer, tx).await?; + if operations.is_empty() { + return Ok(()); + } + + let mut text_buffer = text::Buffer::new(0, 0, base_text); + text_buffer + .apply_ops(operations.into_iter().filter_map(operation_from_wire)) + .unwrap(); + + let base_text = text_buffer.text(); + let epoch = buffer.epoch + 1; + + buffer_snapshot::Model { + buffer_id: buffer.id, + epoch, + text: base_text, + operation_serialization_version: storage::SERIALIZATION_VERSION, + } + .into_active_model() + .insert(tx) + .await?; + + buffer::ActiveModel { + id: ActiveValue::Unchanged(buffer.id), + epoch: ActiveValue::Set(epoch), + ..Default::default() + } + .save(tx) + .await?; + + Ok(()) + } +} + +fn operation_to_storage( + operation: &proto::Operation, + buffer: &buffer::Model, + _format: i32, +) -> Option { + let (replica_id, lamport_timestamp, value) = match operation.variant.as_ref()? { + proto::operation::Variant::Edit(operation) => ( + operation.replica_id, + operation.lamport_timestamp, + storage::Operation { + local_timestamp: operation.local_timestamp, + version: version_to_storage(&operation.version), + is_undo: false, + edit_ranges: operation + .ranges + .iter() + .map(|range| storage::Range { + start: range.start, + end: range.end, + }) + .collect(), + edit_texts: operation.new_text.clone(), + undo_counts: Vec::new(), + }, + ), + proto::operation::Variant::Undo(operation) => ( + operation.replica_id, + operation.lamport_timestamp, + storage::Operation { + local_timestamp: operation.local_timestamp, + version: version_to_storage(&operation.version), + is_undo: true, + edit_ranges: Vec::new(), + edit_texts: Vec::new(), + undo_counts: operation + .counts + .iter() + .map(|entry| storage::UndoCount { + replica_id: entry.replica_id, + local_timestamp: entry.local_timestamp, + count: entry.count, + }) + .collect(), + }, + ), + _ => None?, + }; + + Some(buffer_operation::ActiveModel { + buffer_id: ActiveValue::Set(buffer.id), + epoch: ActiveValue::Set(buffer.epoch), + replica_id: ActiveValue::Set(replica_id as i32), + lamport_timestamp: ActiveValue::Set(lamport_timestamp as i32), + value: ActiveValue::Set(value.encode_to_vec()), + }) +} + +fn operation_from_storage( + row: buffer_operation::Model, + _format_version: i32, +) -> Result { + let operation = + storage::Operation::decode(row.value.as_slice()).map_err(|error| anyhow!("{}", error))?; + let version = version_from_storage(&operation.version); + Ok(if operation.is_undo { + proto::operation::Variant::Undo(proto::operation::Undo { + replica_id: row.replica_id as u32, + local_timestamp: operation.local_timestamp as u32, + lamport_timestamp: row.lamport_timestamp as u32, + version, + counts: operation + .undo_counts + .iter() + .map(|entry| proto::UndoCount { + replica_id: entry.replica_id, + local_timestamp: entry.local_timestamp, + count: entry.count, + }) + .collect(), + }) + } else { + proto::operation::Variant::Edit(proto::operation::Edit { + replica_id: row.replica_id as u32, + local_timestamp: operation.local_timestamp as u32, + lamport_timestamp: row.lamport_timestamp as u32, + version, + ranges: operation + .edit_ranges + .into_iter() + .map(|range| proto::Range { + start: range.start, + end: range.end, + }) + .collect(), + new_text: operation.edit_texts, + }) + }) +} + +fn version_to_storage(version: &Vec) -> Vec { + version + .iter() + .map(|entry| storage::VectorClockEntry { + replica_id: entry.replica_id, + timestamp: entry.timestamp, + }) + .collect() +} + +fn version_from_storage(version: &Vec) -> Vec { + version + .iter() + .map(|entry| proto::VectorClockEntry { + replica_id: entry.replica_id, + timestamp: entry.timestamp, + }) + .collect() +} + +// This is currently a manual copy of the deserialization code in the client's langauge crate +pub fn operation_from_wire(operation: proto::Operation) -> Option { + match operation.variant? { + proto::operation::Variant::Edit(edit) => Some(text::Operation::Edit(EditOperation { + timestamp: InsertionTimestamp { + replica_id: edit.replica_id as text::ReplicaId, + local: edit.local_timestamp, + lamport: edit.lamport_timestamp, + }, + version: version_from_wire(&edit.version), + ranges: edit + .ranges + .into_iter() + .map(|range| { + text::FullOffset(range.start as usize)..text::FullOffset(range.end as usize) + }) + .collect(), + new_text: edit.new_text.into_iter().map(Arc::from).collect(), + })), + proto::operation::Variant::Undo(undo) => Some(text::Operation::Undo { + lamport_timestamp: clock::Lamport { + replica_id: undo.replica_id as text::ReplicaId, + value: undo.lamport_timestamp, + }, + undo: UndoOperation { + id: clock::Local { + replica_id: undo.replica_id as text::ReplicaId, + value: undo.local_timestamp, + }, + version: version_from_wire(&undo.version), + counts: undo + .counts + .into_iter() + .map(|c| { + ( + clock::Local { + replica_id: c.replica_id as text::ReplicaId, + value: c.local_timestamp, + }, + c.count, + ) + }) + .collect(), + }, + }), + _ => None, + } +} + +fn version_from_wire(message: &[proto::VectorClockEntry]) -> clock::Global { + let mut version = clock::Global::new(); + for entry in message { + version.observe(clock::Local { + replica_id: entry.replica_id as text::ReplicaId, + value: entry.timestamp, + }); + } + version +} + +mod storage { + #![allow(non_snake_case)] + use prost::Message; + pub const SERIALIZATION_VERSION: i32 = 1; + + #[derive(Message)] + pub struct Operation { + #[prost(uint32, tag = "1")] + pub local_timestamp: u32, + #[prost(message, repeated, tag = "2")] + pub version: Vec, + #[prost(bool, tag = "3")] + pub is_undo: bool, + #[prost(message, repeated, tag = "4")] + pub edit_ranges: Vec, + #[prost(string, repeated, tag = "5")] + pub edit_texts: Vec, + #[prost(message, repeated, tag = "6")] + pub undo_counts: Vec, + } + + #[derive(Message)] + pub struct VectorClockEntry { + #[prost(uint32, tag = "1")] + pub replica_id: u32, + #[prost(uint32, tag = "2")] + pub timestamp: u32, + } + + #[derive(Message)] + pub struct Range { + #[prost(uint64, tag = "1")] + pub start: u64, + #[prost(uint64, tag = "2")] + pub end: u64, + } + + #[derive(Message)] + pub struct UndoCount { + #[prost(uint32, tag = "1")] + pub replica_id: u32, + #[prost(uint32, tag = "2")] + pub local_timestamp: u32, + #[prost(uint32, tag = "3")] + pub count: u32, + } +} diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index ee79f2cb4f9347c0a12750009d3e3628a6d99c47..435e729fed38c2b5fcd4775e0de92ced74ee0b13 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -465,9 +465,9 @@ impl Database { let mut rejoined_projects = Vec::new(); for rejoined_project in &rejoin_room.rejoined_projects { let project_id = ProjectId::from_proto(rejoined_project.id); - let Some(project) = project::Entity::find_by_id(project_id) - .one(&*tx) - .await? else { continue }; + let Some(project) = project::Entity::find_by_id(project_id).one(&*tx).await? else { + continue; + }; let mut worktrees = Vec::new(); let db_worktrees = project.find_related(worktree::Entity).all(&*tx).await?; @@ -903,15 +903,35 @@ impl Database { ), ) .one(&*tx) - .await? - .ok_or_else(|| anyhow!("not a participant in any room"))?; + .await?; - room_participant::Entity::update(room_participant::ActiveModel { - answering_connection_lost: ActiveValue::set(true), - ..participant.into_active_model() - }) - .exec(&*tx) - .await?; + if let Some(participant) = participant { + room_participant::Entity::update(room_participant::ActiveModel { + answering_connection_lost: ActiveValue::set(true), + ..participant.into_active_model() + }) + .exec(&*tx) + .await?; + } + + channel_buffer_collaborator::Entity::update_many() + .filter( + Condition::all() + .add( + channel_buffer_collaborator::Column::ConnectionId + .eq(connection.id as i32), + ) + .add( + channel_buffer_collaborator::Column::ConnectionServerId + .eq(connection.owner_id as i32), + ), + ) + .set(channel_buffer_collaborator::ActiveModel { + connection_lost: ActiveValue::set(true), + ..Default::default() + }) + .exec(&*tx) + .await?; Ok(()) }) diff --git a/crates/collab/src/db/queries/users.rs b/crates/collab/src/db/queries/users.rs index bac0f14f8324126fe4f403887aa4eb65e4241de2..bd7c3e9ffd62dea8b0d283fb1c6e1c26e8958d2b 100644 --- a/crates/collab/src/db/queries/users.rs +++ b/crates/collab/src/db/queries/users.rs @@ -240,4 +240,58 @@ impl Database { result.push('%'); result } + + #[cfg(debug_assertions)] + pub async fn create_user_flag(&self, flag: &str) -> Result { + self.transaction(|tx| async move { + let flag = feature_flag::Entity::insert(feature_flag::ActiveModel { + flag: ActiveValue::set(flag.to_string()), + ..Default::default() + }) + .exec(&*tx) + .await? + .last_insert_id; + + Ok(flag) + }) + .await + } + + #[cfg(debug_assertions)] + pub async fn add_user_flag(&self, user: UserId, flag: FlagId) -> Result<()> { + self.transaction(|tx| async move { + user_feature::Entity::insert(user_feature::ActiveModel { + user_id: ActiveValue::set(user), + feature_id: ActiveValue::set(flag), + }) + .exec(&*tx) + .await?; + + Ok(()) + }) + .await + } + + pub async fn get_user_flags(&self, user: UserId) -> Result> { + self.transaction(|tx| async move { + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryAs { + Flag, + } + + let flags = user::Model { + id: user, + ..Default::default() + } + .find_linked(user::UserFlags) + .select_only() + .column(feature_flag::Column::Flag) + .into_values::<_, QueryAs>() + .all(&*tx) + .await?; + + Ok(flags) + }) + .await + } } diff --git a/crates/collab/src/db/tables.rs b/crates/collab/src/db/tables.rs index c4c7e4f312afa8bf067fa1778c9c706de0cdb3af..1765cee065fb6c7ae31818568a229e3c3c0bd3f0 100644 --- a/crates/collab/src/db/tables.rs +++ b/crates/collab/src/db/tables.rs @@ -1,8 +1,13 @@ pub mod access_token; +pub mod buffer; +pub mod buffer_operation; +pub mod buffer_snapshot; pub mod channel; +pub mod channel_buffer_collaborator; pub mod channel_member; pub mod channel_path; pub mod contact; +pub mod feature_flag; pub mod follower; pub mod language_server; pub mod project; @@ -12,6 +17,7 @@ pub mod room_participant; pub mod server; pub mod signup; pub mod user; +pub mod user_feature; pub mod worktree; pub mod worktree_diagnostic_summary; pub mod worktree_entry; diff --git a/crates/collab/src/db/tables/buffer.rs b/crates/collab/src/db/tables/buffer.rs new file mode 100644 index 0000000000000000000000000000000000000000..ec2ffd4a68d1958370be7632c943c26432a7c902 --- /dev/null +++ b/crates/collab/src/db/tables/buffer.rs @@ -0,0 +1,45 @@ +use crate::db::{BufferId, ChannelId}; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "buffers")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: BufferId, + pub epoch: i32, + pub channel_id: ChannelId, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::buffer_operation::Entity")] + Operations, + #[sea_orm(has_many = "super::buffer_snapshot::Entity")] + Snapshots, + #[sea_orm( + belongs_to = "super::channel::Entity", + from = "Column::ChannelId", + to = "super::channel::Column::Id" + )] + Channel, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Operations.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Snapshots.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Channel.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tables/buffer_operation.rs b/crates/collab/src/db/tables/buffer_operation.rs new file mode 100644 index 0000000000000000000000000000000000000000..37bd4bedfebf1d0090e27017d19bf22ea8fa38b5 --- /dev/null +++ b/crates/collab/src/db/tables/buffer_operation.rs @@ -0,0 +1,34 @@ +use crate::db::BufferId; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "buffer_operations")] +pub struct Model { + #[sea_orm(primary_key)] + pub buffer_id: BufferId, + #[sea_orm(primary_key)] + pub epoch: i32, + #[sea_orm(primary_key)] + pub lamport_timestamp: i32, + #[sea_orm(primary_key)] + pub replica_id: i32, + pub value: Vec, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::buffer::Entity", + from = "Column::BufferId", + to = "super::buffer::Column::Id" + )] + Buffer, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Buffer.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tables/buffer_snapshot.rs b/crates/collab/src/db/tables/buffer_snapshot.rs new file mode 100644 index 0000000000000000000000000000000000000000..c9de665e438ef373e43b02cc98e7b3ec61640267 --- /dev/null +++ b/crates/collab/src/db/tables/buffer_snapshot.rs @@ -0,0 +1,31 @@ +use crate::db::BufferId; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "buffer_snapshots")] +pub struct Model { + #[sea_orm(primary_key)] + pub buffer_id: BufferId, + #[sea_orm(primary_key)] + pub epoch: i32, + pub text: String, + pub operation_serialization_version: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::buffer::Entity", + from = "Column::BufferId", + to = "super::buffer::Column::Id" + )] + Buffer, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Buffer.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tables/channel.rs b/crates/collab/src/db/tables/channel.rs index f00b4ced627884d3d5c627ab515c61f9bdb0a53d..05895ede4cf6b5080889cf281a1ce3651aebd1c2 100644 --- a/crates/collab/src/db/tables/channel.rs +++ b/crates/collab/src/db/tables/channel.rs @@ -15,8 +15,12 @@ impl ActiveModelBehavior for ActiveModel {} pub enum Relation { #[sea_orm(has_one = "super::room::Entity")] Room, + #[sea_orm(has_one = "super::buffer::Entity")] + Buffer, #[sea_orm(has_many = "super::channel_member::Entity")] Member, + #[sea_orm(has_many = "super::channel_buffer_collaborator::Entity")] + BufferCollaborators, } impl Related for Entity { @@ -30,3 +34,15 @@ impl Related for Entity { Relation::Room.def() } } + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Buffer.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::BufferCollaborators.def() + } +} diff --git a/crates/collab/src/db/tables/channel_buffer_collaborator.rs b/crates/collab/src/db/tables/channel_buffer_collaborator.rs new file mode 100644 index 0000000000000000000000000000000000000000..ac2637b36e3129d1c4582c869db398088b9055d9 --- /dev/null +++ b/crates/collab/src/db/tables/channel_buffer_collaborator.rs @@ -0,0 +1,43 @@ +use crate::db::{ChannelBufferCollaboratorId, ChannelId, ReplicaId, ServerId, UserId}; +use rpc::ConnectionId; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "channel_buffer_collaborators")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: ChannelBufferCollaboratorId, + pub channel_id: ChannelId, + pub connection_id: i32, + pub connection_server_id: ServerId, + pub connection_lost: bool, + pub user_id: UserId, + pub replica_id: ReplicaId, +} + +impl Model { + pub fn connection(&self) -> ConnectionId { + ConnectionId { + owner_id: self.connection_server_id.0 as u32, + id: self.connection_id as u32, + } + } +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::channel::Entity", + from = "Column::ChannelId", + to = "super::channel::Column::Id" + )] + Channel, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Channel.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tables/feature_flag.rs b/crates/collab/src/db/tables/feature_flag.rs new file mode 100644 index 0000000000000000000000000000000000000000..41c1451c648e7115165a2cf3bfc4e84d9ae534a1 --- /dev/null +++ b/crates/collab/src/db/tables/feature_flag.rs @@ -0,0 +1,40 @@ +use sea_orm::entity::prelude::*; + +use crate::db::FlagId; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "feature_flags")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: FlagId, + pub flag: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::user_feature::Entity")] + UserFeature, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::UserFeature.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +pub struct FlaggedUsers; + +impl Linked for FlaggedUsers { + type FromEntity = Entity; + + type ToEntity = super::user::Entity; + + fn link(&self) -> Vec { + vec![ + super::user_feature::Relation::Flag.def().rev(), + super::user_feature::Relation::User.def(), + ] + } +} diff --git a/crates/collab/src/db/tables/user.rs b/crates/collab/src/db/tables/user.rs index 402b06c2a71a164c9f9314ec0d9e4aa5519156c7..739693527f00a594f3376a6093dc8c0b1d270a8f 100644 --- a/crates/collab/src/db/tables/user.rs +++ b/crates/collab/src/db/tables/user.rs @@ -28,6 +28,8 @@ pub enum Relation { HostedProjects, #[sea_orm(has_many = "super::channel_member::Entity")] ChannelMemberships, + #[sea_orm(has_many = "super::user_feature::Entity")] + UserFeatures, } impl Related for Entity { @@ -54,4 +56,25 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::UserFeatures.def() + } +} + impl ActiveModelBehavior for ActiveModel {} + +pub struct UserFlags; + +impl Linked for UserFlags { + type FromEntity = Entity; + + type ToEntity = super::feature_flag::Entity; + + fn link(&self) -> Vec { + vec![ + super::user_feature::Relation::User.def().rev(), + super::user_feature::Relation::Flag.def(), + ] + } +} diff --git a/crates/collab/src/db/tables/user_feature.rs b/crates/collab/src/db/tables/user_feature.rs new file mode 100644 index 0000000000000000000000000000000000000000..cc24b5e796342f7733f59933362d46a0df2be112 --- /dev/null +++ b/crates/collab/src/db/tables/user_feature.rs @@ -0,0 +1,42 @@ +use sea_orm::entity::prelude::*; + +use crate::db::{FlagId, UserId}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "user_features")] +pub struct Model { + #[sea_orm(primary_key)] + pub user_id: UserId, + #[sea_orm(primary_key)] + pub feature_id: FlagId, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::feature_flag::Entity", + from = "Column::FeatureId", + to = "super::feature_flag::Column::Id" + )] + Flag, + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::UserId", + to = "super::user::Column::Id" + )] + User, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Flag.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/test_db.rs b/crates/collab/src/db/tests.rs similarity index 84% rename from crates/collab/src/db/test_db.rs rename to crates/collab/src/db/tests.rs index 064f85c7007fed04c23de5b7c7dadc246294ab29..ee961006cbbf74b019141c0973aca18d73309012 100644 --- a/crates/collab/src/db/test_db.rs +++ b/crates/collab/src/db/tests.rs @@ -1,3 +1,7 @@ +mod buffer_tests; +mod db_tests; +mod feature_flag_tests; + use super::*; use gpui::executor::Background; use parking_lot::Mutex; @@ -91,6 +95,26 @@ impl TestDb { } } +#[macro_export] +macro_rules! test_both_dbs { + ($test_name:ident, $postgres_test_name:ident, $sqlite_test_name:ident) => { + #[gpui::test] + async fn $postgres_test_name() { + let test_db = crate::db::TestDb::postgres( + gpui::executor::Deterministic::new(0).build_background(), + ); + $test_name(test_db.db()).await; + } + + #[gpui::test] + async fn $sqlite_test_name() { + let test_db = + crate::db::TestDb::sqlite(gpui::executor::Deterministic::new(0).build_background()); + $test_name(test_db.db()).await; + } + }; +} + impl Drop for TestDb { fn drop(&mut self) { let db = self.db.take().unwrap(); diff --git a/crates/collab/src/db/tests/buffer_tests.rs b/crates/collab/src/db/tests/buffer_tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..e71748b88b0571c2ccd7840924b8fc325cd368b7 --- /dev/null +++ b/crates/collab/src/db/tests/buffer_tests.rs @@ -0,0 +1,165 @@ +use super::*; +use crate::test_both_dbs; +use language::proto; +use text::Buffer; + +test_both_dbs!( + test_channel_buffers, + test_channel_buffers_postgres, + test_channel_buffers_sqlite +); + +async fn test_channel_buffers(db: &Arc) { + let a_id = db + .create_user( + "user_a@example.com", + false, + NewUserParams { + github_login: "user_a".into(), + github_user_id: 101, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + let b_id = db + .create_user( + "user_b@example.com", + false, + NewUserParams { + github_login: "user_b".into(), + github_user_id: 102, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + // This user will not be a part of the channel + let c_id = db + .create_user( + "user_c@example.com", + false, + NewUserParams { + github_login: "user_c".into(), + github_user_id: 102, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let owner_id = db.create_server("production").await.unwrap().0 as u32; + + let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap(); + + db.invite_channel_member(zed_id, b_id, a_id, false) + .await + .unwrap(); + + db.respond_to_channel_invite(zed_id, b_id, true) + .await + .unwrap(); + + let connection_id_a = ConnectionId { owner_id, id: 1 }; + let _ = db + .join_channel_buffer(zed_id, a_id, connection_id_a) + .await + .unwrap(); + + let mut buffer_a = Buffer::new(0, 0, "".to_string()); + let mut operations = Vec::new(); + operations.push(buffer_a.edit([(0..0, "hello world")])); + operations.push(buffer_a.edit([(5..5, ", cruel")])); + operations.push(buffer_a.edit([(0..5, "goodbye")])); + operations.push(buffer_a.undo().unwrap().1); + assert_eq!(buffer_a.text(), "hello, cruel world"); + + let operations = operations + .into_iter() + .map(|op| proto::serialize_operation(&language::Operation::Buffer(op))) + .collect::>(); + + db.update_channel_buffer(zed_id, a_id, &operations) + .await + .unwrap(); + + let connection_id_b = ConnectionId { owner_id, id: 2 }; + let buffer_response_b = db + .join_channel_buffer(zed_id, b_id, connection_id_b) + .await + .unwrap(); + + let mut buffer_b = Buffer::new(0, 0, buffer_response_b.base_text); + buffer_b + .apply_ops(buffer_response_b.operations.into_iter().map(|operation| { + let operation = proto::deserialize_operation(operation).unwrap(); + if let language::Operation::Buffer(operation) = operation { + operation + } else { + unreachable!() + } + })) + .unwrap(); + + assert_eq!(buffer_b.text(), "hello, cruel world"); + + // Ensure that C fails to open the buffer + assert!(db + .join_channel_buffer(zed_id, c_id, ConnectionId { owner_id, id: 3 }) + .await + .is_err()); + + // Ensure that both collaborators have shown up + assert_eq!( + buffer_response_b.collaborators, + &[ + rpc::proto::Collaborator { + user_id: a_id.to_proto(), + peer_id: Some(rpc::proto::PeerId { id: 1, owner_id }), + replica_id: 0, + }, + rpc::proto::Collaborator { + user_id: b_id.to_proto(), + peer_id: Some(rpc::proto::PeerId { id: 2, owner_id }), + replica_id: 1, + } + ] + ); + + // Ensure that get_channel_buffer_collaborators works + let zed_collaborats = db.get_channel_buffer_collaborators(zed_id).await.unwrap(); + assert_eq!(zed_collaborats, &[a_id, b_id]); + + let collaborators = db + .leave_channel_buffer(zed_id, connection_id_b) + .await + .unwrap(); + + assert_eq!(collaborators, &[connection_id_a],); + + let cargo_id = db.create_root_channel("cargo", "2", a_id).await.unwrap(); + let _ = db + .join_channel_buffer(cargo_id, a_id, connection_id_a) + .await + .unwrap(); + + db.leave_channel_buffers(connection_id_a).await.unwrap(); + + let zed_collaborators = db.get_channel_buffer_collaborators(zed_id).await.unwrap(); + let cargo_collaborators = db.get_channel_buffer_collaborators(cargo_id).await.unwrap(); + assert_eq!(zed_collaborators, &[]); + assert_eq!(cargo_collaborators, &[]); + + // When everyone has left the channel, the operations are collapsed into + // a new base text. + let buffer_response_b = db + .join_channel_buffer(zed_id, b_id, connection_id_b) + .await + .unwrap(); + assert_eq!(buffer_response_b.base_text, "hello, cruel world"); + assert_eq!(buffer_response_b.operations, &[]); +} diff --git a/crates/collab/src/db/db_tests.rs b/crates/collab/src/db/tests/db_tests.rs similarity index 65% rename from crates/collab/src/db/db_tests.rs rename to crates/collab/src/db/tests/db_tests.rs index 8e9a80dbabfa50c580e3bc296315cf7993e6ad40..fc31ee7c4d4aee8dddc46bf6cc0e77fc89e4dd39 100644 --- a/crates/collab/src/db/db_tests.rs +++ b/crates/collab/src/db/tests/db_tests.rs @@ -1,242 +1,234 @@ use super::*; +use crate::test_both_dbs; use gpui::executor::{Background, Deterministic}; use pretty_assertions::{assert_eq, assert_ne}; use std::sync::Arc; -use test_db::TestDb; - -macro_rules! test_both_dbs { - ($postgres_test_name:ident, $sqlite_test_name:ident, $db:ident, $body:block) => { - #[gpui::test] - async fn $postgres_test_name() { - let test_db = TestDb::postgres(Deterministic::new(0).build_background()); - let $db = test_db.db(); - $body - } - - #[gpui::test] - async fn $sqlite_test_name() { - let test_db = TestDb::sqlite(Deterministic::new(0).build_background()); - let $db = test_db.db(); - $body - } - }; -} +use tests::TestDb; test_both_dbs!( + test_get_users, test_get_users_by_ids_postgres, - test_get_users_by_ids_sqlite, - db, - { - let mut user_ids = Vec::new(); - let mut user_metric_ids = Vec::new(); - for i in 1..=4 { - let user = db - .create_user( - &format!("user{i}@example.com"), - false, - NewUserParams { - github_login: format!("user{i}"), - github_user_id: i, - invite_count: 0, - }, - ) - .await - .unwrap(); - user_ids.push(user.user_id); - user_metric_ids.push(user.metrics_id); - } - - assert_eq!( - db.get_users_by_ids(user_ids.clone()).await.unwrap(), - vec![ - User { - id: user_ids[0], - github_login: "user1".to_string(), - github_user_id: Some(1), - email_address: Some("user1@example.com".to_string()), - admin: false, - metrics_id: user_metric_ids[0].parse().unwrap(), - ..Default::default() - }, - User { - id: user_ids[1], - github_login: "user2".to_string(), - github_user_id: Some(2), - email_address: Some("user2@example.com".to_string()), - admin: false, - metrics_id: user_metric_ids[1].parse().unwrap(), - ..Default::default() - }, - User { - id: user_ids[2], - github_login: "user3".to_string(), - github_user_id: Some(3), - email_address: Some("user3@example.com".to_string()), - admin: false, - metrics_id: user_metric_ids[2].parse().unwrap(), - ..Default::default() - }, - User { - id: user_ids[3], - github_login: "user4".to_string(), - github_user_id: Some(4), - email_address: Some("user4@example.com".to_string()), - admin: false, - metrics_id: user_metric_ids[3].parse().unwrap(), - ..Default::default() - } - ] - ); - } + test_get_users_by_ids_sqlite ); -test_both_dbs!( - test_get_or_create_user_by_github_account_postgres, - test_get_or_create_user_by_github_account_sqlite, - db, - { - let user_id1 = db - .create_user( - "user1@example.com", - false, - NewUserParams { - github_login: "login1".into(), - github_user_id: 101, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - let user_id2 = db +async fn test_get_users(db: &Arc) { + let mut user_ids = Vec::new(); + let mut user_metric_ids = Vec::new(); + for i in 1..=4 { + let user = db .create_user( - "user2@example.com", + &format!("user{i}@example.com"), false, NewUserParams { - github_login: "login2".into(), - github_user_id: 102, + github_login: format!("user{i}"), + github_user_id: i, invite_count: 0, }, ) .await - .unwrap() - .user_id; - - let user = db - .get_or_create_user_by_github_account("login1", None, None) - .await - .unwrap() .unwrap(); - assert_eq!(user.id, user_id1); - assert_eq!(&user.github_login, "login1"); - assert_eq!(user.github_user_id, Some(101)); - - assert!(db - .get_or_create_user_by_github_account("non-existent-login", None, None) - .await - .unwrap() - .is_none()); + user_ids.push(user.user_id); + user_metric_ids.push(user.metrics_id); + } - let user = db - .get_or_create_user_by_github_account("the-new-login2", Some(102), None) - .await - .unwrap() - .unwrap(); - assert_eq!(user.id, user_id2); - assert_eq!(&user.github_login, "the-new-login2"); - assert_eq!(user.github_user_id, Some(102)); + assert_eq!( + db.get_users_by_ids(user_ids.clone()).await.unwrap(), + vec![ + User { + id: user_ids[0], + github_login: "user1".to_string(), + github_user_id: Some(1), + email_address: Some("user1@example.com".to_string()), + admin: false, + metrics_id: user_metric_ids[0].parse().unwrap(), + ..Default::default() + }, + User { + id: user_ids[1], + github_login: "user2".to_string(), + github_user_id: Some(2), + email_address: Some("user2@example.com".to_string()), + admin: false, + metrics_id: user_metric_ids[1].parse().unwrap(), + ..Default::default() + }, + User { + id: user_ids[2], + github_login: "user3".to_string(), + github_user_id: Some(3), + email_address: Some("user3@example.com".to_string()), + admin: false, + metrics_id: user_metric_ids[2].parse().unwrap(), + ..Default::default() + }, + User { + id: user_ids[3], + github_login: "user4".to_string(), + github_user_id: Some(4), + email_address: Some("user4@example.com".to_string()), + admin: false, + metrics_id: user_metric_ids[3].parse().unwrap(), + ..Default::default() + } + ] + ); +} - let user = db - .get_or_create_user_by_github_account("login3", Some(103), Some("user3@example.com")) - .await - .unwrap() - .unwrap(); - assert_eq!(&user.github_login, "login3"); - assert_eq!(user.github_user_id, Some(103)); - assert_eq!(user.email_address, Some("user3@example.com".into())); - } +test_both_dbs!( + test_get_or_create_user_by_github_account, + test_get_or_create_user_by_github_account_postgres, + test_get_or_create_user_by_github_account_sqlite ); +async fn test_get_or_create_user_by_github_account(db: &Arc) { + let user_id1 = db + .create_user( + "user1@example.com", + false, + NewUserParams { + github_login: "login1".into(), + github_user_id: 101, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + let user_id2 = db + .create_user( + "user2@example.com", + false, + NewUserParams { + github_login: "login2".into(), + github_user_id: 102, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let user = db + .get_or_create_user_by_github_account("login1", None, None) + .await + .unwrap() + .unwrap(); + assert_eq!(user.id, user_id1); + assert_eq!(&user.github_login, "login1"); + assert_eq!(user.github_user_id, Some(101)); + + assert!(db + .get_or_create_user_by_github_account("non-existent-login", None, None) + .await + .unwrap() + .is_none()); + + let user = db + .get_or_create_user_by_github_account("the-new-login2", Some(102), None) + .await + .unwrap() + .unwrap(); + assert_eq!(user.id, user_id2); + assert_eq!(&user.github_login, "the-new-login2"); + assert_eq!(user.github_user_id, Some(102)); + + let user = db + .get_or_create_user_by_github_account("login3", Some(103), Some("user3@example.com")) + .await + .unwrap() + .unwrap(); + assert_eq!(&user.github_login, "login3"); + assert_eq!(user.github_user_id, Some(103)); + assert_eq!(user.email_address, Some("user3@example.com".into())); +} + test_both_dbs!( + test_create_access_tokens, test_create_access_tokens_postgres, - test_create_access_tokens_sqlite, - db, - { - let user = db - .create_user( - "u1@example.com", - false, - NewUserParams { - github_login: "u1".into(), - github_user_id: 1, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - - let token_1 = db.create_access_token(user, "h1", 2).await.unwrap(); - let token_2 = db.create_access_token(user, "h2", 2).await.unwrap(); - assert_eq!( - db.get_access_token(token_1).await.unwrap(), - access_token::Model { - id: token_1, - user_id: user, - hash: "h1".into(), - } - ); - assert_eq!( - db.get_access_token(token_2).await.unwrap(), - access_token::Model { - id: token_2, - user_id: user, - hash: "h2".into() - } - ); + test_create_access_tokens_sqlite +); - let token_3 = db.create_access_token(user, "h3", 2).await.unwrap(); - assert_eq!( - db.get_access_token(token_3).await.unwrap(), - access_token::Model { - id: token_3, - user_id: user, - hash: "h3".into() - } - ); - assert_eq!( - db.get_access_token(token_2).await.unwrap(), - access_token::Model { - id: token_2, - user_id: user, - hash: "h2".into() - } - ); - assert!(db.get_access_token(token_1).await.is_err()); - - let token_4 = db.create_access_token(user, "h4", 2).await.unwrap(); - assert_eq!( - db.get_access_token(token_4).await.unwrap(), - access_token::Model { - id: token_4, - user_id: user, - hash: "h4".into() - } - ); - assert_eq!( - db.get_access_token(token_3).await.unwrap(), - access_token::Model { - id: token_3, - user_id: user, - hash: "h3".into() - } - ); - assert!(db.get_access_token(token_2).await.is_err()); - assert!(db.get_access_token(token_1).await.is_err()); - } +async fn test_create_access_tokens(db: &Arc) { + let user = db + .create_user( + "u1@example.com", + false, + NewUserParams { + github_login: "u1".into(), + github_user_id: 1, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let token_1 = db.create_access_token(user, "h1", 2).await.unwrap(); + let token_2 = db.create_access_token(user, "h2", 2).await.unwrap(); + assert_eq!( + db.get_access_token(token_1).await.unwrap(), + access_token::Model { + id: token_1, + user_id: user, + hash: "h1".into(), + } + ); + assert_eq!( + db.get_access_token(token_2).await.unwrap(), + access_token::Model { + id: token_2, + user_id: user, + hash: "h2".into() + } + ); + + let token_3 = db.create_access_token(user, "h3", 2).await.unwrap(); + assert_eq!( + db.get_access_token(token_3).await.unwrap(), + access_token::Model { + id: token_3, + user_id: user, + hash: "h3".into() + } + ); + assert_eq!( + db.get_access_token(token_2).await.unwrap(), + access_token::Model { + id: token_2, + user_id: user, + hash: "h2".into() + } + ); + assert!(db.get_access_token(token_1).await.is_err()); + + let token_4 = db.create_access_token(user, "h4", 2).await.unwrap(); + assert_eq!( + db.get_access_token(token_4).await.unwrap(), + access_token::Model { + id: token_4, + user_id: user, + hash: "h4".into() + } + ); + assert_eq!( + db.get_access_token(token_3).await.unwrap(), + access_token::Model { + id: token_3, + user_id: user, + hash: "h3".into() + } + ); + assert!(db.get_access_token(token_2).await.is_err()); + assert!(db.get_access_token(token_1).await.is_err()); +} + +test_both_dbs!( + test_add_contacts, + test_add_contacts_postgres, + test_add_contacts_sqlite ); -test_both_dbs!(test_add_contacts_postgres, test_add_contacts_sqlite, db, { +async fn test_add_contacts(db: &Arc) { let mut user_ids = Vec::new(); for i in 0..3 { user_ids.push( @@ -403,9 +395,15 @@ test_both_dbs!(test_add_contacts_postgres, test_add_contacts_sqlite, db, { busy: false, }], ); -}); +} -test_both_dbs!(test_metrics_id_postgres, test_metrics_id_sqlite, db, { +test_both_dbs!( + test_metrics_id, + test_metrics_id_postgres, + test_metrics_id_sqlite +); + +async fn test_metrics_id(db: &Arc) { let NewUserResult { user_id: user1, metrics_id: metrics_id1, @@ -444,82 +442,83 @@ test_both_dbs!(test_metrics_id_postgres, test_metrics_id_sqlite, db, { assert_eq!(metrics_id1.len(), 36); assert_eq!(metrics_id2.len(), 36); assert_ne!(metrics_id1, metrics_id2); -}); +} test_both_dbs!( + test_project_count, test_project_count_postgres, - test_project_count_sqlite, - db, - { - let owner_id = db.create_server("test").await.unwrap().0 as u32; + test_project_count_sqlite +); - let user1 = db - .create_user( - &format!("admin@example.com"), - true, - NewUserParams { - github_login: "admin".into(), - github_user_id: 0, - invite_count: 0, - }, - ) - .await - .unwrap(); - let user2 = db - .create_user( - &format!("user@example.com"), - false, - NewUserParams { - github_login: "user".into(), - github_user_id: 1, - invite_count: 0, - }, - ) - .await - .unwrap(); +async fn test_project_count(db: &Arc) { + let owner_id = db.create_server("test").await.unwrap().0 as u32; - let room_id = RoomId::from_proto( - db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "") - .await - .unwrap() - .id, - ); - db.call( - room_id, - user1.user_id, - ConnectionId { owner_id, id: 0 }, - user2.user_id, - None, + let user1 = db + .create_user( + &format!("admin@example.com"), + true, + NewUserParams { + github_login: "admin".into(), + github_user_id: 0, + invite_count: 0, + }, + ) + .await + .unwrap(); + let user2 = db + .create_user( + &format!("user@example.com"), + false, + NewUserParams { + github_login: "user".into(), + github_user_id: 1, + invite_count: 0, + }, ) .await .unwrap(); - db.join_room(room_id, user2.user_id, ConnectionId { owner_id, id: 1 }) - .await - .unwrap(); - assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0); - db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[]) + let room_id = RoomId::from_proto( + db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "") .await - .unwrap(); - assert_eq!(db.project_count_excluding_admins().await.unwrap(), 1); + .unwrap() + .id, + ); + db.call( + room_id, + user1.user_id, + ConnectionId { owner_id, id: 0 }, + user2.user_id, + None, + ) + .await + .unwrap(); + db.join_room(room_id, user2.user_id, ConnectionId { owner_id, id: 1 }) + .await + .unwrap(); + assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0); - db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[]) - .await - .unwrap(); - assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2); + db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[]) + .await + .unwrap(); + assert_eq!(db.project_count_excluding_admins().await.unwrap(), 1); - // Projects shared by admins aren't counted. - db.share_project(room_id, ConnectionId { owner_id, id: 0 }, &[]) - .await - .unwrap(); - assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2); + db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[]) + .await + .unwrap(); + assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2); - db.leave_room(ConnectionId { owner_id, id: 1 }) - .await - .unwrap(); - assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0); - } -); + // Projects shared by admins aren't counted. + db.share_project(room_id, ConnectionId { owner_id, id: 0 }, &[]) + .await + .unwrap(); + assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2); + + db.leave_room(ConnectionId { owner_id, id: 1 }) + .await + .unwrap(); + assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0); +} #[test] fn test_fuzzy_like_string() { @@ -878,7 +877,9 @@ async fn test_invite_codes() { assert!(db.has_contact(user5, user1).await.unwrap()); } -test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, { +test_both_dbs!(test_channels, test_channels_postgres, test_channels_sqlite); + +async fn test_channels(db: &Arc) { let a_id = db .create_user( "user1@example.com", @@ -1063,267 +1064,270 @@ test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, { assert!(db.get_channel(rust_id, a_id).await.unwrap().is_none()); assert!(db.get_channel(cargo_id, a_id).await.unwrap().is_none()); assert!(db.get_channel(cargo_ra_id, a_id).await.unwrap().is_none()); -}); +} test_both_dbs!( + test_joining_channels, test_joining_channels_postgres, - test_joining_channels_sqlite, - db, - { - let owner_id = db.create_server("test").await.unwrap().0 as u32; + test_joining_channels_sqlite +); - let user_1 = db - .create_user( - "user1@example.com", - false, - NewUserParams { - github_login: "user1".into(), - github_user_id: 5, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - let user_2 = db - .create_user( - "user2@example.com", - false, - NewUserParams { - github_login: "user2".into(), - github_user_id: 6, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; +async fn test_joining_channels(db: &Arc) { + let owner_id = db.create_server("test").await.unwrap().0 as u32; - let channel_1 = db - .create_root_channel("channel_1", "1", user_1) - .await - .unwrap(); - let room_1 = db.room_id_for_channel(channel_1).await.unwrap(); + let user_1 = db + .create_user( + "user1@example.com", + false, + NewUserParams { + github_login: "user1".into(), + github_user_id: 5, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + let user_2 = db + .create_user( + "user2@example.com", + false, + NewUserParams { + github_login: "user2".into(), + github_user_id: 6, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; - // can join a room with membership to its channel - let joined_room = db - .join_room(room_1, user_1, ConnectionId { owner_id, id: 1 }) - .await - .unwrap(); - assert_eq!(joined_room.room.participants.len(), 1); + let channel_1 = db + .create_root_channel("channel_1", "1", user_1) + .await + .unwrap(); + let room_1 = db.room_id_for_channel(channel_1).await.unwrap(); - drop(joined_room); - // cannot join a room without membership to its channel - assert!(db - .join_room(room_1, user_2, ConnectionId { owner_id, id: 1 }) - .await - .is_err()); - } -); + // can join a room with membership to its channel + let joined_room = db + .join_room(room_1, user_1, ConnectionId { owner_id, id: 1 }) + .await + .unwrap(); + assert_eq!(joined_room.room.participants.len(), 1); + + drop(joined_room); + // cannot join a room without membership to its channel + assert!(db + .join_room(room_1, user_2, ConnectionId { owner_id, id: 1 }) + .await + .is_err()); +} test_both_dbs!( + test_channel_invites, test_channel_invites_postgres, - test_channel_invites_sqlite, - db, - { - db.create_server("test").await.unwrap(); + test_channel_invites_sqlite +); - let user_1 = db - .create_user( - "user1@example.com", - false, - NewUserParams { - github_login: "user1".into(), - github_user_id: 5, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - let user_2 = db - .create_user( - "user2@example.com", - false, - NewUserParams { - github_login: "user2".into(), - github_user_id: 6, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; +async fn test_channel_invites(db: &Arc) { + db.create_server("test").await.unwrap(); - let user_3 = db - .create_user( - "user3@example.com", - false, - NewUserParams { - github_login: "user3".into(), - github_user_id: 7, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; + let user_1 = db + .create_user( + "user1@example.com", + false, + NewUserParams { + github_login: "user1".into(), + github_user_id: 5, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + let user_2 = db + .create_user( + "user2@example.com", + false, + NewUserParams { + github_login: "user2".into(), + github_user_id: 6, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; - let channel_1_1 = db - .create_root_channel("channel_1", "1", user_1) - .await - .unwrap(); + let user_3 = db + .create_user( + "user3@example.com", + false, + NewUserParams { + github_login: "user3".into(), + github_user_id: 7, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; - let channel_1_2 = db - .create_root_channel("channel_2", "2", user_1) - .await - .unwrap(); + let channel_1_1 = db + .create_root_channel("channel_1", "1", user_1) + .await + .unwrap(); - db.invite_channel_member(channel_1_1, user_2, user_1, false) - .await - .unwrap(); - db.invite_channel_member(channel_1_2, user_2, user_1, false) - .await - .unwrap(); - db.invite_channel_member(channel_1_1, user_3, user_1, true) - .await - .unwrap(); + let channel_1_2 = db + .create_root_channel("channel_2", "2", user_1) + .await + .unwrap(); - let user_2_invites = db - .get_channel_invites_for_user(user_2) // -> [channel_1_1, channel_1_2] - .await - .unwrap() - .into_iter() - .map(|channel| channel.id) - .collect::>(); + db.invite_channel_member(channel_1_1, user_2, user_1, false) + .await + .unwrap(); + db.invite_channel_member(channel_1_2, user_2, user_1, false) + .await + .unwrap(); + db.invite_channel_member(channel_1_1, user_3, user_1, true) + .await + .unwrap(); - assert_eq!(user_2_invites, &[channel_1_1, channel_1_2]); + let user_2_invites = db + .get_channel_invites_for_user(user_2) // -> [channel_1_1, channel_1_2] + .await + .unwrap() + .into_iter() + .map(|channel| channel.id) + .collect::>(); - let user_3_invites = db - .get_channel_invites_for_user(user_3) // -> [channel_1_1] - .await - .unwrap() - .into_iter() - .map(|channel| channel.id) - .collect::>(); + assert_eq!(user_2_invites, &[channel_1_1, channel_1_2]); - assert_eq!(user_3_invites, &[channel_1_1]); + let user_3_invites = db + .get_channel_invites_for_user(user_3) // -> [channel_1_1] + .await + .unwrap() + .into_iter() + .map(|channel| channel.id) + .collect::>(); - let members = db - .get_channel_member_details(channel_1_1, user_1) - .await - .unwrap(); - assert_eq!( - members, - &[ - proto::ChannelMember { - user_id: user_1.to_proto(), - kind: proto::channel_member::Kind::Member.into(), - admin: true, - }, - proto::ChannelMember { - user_id: user_2.to_proto(), - kind: proto::channel_member::Kind::Invitee.into(), - admin: false, - }, - proto::ChannelMember { - user_id: user_3.to_proto(), - kind: proto::channel_member::Kind::Invitee.into(), - admin: true, - }, - ] - ); + assert_eq!(user_3_invites, &[channel_1_1]); - db.respond_to_channel_invite(channel_1_1, user_2, true) - .await - .unwrap(); + let members = db + .get_channel_member_details(channel_1_1, user_1) + .await + .unwrap(); + assert_eq!( + members, + &[ + proto::ChannelMember { + user_id: user_1.to_proto(), + kind: proto::channel_member::Kind::Member.into(), + admin: true, + }, + proto::ChannelMember { + user_id: user_2.to_proto(), + kind: proto::channel_member::Kind::Invitee.into(), + admin: false, + }, + proto::ChannelMember { + user_id: user_3.to_proto(), + kind: proto::channel_member::Kind::Invitee.into(), + admin: true, + }, + ] + ); - let channel_1_3 = db - .create_channel("channel_3", Some(channel_1_1), "1", user_1) - .await - .unwrap(); + db.respond_to_channel_invite(channel_1_1, user_2, true) + .await + .unwrap(); - let members = db - .get_channel_member_details(channel_1_3, user_1) - .await - .unwrap(); - assert_eq!( - members, - &[ - proto::ChannelMember { - user_id: user_1.to_proto(), - kind: proto::channel_member::Kind::Member.into(), - admin: true, - }, - proto::ChannelMember { - user_id: user_2.to_proto(), - kind: proto::channel_member::Kind::AncestorMember.into(), - admin: false, - }, - ] - ); - } -); + let channel_1_3 = db + .create_channel("channel_3", Some(channel_1_1), "1", user_1) + .await + .unwrap(); + + let members = db + .get_channel_member_details(channel_1_3, user_1) + .await + .unwrap(); + assert_eq!( + members, + &[ + proto::ChannelMember { + user_id: user_1.to_proto(), + kind: proto::channel_member::Kind::Member.into(), + admin: true, + }, + proto::ChannelMember { + user_id: user_2.to_proto(), + kind: proto::channel_member::Kind::AncestorMember.into(), + admin: false, + }, + ] + ); +} test_both_dbs!( + test_channel_renames, test_channel_renames_postgres, - test_channel_renames_sqlite, - db, - { - db.create_server("test").await.unwrap(); + test_channel_renames_sqlite +); - let user_1 = db - .create_user( - "user1@example.com", - false, - NewUserParams { - github_login: "user1".into(), - github_user_id: 5, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; +async fn test_channel_renames(db: &Arc) { + db.create_server("test").await.unwrap(); - let user_2 = db - .create_user( - "user2@example.com", - false, - NewUserParams { - github_login: "user2".into(), - github_user_id: 6, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; + let user_1 = db + .create_user( + "user1@example.com", + false, + NewUserParams { + github_login: "user1".into(), + github_user_id: 5, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; - let zed_id = db.create_root_channel("zed", "1", user_1).await.unwrap(); + let user_2 = db + .create_user( + "user2@example.com", + false, + NewUserParams { + github_login: "user2".into(), + github_user_id: 6, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; - db.rename_channel(zed_id, user_1, "#zed-archive") - .await - .unwrap(); + let zed_id = db.create_root_channel("zed", "1", user_1).await.unwrap(); - let zed_archive_id = zed_id; + db.rename_channel(zed_id, user_1, "#zed-archive") + .await + .unwrap(); - let (channel, _) = db - .get_channel(zed_archive_id, user_1) - .await - .unwrap() - .unwrap(); - assert_eq!(channel.name, "zed-archive"); + let zed_archive_id = zed_id; - let non_permissioned_rename = db - .rename_channel(zed_archive_id, user_2, "hacked-lol") - .await; - assert!(non_permissioned_rename.is_err()); + let (channel, _) = db + .get_channel(zed_archive_id, user_1) + .await + .unwrap() + .unwrap(); + assert_eq!(channel.name, "zed-archive"); - let bad_name_rename = db.rename_channel(zed_id, user_1, "#").await; - assert!(bad_name_rename.is_err()) - } -); + let non_permissioned_rename = db + .rename_channel(zed_archive_id, user_2, "hacked-lol") + .await; + assert!(non_permissioned_rename.is_err()); + + let bad_name_rename = db.rename_channel(zed_id, user_1, "#").await; + assert!(bad_name_rename.is_err()) +} #[gpui::test] async fn test_multiple_signup_overwrite() { diff --git a/crates/collab/src/db/tests/feature_flag_tests.rs b/crates/collab/src/db/tests/feature_flag_tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..9d5f039747c18fb6cfae77191654ba5b4584e21e --- /dev/null +++ b/crates/collab/src/db/tests/feature_flag_tests.rs @@ -0,0 +1,60 @@ +use crate::{ + db::{Database, NewUserParams}, + test_both_dbs, +}; +use std::sync::Arc; + +test_both_dbs!( + test_get_user_flags, + test_get_user_flags_postgres, + test_get_user_flags_sqlite +); + +async fn test_get_user_flags(db: &Arc) { + let user_1 = db + .create_user( + &format!("user1@example.com"), + false, + NewUserParams { + github_login: format!("user1"), + github_user_id: 1, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let user_2 = db + .create_user( + &format!("user2@example.com"), + false, + NewUserParams { + github_login: format!("user2"), + github_user_id: 2, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + const CHANNELS_ALPHA: &'static str = "channels-alpha"; + const NEW_SEARCH: &'static str = "new-search"; + + let channels_flag = db.create_user_flag(CHANNELS_ALPHA).await.unwrap(); + let search_flag = db.create_user_flag(NEW_SEARCH).await.unwrap(); + + db.add_user_flag(user_1, channels_flag).await.unwrap(); + db.add_user_flag(user_1, search_flag).await.unwrap(); + + db.add_user_flag(user_2, channels_flag).await.unwrap(); + + let mut user_1_flags = db.get_user_flags(user_1).await.unwrap(); + user_1_flags.sort(); + assert_eq!(user_1_flags, &[CHANNELS_ALPHA, NEW_SEARCH]); + + let mut user_2_flags = db.get_user_flags(user_2).await.unwrap(); + user_2_flags.sort(); + assert_eq!(user_2_flags, &[CHANNELS_ALPHA]); +} diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 521aa3e7b45b7be2683a4312395b8328df2892b0..6b44711c42f4a37eea15c437879650a7c269aad5 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -35,8 +35,8 @@ use lazy_static::lazy_static; use prometheus::{register_int_gauge, IntGauge}; use rpc::{ proto::{ - self, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo, - RequestMessage, + self, Ack, AddChannelBufferCollaborator, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, + LiveKitConnectionInfo, RequestMessage, }, Connection, ConnectionId, Peer, Receipt, TypedEnvelope, }; @@ -248,6 +248,9 @@ impl Server { .add_request_handler(remove_channel_member) .add_request_handler(set_channel_member_admin) .add_request_handler(rename_channel) + .add_request_handler(join_channel_buffer) + .add_request_handler(leave_channel_buffer) + .add_message_handler(update_channel_buffer) .add_request_handler(get_channel_members) .add_request_handler(respond_to_channel_invite) .add_request_handler(join_channel) @@ -851,6 +854,10 @@ async fn connection_lost( .await .trace_err(); + leave_channel_buffers_for_session(&session) + .await + .trace_err(); + futures::select_biased! { _ = executor.sleep(RECONNECT_TIMEOUT).fuse() => { leave_room_for_session(&session).await.trace_err(); @@ -866,6 +873,8 @@ async fn connection_lost( } } update_user_contacts(session.user_id, &session).await?; + + } _ = teardown.changed().fuse() => {} } @@ -2478,6 +2487,104 @@ async fn join_channel( Ok(()) } +async fn join_channel_buffer( + request: proto::JoinChannelBuffer, + response: Response, + session: Session, +) -> Result<()> { + let db = session.db().await; + let channel_id = ChannelId::from_proto(request.channel_id); + + let open_response = db + .join_channel_buffer(channel_id, session.user_id, session.connection_id) + .await?; + + let replica_id = open_response.replica_id; + let collaborators = open_response.collaborators.clone(); + + response.send(open_response)?; + + let update = AddChannelBufferCollaborator { + channel_id: channel_id.to_proto(), + collaborator: Some(proto::Collaborator { + user_id: session.user_id.to_proto(), + peer_id: Some(session.connection_id.into()), + replica_id, + }), + }; + channel_buffer_updated( + session.connection_id, + collaborators + .iter() + .filter_map(|collaborator| Some(collaborator.peer_id?.into())), + &update, + &session.peer, + ); + + Ok(()) +} + +async fn update_channel_buffer( + request: proto::UpdateChannelBuffer, + session: Session, +) -> Result<()> { + let db = session.db().await; + let channel_id = ChannelId::from_proto(request.channel_id); + + let collaborators = db + .update_channel_buffer(channel_id, session.user_id, &request.operations) + .await?; + + channel_buffer_updated( + session.connection_id, + collaborators, + &proto::UpdateChannelBuffer { + channel_id: channel_id.to_proto(), + operations: request.operations, + }, + &session.peer, + ); + Ok(()) +} + +async fn leave_channel_buffer( + request: proto::LeaveChannelBuffer, + response: Response, + session: Session, +) -> Result<()> { + let db = session.db().await; + let channel_id = ChannelId::from_proto(request.channel_id); + + let collaborators_to_notify = db + .leave_channel_buffer(channel_id, session.connection_id) + .await?; + + response.send(Ack {})?; + + channel_buffer_updated( + session.connection_id, + collaborators_to_notify, + &proto::RemoveChannelBufferCollaborator { + channel_id: channel_id.to_proto(), + peer_id: Some(session.connection_id.into()), + }, + &session.peer, + ); + + Ok(()) +} + +fn channel_buffer_updated( + sender_id: ConnectionId, + collaborators: impl IntoIterator, + message: &T, + peer: &Peer, +) { + broadcast(Some(sender_id), collaborators.into_iter(), |peer_id| { + peer.send(peer_id.into(), message.clone()) + }); +} + async fn update_diff_base(request: proto::UpdateDiffBase, session: Session) -> Result<()> { let project_id = ProjectId::from_proto(request.project_id); let project_connection_ids = session @@ -2502,20 +2609,19 @@ async fn get_private_user_info( response: Response, session: Session, ) -> Result<()> { - let metrics_id = session - .db() - .await - .get_user_metrics_id(session.user_id) - .await?; - let user = session - .db() - .await + let db = session.db().await; + + let metrics_id = db.get_user_metrics_id(session.user_id).await?; + let user = db .get_user_by_id(session.user_id) .await? .ok_or_else(|| anyhow!("user not found"))?; + let flags = db.get_user_flags(session.user_id).await?; + response.send(proto::GetPrivateUserInfoResponse { metrics_id, staff: user.admin, + flags, })?; Ok(()) } @@ -2803,6 +2909,28 @@ async fn leave_room_for_session(session: &Session) -> Result<()> { Ok(()) } +async fn leave_channel_buffers_for_session(session: &Session) -> Result<()> { + let left_channel_buffers = session + .db() + .await + .leave_channel_buffers(session.connection_id) + .await?; + + for (channel_id, connections) in left_channel_buffers { + channel_buffer_updated( + session.connection_id, + connections, + &proto::RemoveChannelBufferCollaborator { + channel_id: channel_id.to_proto(), + peer_id: Some(session.connection_id.into()), + }, + &session.peer, + ); + } + + Ok(()) +} + fn project_left(project: &db::LeftProject, session: &Session) { for connection_id in &project.connection_ids { if project.host_user_id == session.user_id { diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index c9f358ca5bbdb875bb054c40b605c004d460075f..25f059c0aa959fe20116dd7596682adf6a02f945 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -1,14 +1,14 @@ use crate::{ - db::{test_db::TestDb, NewUserParams, UserId}, + db::{tests::TestDb, NewUserParams, UserId}, executor::Executor, rpc::{Server, CLEANUP_TIMEOUT}, AppState, }; use anyhow::anyhow; use call::{ActiveCall, Room}; +use channel::ChannelStore; use client::{ - self, proto::PeerId, ChannelStore, Client, Connection, Credentials, EstablishConnectionError, - UserStore, + self, proto::PeerId, Client, Connection, Credentials, EstablishConnectionError, UserStore, }; use collections::{HashMap, HashSet}; use fs::FakeFs; @@ -31,6 +31,7 @@ use std::{ use util::http::FakeHttpClient; use workspace::Workspace; +mod channel_buffer_tests; mod channel_tests; mod integration_tests; mod randomized_integration_tests; @@ -210,6 +211,7 @@ impl TestServer { workspace::init(app_state.clone(), cx); audio::init((), cx); call::init(client.clone(), user_store.clone(), cx); + channel::init(&client); }); client diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..8ac4dbbd3f1c606b52fb445a1c08ca4f1e8c6883 --- /dev/null +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -0,0 +1,426 @@ +use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; +use call::ActiveCall; +use channel::Channel; +use client::UserId; +use collab_ui::channel_view::ChannelView; +use collections::HashMap; +use futures::future; +use gpui::{executor::Deterministic, ModelHandle, TestAppContext}; +use rpc::{proto, RECEIVE_TIMEOUT}; +use serde_json::json; +use std::sync::Arc; + +#[gpui::test] +async fn test_core_channel_buffers( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + let zed_id = server + .make_channel("zed", (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .await; + + // Client A joins the channel buffer + let channel_buffer_a = client_a + .channel_store() + .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx)) + .await + .unwrap(); + + // Client A edits the buffer + let buffer_a = channel_buffer_a.read_with(cx_a, |buffer, _| buffer.buffer()); + + buffer_a.update(cx_a, |buffer, cx| { + buffer.edit([(0..0, "hello world")], None, cx) + }); + buffer_a.update(cx_a, |buffer, cx| { + buffer.edit([(5..5, ", cruel")], None, cx) + }); + buffer_a.update(cx_a, |buffer, cx| { + buffer.edit([(0..5, "goodbye")], None, cx) + }); + buffer_a.update(cx_a, |buffer, cx| buffer.undo(cx)); + deterministic.run_until_parked(); + + assert_eq!(buffer_text(&buffer_a, cx_a), "hello, cruel world"); + + // Client B joins the channel buffer + let channel_buffer_b = client_b + .channel_store() + .update(cx_b, |channel, cx| channel.open_channel_buffer(zed_id, cx)) + .await + .unwrap(); + + channel_buffer_b.read_with(cx_b, |buffer, _| { + assert_collaborators( + buffer.collaborators(), + &[client_a.user_id(), client_b.user_id()], + ); + }); + + // Client B sees the correct text, and then edits it + let buffer_b = channel_buffer_b.read_with(cx_b, |buffer, _| buffer.buffer()); + assert_eq!( + buffer_b.read_with(cx_b, |buffer, _| buffer.remote_id()), + buffer_a.read_with(cx_a, |buffer, _| buffer.remote_id()) + ); + assert_eq!(buffer_text(&buffer_b, cx_b), "hello, cruel world"); + buffer_b.update(cx_b, |buffer, cx| { + buffer.edit([(7..12, "beautiful")], None, cx) + }); + + // Both A and B see the new edit + deterministic.run_until_parked(); + assert_eq!(buffer_text(&buffer_a, cx_a), "hello, beautiful world"); + assert_eq!(buffer_text(&buffer_b, cx_b), "hello, beautiful world"); + + // Client A closes the channel buffer. + cx_a.update(|_| drop(channel_buffer_a)); + deterministic.run_until_parked(); + + // Client B sees that client A is gone from the channel buffer. + channel_buffer_b.read_with(cx_b, |buffer, _| { + assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]); + }); + + // Client A rejoins the channel buffer + let _channel_buffer_a = client_a + .channel_store() + .update(cx_a, |channels, cx| { + channels.open_channel_buffer(zed_id, cx) + }) + .await + .unwrap(); + deterministic.run_until_parked(); + + // Sanity test, make sure we saw A rejoining + channel_buffer_b.read_with(cx_b, |buffer, _| { + assert_collaborators( + &buffer.collaborators(), + &[client_b.user_id(), client_a.user_id()], + ); + }); + + // Client A loses connection. + server.forbid_connections(); + server.disconnect_client(client_a.peer_id().unwrap()); + deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + + // Client B observes A disconnect + channel_buffer_b.read_with(cx_b, |buffer, _| { + assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]); + }); + + // TODO: + // - Test synchronizing offline updates, what happens to A's channel buffer when A disconnects + // - Test interaction with channel deletion while buffer is open +} + +#[gpui::test] +async fn test_channel_buffer_replica_ids( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + let client_c = server.create_client(cx_c, "user_c").await; + + let channel_id = server + .make_channel( + "zed", + (&client_a, cx_a), + &mut [(&client_b, cx_b), (&client_c, cx_c)], + ) + .await; + + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + let active_call_c = cx_c.read(ActiveCall::global); + + // Clients A and B join a channel. + active_call_a + .update(cx_a, |call, cx| call.join_channel(channel_id, cx)) + .await + .unwrap(); + active_call_b + .update(cx_b, |call, cx| call.join_channel(channel_id, cx)) + .await + .unwrap(); + + // Clients A, B, and C join a channel buffer + // C first so that the replica IDs in the project and the channel buffer are different + let channel_buffer_c = client_c + .channel_store() + .update(cx_c, |channel, cx| { + channel.open_channel_buffer(channel_id, cx) + }) + .await + .unwrap(); + let channel_buffer_b = client_b + .channel_store() + .update(cx_b, |channel, cx| { + channel.open_channel_buffer(channel_id, cx) + }) + .await + .unwrap(); + let channel_buffer_a = client_a + .channel_store() + .update(cx_a, |channel, cx| { + channel.open_channel_buffer(channel_id, cx) + }) + .await + .unwrap(); + + // Client B shares a project + client_b + .fs() + .insert_tree("/dir", json!({ "file.txt": "contents" })) + .await; + let (project_b, _) = client_b.build_local_project("/dir", cx_b).await; + let shared_project_id = active_call_b + .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx)) + .await + .unwrap(); + + // Client A joins the project + let project_a = client_a.build_remote_project(shared_project_id, cx_a).await; + deterministic.run_until_parked(); + + // Client C is in a separate project. + client_c.fs().insert_tree("/dir", json!({})).await; + let (separate_project_c, _) = client_c.build_local_project("/dir", cx_c).await; + + // Note that each user has a different replica id in the projects vs the + // channel buffer. + channel_buffer_a.read_with(cx_a, |channel_buffer, cx| { + assert_eq!(project_a.read(cx).replica_id(), 1); + assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 2); + }); + channel_buffer_b.read_with(cx_b, |channel_buffer, cx| { + assert_eq!(project_b.read(cx).replica_id(), 0); + assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 1); + }); + channel_buffer_c.read_with(cx_c, |channel_buffer, cx| { + // C is not in the project + assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 0); + }); + + let channel_window_a = + cx_a.add_window(|cx| ChannelView::new(project_a.clone(), channel_buffer_a.clone(), cx)); + let channel_window_b = + cx_b.add_window(|cx| ChannelView::new(project_b.clone(), channel_buffer_b.clone(), cx)); + let channel_window_c = cx_c.add_window(|cx| { + ChannelView::new(separate_project_c.clone(), channel_buffer_c.clone(), cx) + }); + + let channel_view_a = channel_window_a.root(cx_a); + let channel_view_b = channel_window_b.root(cx_b); + let channel_view_c = channel_window_c.root(cx_c); + + // For clients A and B, the replica ids in the channel buffer are mapped + // so that they match the same users' replica ids in their shared project. + channel_view_a.read_with(cx_a, |view, cx| { + assert_eq!( + view.editor.read(cx).replica_id_map().unwrap(), + &[(1, 0), (2, 1)].into_iter().collect::>() + ); + }); + channel_view_b.read_with(cx_b, |view, cx| { + assert_eq!( + view.editor.read(cx).replica_id_map().unwrap(), + &[(1, 0), (2, 1)].into_iter().collect::>(), + ) + }); + + // Client C only sees themself, as they're not part of any shared project + channel_view_c.read_with(cx_c, |view, cx| { + assert_eq!( + view.editor.read(cx).replica_id_map().unwrap(), + &[(0, 0)].into_iter().collect::>(), + ); + }); + + // Client C joins the project that clients A and B are in. + active_call_c + .update(cx_c, |call, cx| call.join_channel(channel_id, cx)) + .await + .unwrap(); + let project_c = client_c.build_remote_project(shared_project_id, cx_c).await; + deterministic.run_until_parked(); + project_c.read_with(cx_c, |project, _| { + assert_eq!(project.replica_id(), 2); + }); + + // For clients A and B, client C's replica id in the channel buffer is + // now mapped to their replica id in the shared project. + channel_view_a.read_with(cx_a, |view, cx| { + assert_eq!( + view.editor.read(cx).replica_id_map().unwrap(), + &[(1, 0), (2, 1), (0, 2)] + .into_iter() + .collect::>() + ); + }); + channel_view_b.read_with(cx_b, |view, cx| { + assert_eq!( + view.editor.read(cx).replica_id_map().unwrap(), + &[(1, 0), (2, 1), (0, 2)] + .into_iter() + .collect::>(), + ) + }); +} + +#[gpui::test] +async fn test_reopen_channel_buffer(deterministic: Arc, cx_a: &mut TestAppContext) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + + let zed_id = server.make_channel("zed", (&client_a, cx_a), &mut []).await; + + let channel_buffer_1 = client_a + .channel_store() + .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx)); + let channel_buffer_2 = client_a + .channel_store() + .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx)); + let channel_buffer_3 = client_a + .channel_store() + .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx)); + + // All concurrent tasks for opening a channel buffer return the same model handle. + let (channel_buffer_1, channel_buffer_2, channel_buffer_3) = + future::try_join3(channel_buffer_1, channel_buffer_2, channel_buffer_3) + .await + .unwrap(); + let model_id = channel_buffer_1.id(); + assert_eq!(channel_buffer_1, channel_buffer_2); + assert_eq!(channel_buffer_1, channel_buffer_3); + + channel_buffer_1.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "hello")], None, cx); + }) + }); + deterministic.run_until_parked(); + + cx_a.update(|_| { + drop(channel_buffer_1); + drop(channel_buffer_2); + drop(channel_buffer_3); + }); + deterministic.run_until_parked(); + + // The channel buffer can be reopened after dropping it. + let channel_buffer = client_a + .channel_store() + .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx)) + .await + .unwrap(); + assert_ne!(channel_buffer.id(), model_id); + channel_buffer.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, _| { + assert_eq!(buffer.text(), "hello"); + }) + }); +} + +#[gpui::test] +async fn test_channel_buffer_disconnect( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + let channel_id = server + .make_channel("zed", (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .await; + + let channel_buffer_a = client_a + .channel_store() + .update(cx_a, |channel, cx| { + channel.open_channel_buffer(channel_id, cx) + }) + .await + .unwrap(); + + let channel_buffer_b = client_b + .channel_store() + .update(cx_b, |channel, cx| { + channel.open_channel_buffer(channel_id, cx) + }) + .await + .unwrap(); + + server.forbid_connections(); + server.disconnect_client(client_a.peer_id().unwrap()); + deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + + channel_buffer_a.update(cx_a, |buffer, _| { + assert_eq!( + buffer.channel().as_ref(), + &Channel { + id: channel_id, + name: "zed".to_string() + } + ); + assert!(!buffer.is_connected()); + }); + + deterministic.run_until_parked(); + + server.allow_connections(); + deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + + deterministic.run_until_parked(); + + client_a + .channel_store() + .update(cx_a, |channel_store, _| { + channel_store.remove_channel(channel_id) + }) + .await + .unwrap(); + deterministic.run_until_parked(); + + // Channel buffer observed the deletion + channel_buffer_b.update(cx_b, |buffer, _| { + assert_eq!( + buffer.channel().as_ref(), + &Channel { + id: channel_id, + name: "zed".to_string() + } + ); + assert!(!buffer.is_connected()); + }); +} + +#[track_caller] +fn assert_collaborators(collaborators: &[proto::Collaborator], ids: &[Option]) { + assert_eq!( + collaborators + .into_iter() + .map(|collaborator| collaborator.user_id) + .collect::>(), + ids.into_iter().map(|id| id.unwrap()).collect::>() + ); +} + +fn buffer_text(channel_buffer: &ModelHandle, cx: &mut TestAppContext) -> String { + channel_buffer.read_with(cx, |buffer, _| buffer.text()) +} diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 06cf3607c0555a606b2409a6b419d5df12794121..b54b4d349ba54e5c23e048cd81b292a10566445d 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -3,7 +3,8 @@ use crate::{ tests::{room_participants, RoomParticipants, TestServer}, }; use call::ActiveCall; -use client::{ChannelId, ChannelMembership, ChannelStore, User}; +use channel::{ChannelId, ChannelMembership, ChannelStore}; +use client::User; use gpui::{executor::Deterministic, ModelHandle, TestAppContext}; use rpc::{proto, RECEIVE_TIMEOUT}; use std::sync::Arc; @@ -798,7 +799,7 @@ async fn test_lost_channel_creation( deterministic.run_until_parked(); - // Sanity check + // Sanity check, B has the invitation assert_channel_invitations( client_b.channel_store(), cx_b, @@ -810,6 +811,7 @@ async fn test_lost_channel_creation( }], ); + // A creates a subchannel while the invite is still pending. let subchannel_id = client_a .channel_store() .update(cx_a, |channel_store, cx| { @@ -840,7 +842,7 @@ async fn test_lost_channel_creation( ], ); - // Accept the invite + // Client B accepts the invite client_b .channel_store() .update(cx_b, |channel_store, _| { @@ -851,7 +853,7 @@ async fn test_lost_channel_creation( deterministic.run_until_parked(); - // B should now see the channel + // Client B should now see the channel assert_channels( client_b.channel_store(), cx_b, diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 9bee8d434cd9ccb6d0fa252e2badc49be99a54d4..f64a82e32e938a573721ceecffbeb28e23604fda 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -4,7 +4,7 @@ use crate::{ }; use call::{room, ActiveCall, ParticipantLocation, Room}; use client::{User, RECEIVE_TIMEOUT}; -use collections::HashSet; +use collections::{HashMap, HashSet}; use editor::{ test::editor_test_context::EditorTestContext, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToggleCodeActions, Undo, @@ -4821,15 +4821,16 @@ async fn test_project_search( let project_b = client_b.build_remote_project(project_id, cx_b).await; // Perform a search as the guest. - let results = project_b - .update(cx_b, |project, cx| { - project.search( - SearchQuery::text("world", false, false, Vec::new(), Vec::new()), - cx, - ) - }) - .await - .unwrap(); + let mut results = HashMap::default(); + let mut search_rx = project_b.update(cx_b, |project, cx| { + project.search( + SearchQuery::text("world", false, false, Vec::new(), Vec::new()), + cx, + ) + }); + while let Some((buffer, ranges)) = search_rx.next().await { + results.entry(buffer).or_insert(ranges); + } let mut ranges_by_path = results .into_iter() @@ -5320,7 +5321,7 @@ async fn test_collaborating_with_code_actions( .unwrap(); let mut fake_language_server = fake_language_servers.next().await.unwrap(); - fake_language_server + let mut requests = fake_language_server .handle_request::(|params, _| async move { assert_eq!( params.text_document.uri, @@ -5329,9 +5330,9 @@ async fn test_collaborating_with_code_actions( assert_eq!(params.range.start, lsp::Position::new(0, 0)); assert_eq!(params.range.end, lsp::Position::new(0, 0)); Ok(None) - }) - .next() - .await; + }); + deterministic.advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2); + requests.next().await; // Move cursor to a location that contains code actions. editor_b.update(cx_b, |editor, cx| { @@ -5341,7 +5342,7 @@ async fn test_collaborating_with_code_actions( cx.focus(&editor_b); }); - fake_language_server + let mut requests = fake_language_server .handle_request::(|params, _| async move { assert_eq!( params.text_document.uri, @@ -5393,9 +5394,9 @@ async fn test_collaborating_with_code_actions( ..Default::default() }, )])) - }) - .next() - .await; + }); + deterministic.advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2); + requests.next().await; // Toggle code actions and wait for them to display. editor_b.update(cx_b, |editor, cx| { @@ -7863,6 +7864,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( client_a.language_registry().add(Arc::clone(&language)); client_b.language_registry().add(language); + // Client A opens a project. client_a .fs() .insert_tree( @@ -7883,6 +7885,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( .await .unwrap(); + // Client B joins the project let project_b = client_b.build_remote_project(project_id, cx_b).await; active_call_b .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) @@ -7892,6 +7895,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); cx_a.foreground().start_waiting(); + // The host opens a rust file. let _buffer_a = project_a .update(cx_a, |project, cx| { project.open_local_buffer("/a/main.rs", cx) @@ -7899,7 +7903,6 @@ async fn test_mutual_editor_inlay_hint_cache_update( .await .unwrap(); let fake_language_server = fake_language_servers.next().await.unwrap(); - let next_call_id = Arc::new(AtomicU32::new(0)); let editor_a = workspace_a .update(cx_a, |workspace, cx| { workspace.open_path((worktree_id, "main.rs"), None, true, cx) @@ -7908,6 +7911,9 @@ async fn test_mutual_editor_inlay_hint_cache_update( .unwrap() .downcast::() .unwrap(); + + // Set up the language server to return an additional inlay hint on each request. + let next_call_id = Arc::new(AtomicU32::new(0)); fake_language_server .handle_request::(move |params, _| { let task_next_call_id = Arc::clone(&next_call_id); @@ -7916,33 +7922,28 @@ async fn test_mutual_editor_inlay_hint_cache_update( params.text_document.uri, lsp::Url::from_file_path("/a/main.rs").unwrap(), ); - let mut current_call_id = Arc::clone(&task_next_call_id).fetch_add(1, SeqCst); - let mut new_hints = Vec::with_capacity(current_call_id as usize); - loop { - new_hints.push(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, - }); - if current_call_id == 0 { - break; - } - current_call_id -= 1; - } - Ok(Some(new_hints)) + let call_count = task_next_call_id.fetch_add(1, SeqCst); + Ok(Some( + (0..=call_count) + .map(|ix| lsp::InlayHint { + position: lsp::Position::new(0, ix), + label: lsp::InlayHintLabel::String(ix.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }) + .collect(), + )) } }) .next() .await .unwrap(); - cx_a.foreground().finish_waiting(); - cx_a.foreground().run_until_parked(); + deterministic.run_until_parked(); let mut edits_made = 1; editor_a.update(cx_a, |editor, _| { @@ -7968,7 +7969,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( .downcast::() .unwrap(); - cx_b.foreground().run_until_parked(); + deterministic.run_until_parked(); editor_b.update(cx_b, |editor, _| { assert_eq!( vec!["0".to_string(), "1".to_string()], @@ -7989,25 +7990,25 @@ async fn test_mutual_editor_inlay_hint_cache_update( cx.focus(&editor_b); edits_made += 1; }); - cx_a.foreground().run_until_parked(); - cx_b.foreground().run_until_parked(); + + deterministic.run_until_parked(); editor_a.update(cx_a, |editor, _| { assert_eq!( - vec!["0".to_string(), "1".to_string(), "2".to_string()], + vec![ + "0".to_string(), + "1".to_string(), + "2".to_string(), + "3".to_string() + ], extract_hint_labels(editor), - "Host should get hints from the 1st edit and 1st LSP query" + "Guest should get hints the 1st edit and 2nd LSP query" ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!(inlay_cache.version(), edits_made); }); editor_b.update(cx_b, |editor, _| { assert_eq!( - vec![ - "0".to_string(), - "1".to_string(), - "2".to_string(), - "3".to_string() - ], + vec!["0".to_string(), "1".to_string(), "2".to_string(),], extract_hint_labels(editor), "Guest should get hints the 1st edit and 2nd LSP query" ); @@ -8021,8 +8022,8 @@ async fn test_mutual_editor_inlay_hint_cache_update( cx.focus(&editor_a); edits_made += 1; }); - cx_a.foreground().run_until_parked(); - cx_b.foreground().run_until_parked(); + + deterministic.run_until_parked(); editor_a.update(cx_a, |editor, _| { assert_eq!( vec![ @@ -8061,8 +8062,8 @@ async fn test_mutual_editor_inlay_hint_cache_update( .await .expect("inlay refresh request failed"); edits_made += 1; - cx_a.foreground().run_until_parked(); - cx_b.foreground().run_until_parked(); + + deterministic.run_until_parked(); editor_a.update(cx_a, |editor, _| { assert_eq!( vec![ diff --git a/crates/collab/src/tests/randomized_integration_tests.rs b/crates/collab/src/tests/randomized_integration_tests.rs index 18fe6734cdda0dcb5194e518f06caf589751080e..814f248b6dc722fa67f2af2dd70c66f54af3a57a 100644 --- a/crates/collab/src/tests/randomized_integration_tests.rs +++ b/crates/collab/src/tests/randomized_integration_tests.rs @@ -6,7 +6,7 @@ use crate::{ use anyhow::{anyhow, Result}; use call::ActiveCall; use client::RECEIVE_TIMEOUT; -use collections::BTreeMap; +use collections::{BTreeMap, HashMap}; use editor::Bias; use fs::{repository::GitFileStatus, FakeFs, Fs as _}; use futures::StreamExt as _; @@ -121,7 +121,9 @@ async fn test_random_collaboration( let mut operation_channels = Vec::new(); loop { - let Some((next_operation, applied)) = plan.lock().next_server_operation(&clients) else { break }; + let Some((next_operation, applied)) = plan.lock().next_server_operation(&clients) else { + break; + }; applied.store(true, SeqCst); let did_apply = apply_server_operation( deterministic.clone(), @@ -224,7 +226,9 @@ async fn apply_server_operation( let client_ix = clients .iter() .position(|(client, cx)| client.current_user_id(cx) == removed_user_id); - let Some(client_ix) = client_ix else { return false }; + let Some(client_ix) = client_ix else { + return false; + }; let user_connection_ids = server .connection_pool .lock() @@ -718,7 +722,7 @@ async fn apply_client_operation( if detach { "detaching" } else { "awaiting" } ); - let search = project.update(cx, |project, cx| { + let mut search = project.update(cx, |project, cx| { project.search( SearchQuery::text(query, false, false, Vec::new(), Vec::new()), cx, @@ -726,15 +730,13 @@ async fn apply_client_operation( }); drop(project); let search = cx.background().spawn(async move { - search - .await - .map_err(|err| anyhow!("search request failed: {:?}", err)) + let mut results = HashMap::default(); + while let Some((buffer, ranges)) = search.next().await { + results.entry(buffer).or_insert(ranges); + } + results }); - if detach { - cx.update(|cx| search.detach_and_log_err(cx)); - } else { - search.await?; - } + search.await; } ClientOperation::WriteFsEntry { @@ -1591,10 +1593,11 @@ impl TestPlan { 81.. => match self.rng.gen_range(0..100_u32) { // Add a worktree to a local project 0..=50 => { - let Some(project) = client - .local_projects() - .choose(&mut self.rng) - .cloned() else { continue }; + let Some(project) = + client.local_projects().choose(&mut self.rng).cloned() + else { + continue; + }; let project_root_name = root_name_for_project(&project, cx); let mut paths = client.fs().paths(false); paths.remove(0); @@ -1611,7 +1614,9 @@ impl TestPlan { // Add an entry to a worktree _ => { - let Some(project) = choose_random_project(client, &mut self.rng) else { continue }; + let Some(project) = choose_random_project(client, &mut self.rng) else { + continue; + }; let project_root_name = root_name_for_project(&project, cx); let is_local = project.read_with(cx, |project, _| project.is_local()); let worktree = project.read_with(cx, |project, cx| { @@ -1645,7 +1650,9 @@ impl TestPlan { // Query and mutate buffers 60..=90 => { - let Some(project) = choose_random_project(client, &mut self.rng) else { continue }; + let Some(project) = choose_random_project(client, &mut self.rng) else { + continue; + }; let project_root_name = root_name_for_project(&project, cx); let is_local = project.read_with(cx, |project, _| project.is_local()); @@ -1656,7 +1663,10 @@ impl TestPlan { .buffers_for_project(&project) .iter() .choose(&mut self.rng) - .cloned() else { continue }; + .cloned() + else { + continue; + }; let full_path = buffer .read_with(cx, |buffer, cx| buffer.file().unwrap().full_path(cx)); @@ -2026,7 +2036,10 @@ async fn simulate_client( client.app_state.languages.add(Arc::new(language)); while let Some(batch_id) = operation_rx.next().await { - let Some((operation, applied)) = plan.lock().next_client_operation(&client, batch_id, &cx) else { break }; + let Some((operation, applied)) = plan.lock().next_client_operation(&client, batch_id, &cx) + else { + break; + }; applied.store(true, SeqCst); match apply_client_operation(&client, operation, &mut cx).await { Ok(()) => {} diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index 471608c43ec86a1afce15bc5552bdd58b7d0cd86..da32308558f7c7e8279c420961f8d42d9356d37b 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -26,6 +26,7 @@ auto_update = { path = "../auto_update" } db = { path = "../db" } call = { path = "../call" } client = { path = "../client" } +channel = { path = "../channel" } clock = { path = "../clock" } collections = { path = "../collections" } context_menu = { path = "../context_menu" } @@ -33,12 +34,13 @@ editor = { path = "../editor" } feedback = { path = "../feedback" } fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } +language = { path = "../language" } menu = { path = "../menu" } picker = { path = "../picker" } project = { path = "../project" } recent_projects = {path = "../recent_projects"} settings = { path = "../settings" } -staff_mode = {path = "../staff_mode"} +feature_flags = {path = "../feature_flags"} theme = { path = "../theme" } theme_selector = { path = "../theme_selector" } vcs_menu = { path = "../vcs_menu" } diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs new file mode 100644 index 0000000000000000000000000000000000000000..a34f10b2db29f3f132e823fce27209d0a24a12c6 --- /dev/null +++ b/crates/collab_ui/src/channel_view.rs @@ -0,0 +1,355 @@ +use anyhow::{anyhow, Result}; +use channel::{ + channel_buffer::{self, ChannelBuffer}, + ChannelId, +}; +use client::proto; +use clock::ReplicaId; +use collections::HashMap; +use editor::Editor; +use gpui::{ + actions, + elements::{ChildView, Label}, + geometry::vector::Vector2F, + AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, Task, View, + ViewContext, ViewHandle, +}; +use project::Project; +use std::any::Any; +use workspace::{ + item::{FollowableItem, Item, ItemHandle}, + register_followable_item, + searchable::SearchableItemHandle, + ItemNavHistory, Pane, ViewId, Workspace, WorkspaceId, +}; + +actions!(channel_view, [Deploy]); + +pub(crate) fn init(cx: &mut AppContext) { + register_followable_item::(cx) +} + +pub struct ChannelView { + pub editor: ViewHandle, + project: ModelHandle, + channel_buffer: ModelHandle, + remote_id: Option, + _editor_event_subscription: Subscription, +} + +impl ChannelView { + pub fn open( + channel_id: ChannelId, + pane: ViewHandle, + workspace: ViewHandle, + cx: &mut AppContext, + ) -> Task>> { + let workspace = workspace.read(cx); + let project = workspace.project().to_owned(); + let channel_store = workspace.app_state().channel_store.clone(); + let markdown = workspace + .app_state() + .languages + .language_for_name("Markdown"); + let channel_buffer = + channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx)); + + cx.spawn(|mut cx| async move { + let channel_buffer = channel_buffer.await?; + let markdown = markdown.await?; + channel_buffer.update(&mut cx, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.set_language(Some(markdown), cx); + }) + }); + + pane.update(&mut cx, |pane, cx| { + pane.items_of_type::() + .find(|channel_view| channel_view.read(cx).channel_buffer == channel_buffer) + .unwrap_or_else(|| cx.add_view(|cx| Self::new(project, channel_buffer, cx))) + }) + .ok_or_else(|| anyhow!("pane was dropped")) + }) + } + + pub fn new( + project: ModelHandle, + channel_buffer: ModelHandle, + cx: &mut ViewContext, + ) -> Self { + let buffer = channel_buffer.read(cx).buffer(); + // buffer.update(cx, |buffer, cx| buffer.set_language(language, cx)); + let editor = cx.add_view(|cx| Editor::for_buffer(buffer, None, cx)); + let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone())); + + cx.subscribe(&project, Self::handle_project_event).detach(); + cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event) + .detach(); + + let this = Self { + editor, + project, + channel_buffer, + remote_id: None, + _editor_event_subscription, + }; + this.refresh_replica_id_map(cx); + this + } + + fn handle_project_event( + &mut self, + _: ModelHandle, + event: &project::Event, + cx: &mut ViewContext, + ) { + match event { + project::Event::RemoteIdChanged(_) => {} + project::Event::DisconnectedFromHost => {} + project::Event::Closed => {} + project::Event::CollaboratorUpdated { .. } => {} + project::Event::CollaboratorLeft(_) => {} + project::Event::CollaboratorJoined(_) => {} + _ => return, + } + self.refresh_replica_id_map(cx); + } + + fn handle_channel_buffer_event( + &mut self, + _: ModelHandle, + event: &channel_buffer::Event, + cx: &mut ViewContext, + ) { + match event { + channel_buffer::Event::CollaboratorsChanged => { + self.refresh_replica_id_map(cx); + } + channel_buffer::Event::Disconnected => self.editor.update(cx, |editor, cx| { + editor.set_read_only(true); + cx.notify(); + }), + } + } + + /// Build a mapping of channel buffer replica ids to the corresponding + /// replica ids in the current project. + /// + /// Using this mapping, a given user can be displayed with the same color + /// in the channel buffer as in other files in the project. Users who are + /// in the channel buffer but not the project will not have a color. + fn refresh_replica_id_map(&self, cx: &mut ViewContext) { + let mut project_replica_ids_by_channel_buffer_replica_id = HashMap::default(); + let project = self.project.read(cx); + let channel_buffer = self.channel_buffer.read(cx); + project_replica_ids_by_channel_buffer_replica_id + .insert(channel_buffer.replica_id(cx), project.replica_id()); + project_replica_ids_by_channel_buffer_replica_id.extend( + channel_buffer + .collaborators() + .iter() + .filter_map(|channel_buffer_collaborator| { + project + .collaborators() + .values() + .find_map(|project_collaborator| { + (project_collaborator.user_id == channel_buffer_collaborator.user_id) + .then_some(( + channel_buffer_collaborator.replica_id as ReplicaId, + project_collaborator.replica_id, + )) + }) + }), + ); + + self.editor.update(cx, |editor, cx| { + editor.set_replica_id_map(Some(project_replica_ids_by_channel_buffer_replica_id), cx) + }); + } +} + +impl Entity for ChannelView { + type Event = editor::Event; +} + +impl View for ChannelView { + fn ui_name() -> &'static str { + "ChannelView" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + ChildView::new(self.editor.as_any(), cx).into_any() + } + + fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { + if cx.is_self_focused() { + cx.focus(self.editor.as_any()) + } + } +} + +impl Item for ChannelView { + fn tab_content( + &self, + _: Option, + style: &theme::Tab, + cx: &gpui::AppContext, + ) -> AnyElement { + let channel_name = &self.channel_buffer.read(cx).channel().name; + let label = if self.channel_buffer.read(cx).is_connected() { + format!("#{}", channel_name) + } else { + format!("#{} (disconnected)", channel_name) + }; + Label::new(label, style.label.to_owned()).into_any() + } + + fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext) -> Option { + Some(Self::new( + self.project.clone(), + self.channel_buffer.clone(), + cx, + )) + } + + fn is_singleton(&self, _cx: &AppContext) -> bool { + true + } + + fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { + self.editor + .update(cx, |editor, cx| editor.navigate(data, cx)) + } + + fn deactivated(&mut self, cx: &mut ViewContext) { + self.editor + .update(cx, |editor, cx| Item::deactivated(editor, cx)) + } + + fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext) { + self.editor + .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx)) + } + + fn as_searchable(&self, _: &ViewHandle) -> Option> { + Some(Box::new(self.editor.clone())) + } + + fn show_toolbar(&self) -> bool { + true + } + + fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option { + self.editor.read(cx).pixel_position_of_cursor(cx) + } +} + +impl FollowableItem for ChannelView { + fn remote_id(&self) -> Option { + self.remote_id + } + + fn to_state_proto(&self, cx: &AppContext) -> Option { + let channel = self.channel_buffer.read(cx).channel(); + Some(proto::view::Variant::ChannelView( + proto::view::ChannelView { + channel_id: channel.id, + editor: if let Some(proto::view::Variant::Editor(proto)) = + self.editor.read(cx).to_state_proto(cx) + { + Some(proto) + } else { + None + }, + }, + )) + } + + fn from_state_proto( + pane: ViewHandle, + workspace: ViewHandle, + remote_id: workspace::ViewId, + state: &mut Option, + cx: &mut AppContext, + ) -> Option>>> { + let Some(proto::view::Variant::ChannelView(_)) = state else { + return None; + }; + let Some(proto::view::Variant::ChannelView(state)) = state.take() else { + unreachable!() + }; + + let open = ChannelView::open(state.channel_id, pane, workspace, cx); + + Some(cx.spawn(|mut cx| async move { + let this = open.await?; + + let task = this + .update(&mut cx, |this, cx| { + this.remote_id = Some(remote_id); + + if let Some(state) = state.editor { + Some(this.editor.update(cx, |editor, cx| { + editor.apply_update_proto( + &this.project, + proto::update_view::Variant::Editor(proto::update_view::Editor { + selections: state.selections, + pending_selection: state.pending_selection, + scroll_top_anchor: state.scroll_top_anchor, + scroll_x: state.scroll_x, + scroll_y: state.scroll_y, + ..Default::default() + }), + cx, + ) + })) + } else { + None + } + }) + .ok_or_else(|| anyhow!("window was closed"))?; + + if let Some(task) = task { + task.await?; + } + + Ok(this) + })) + } + + fn add_event_to_update_proto( + &self, + event: &Self::Event, + update: &mut Option, + cx: &AppContext, + ) -> bool { + self.editor + .read(cx) + .add_event_to_update_proto(event, update, cx) + } + + fn apply_update_proto( + &mut self, + project: &ModelHandle, + message: proto::update_view::Variant, + cx: &mut ViewContext, + ) -> gpui::Task> { + self.editor.update(cx, |editor, cx| { + editor.apply_update_proto(project, message, cx) + }) + } + + fn set_leader_replica_id( + &mut self, + leader_replica_id: Option, + cx: &mut ViewContext, + ) { + self.editor.update(cx, |editor, cx| { + editor.set_leader_replica_id(leader_replica_id, cx) + }) + } + + fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool { + Editor::should_unfollow_on_event(event, cx) + } +} diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index c49011b86b7b17d2d73c7171676962ddef9e9cb7..0593bfcb1f279be0ce9fd7fed4dd2672d1813cc4 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -4,28 +4,30 @@ mod panel_settings; use anyhow::Result; use call::ActiveCall; -use client::{ - proto::PeerId, Channel, ChannelEvent, ChannelId, ChannelStore, Client, Contact, User, UserStore, -}; - +use channel::{Channel, ChannelEvent, ChannelId, ChannelStore}; +use client::{proto::PeerId, Client, Contact, User, UserStore}; use context_menu::{ContextMenu, ContextMenuItem}; use db::kvp::KEY_VALUE_STORE; use editor::{Cancel, Editor}; + +use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt}; use futures::StreamExt; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ actions, elements::{ - Canvas, ChildView, Empty, Flex, Image, Label, List, ListOffset, ListState, - MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, Stack, Svg, + Canvas, ChildView, Component, Empty, Flex, Image, Label, List, ListOffset, ListState, + MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, SafeStylable, + Stack, Svg, }, + fonts::TextStyle, geometry::{ rect::RectF, vector::{vec2f, Vector2F}, }, impl_actions, platform::{CursorStyle, MouseButton, PromptLevel}, - serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, ModelHandle, + serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, FontCache, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; use menu::{Confirm, SelectNext, SelectPrev}; @@ -33,9 +35,8 @@ use panel_settings::{CollaborationPanelDockPosition, CollaborationPanelSettings} use project::{Fs, Project}; use serde_derive::{Deserialize, Serialize}; use settings::SettingsStore; -use staff_mode::StaffMode; use std::{borrow::Cow, mem, sync::Arc}; -use theme::IconButton; +use theme::{components::ComponentExt, IconButton}; use util::{iife, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel}, @@ -43,7 +44,10 @@ use workspace::{ Workspace, }; -use crate::face_pile::FacePile; +use crate::{ + channel_view::{self, ChannelView}, + face_pile::FacePile, +}; use channel_modal::ChannelModal; use self::contact_finder::ContactFinder; @@ -53,6 +57,11 @@ struct RemoveChannel { channel_id: u64, } +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +struct ToggleCollapse { + channel_id: u64, +} + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] struct NewChannel { channel_id: u64, @@ -73,7 +82,21 @@ struct RenameChannel { channel_id: u64, } -actions!(collab_panel, [ToggleFocus, Remove, Secondary]); +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +struct OpenChannelBuffer { + channel_id: u64, +} + +actions!( + collab_panel, + [ + ToggleFocus, + Remove, + Secondary, + CollapseSelectedChannel, + ExpandSelectedChannel + ] +); impl_actions!( collab_panel, @@ -82,7 +105,9 @@ impl_actions!( NewChannel, InviteMembers, ManageMembers, - RenameChannel + RenameChannel, + ToggleCollapse, + OpenChannelBuffer ] ); @@ -92,6 +117,7 @@ pub fn init(_client: Arc, cx: &mut AppContext) { settings::register::(cx); contact_finder::init(cx); channel_modal::init(cx); + channel_view::init(cx); cx.add_action(CollabPanel::cancel); cx.add_action(CollabPanel::select_next); @@ -105,6 +131,10 @@ pub fn init(_client: Arc, cx: &mut AppContext) { cx.add_action(CollabPanel::manage_members); cx.add_action(CollabPanel::rename_selected_channel); cx.add_action(CollabPanel::rename_channel); + cx.add_action(CollabPanel::toggle_channel_collapsed); + cx.add_action(CollabPanel::collapse_selected_channel); + cx.add_action(CollabPanel::expand_selected_channel); + cx.add_action(CollabPanel::open_channel_buffer); } #[derive(Debug)] @@ -147,13 +177,15 @@ pub struct CollabPanel { list_state: ListState, subscriptions: Vec, collapsed_sections: Vec
, + collapsed_channels: Vec, workspace: WeakViewHandle, context_menu_on_selected: bool, } #[derive(Serialize, Deserialize)] -struct SerializedChannelsPanel { +struct SerializedCollabPanel { width: Option, + collapsed_channels: Option>, } #[derive(Debug)] @@ -198,6 +230,9 @@ enum ListEntry { channel: Arc, depth: usize, }, + ChannelNotes { + channel_id: ChannelId, + }, ChannelEditor { depth: usize, }, @@ -341,6 +376,12 @@ impl CollabPanel { return channel_row; } } + ListEntry::ChannelNotes { channel_id } => this.render_channel_notes( + *channel_id, + &theme.collab_panel, + is_selected, + cx, + ), ListEntry::ChannelInvite(channel) => Self::render_channel_invite( channel.clone(), this.channel_store.clone(), @@ -398,6 +439,7 @@ impl CollabPanel { subscriptions: Vec::default(), match_candidates: Vec::default(), collapsed_sections: vec![Section::Offline], + collapsed_channels: Vec::default(), workspace: workspace.weak_handle(), client: workspace.app_state().client.clone(), context_menu_on_selected: true, @@ -431,9 +473,10 @@ impl CollabPanel { })); this.subscriptions .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx))); - this.subscriptions.push( - cx.observe_global::(move |this, cx| this.update_entries(true, cx)), - ); + this.subscriptions + .push(cx.observe_flag::(move |_, this, cx| { + this.update_entries(true, cx) + })); this.subscriptions.push(cx.subscribe( &this.channel_store, |this, _channel_store, e, cx| match e { @@ -469,7 +512,7 @@ impl CollabPanel { .log_err() .flatten() { - Some(serde_json::from_str::(&panel)?) + Some(serde_json::from_str::(&panel)?) } else { None }; @@ -479,6 +522,9 @@ impl CollabPanel { if let Some(serialized_panel) = serialized_panel { panel.update(cx, |panel, cx| { panel.width = serialized_panel.width; + panel.collapsed_channels = serialized_panel + .collapsed_channels + .unwrap_or_else(|| Vec::new()); cx.notify(); }); } @@ -489,12 +535,16 @@ impl CollabPanel { fn serialize(&mut self, cx: &mut ViewContext) { let width = self.width; + let collapsed_channels = self.collapsed_channels.clone(); self.pending_serialization = cx.background().spawn( async move { KEY_VALUE_STORE .write_kvp( COLLABORATION_PANEL_KEY.into(), - serde_json::to_string(&SerializedChannelsPanel { width })?, + serde_json::to_string(&SerializedCollabPanel { + width, + collapsed_channels: Some(collapsed_channels), + })?, ) .await?; anyhow::Ok(()) @@ -518,6 +568,10 @@ impl CollabPanel { if !self.collapsed_sections.contains(&Section::ActiveCall) { let room = room.read(cx); + if let Some(channel_id) = room.channel_id() { + self.entries.push(ListEntry::ChannelNotes { channel_id }) + } + // Populate the active user. if let Some(user) = user_store.current_user() { self.match_candidates.clear(); @@ -622,7 +676,8 @@ impl CollabPanel { } let mut request_entries = Vec::new(); - if self.include_channels_section(cx) { + + if cx.has_flag::() { self.entries.push(ListEntry::Header(Section::Channels, 0)); if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() { @@ -657,10 +712,24 @@ impl CollabPanel { self.entries.push(ListEntry::ChannelEditor { depth: 0 }); } } + let mut collapse_depth = None; for mat in matches { let (depth, channel) = channel_store.channel_at_index(mat.candidate_id).unwrap(); + if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) { + collapse_depth = Some(depth); + } else if let Some(collapsed_depth) = collapse_depth { + if depth > collapsed_depth { + continue; + } + if self.is_channel_collapsed(channel.id) { + collapse_depth = Some(depth); + } else { + collapse_depth = None; + } + } + match &self.channel_editing_state { Some(ChannelEditingState::Create { parent_id, .. }) if *parent_id == Some(channel.id) => @@ -963,25 +1032,19 @@ impl CollabPanel { ) -> AnyElement { enum JoinProject {} - let font_cache = cx.font_cache(); - let host_avatar_height = theme + let host_avatar_width = theme .contact_avatar .width .or(theme.contact_avatar.height) .unwrap_or(0.); - let row = &theme.project_row.inactive_state().default; let tree_branch = theme.tree_branch; - let line_height = row.name.text.line_height(font_cache); - let cap_height = row.name.text.cap_height(font_cache); - let baseline_offset = - row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.; let project_name = if worktree_root_names.is_empty() { "untitled".to_string() } else { worktree_root_names.join(", ") }; - MouseEventHandler::new::(project_id as usize, cx, |mouse_state, _| { + MouseEventHandler::new::(project_id as usize, cx, |mouse_state, cx| { let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state); let row = theme .project_row @@ -989,39 +1052,20 @@ impl CollabPanel { .style_for(mouse_state); Flex::row() + .with_child(render_tree_branch( + tree_branch, + &row.name.text, + is_last, + vec2f(host_avatar_width, theme.row_height), + cx.font_cache(), + )) .with_child( - Stack::new() - .with_child(Canvas::new(move |scene, bounds, _, _, _| { - let start_x = - bounds.min_x() + (bounds.width() / 2.) - (tree_branch.width / 2.); - let end_x = bounds.max_x(); - let start_y = bounds.min_y(); - let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.); - - scene.push_quad(gpui::Quad { - bounds: RectF::from_points( - vec2f(start_x, start_y), - vec2f( - start_x + tree_branch.width, - if is_last { end_y } else { bounds.max_y() }, - ), - ), - background: Some(tree_branch.color), - border: gpui::Border::default(), - corner_radii: (0.).into(), - }); - scene.push_quad(gpui::Quad { - bounds: RectF::from_points( - vec2f(start_x, end_y), - vec2f(end_x, end_y + tree_branch.width), - ), - background: Some(tree_branch.color), - border: gpui::Border::default(), - corner_radii: (0.).into(), - }); - })) + Svg::new("icons/file_icons/folder.svg") + .with_color(theme.channel_hash.color) .constrained() - .with_width(host_avatar_height), + .with_width(theme.channel_hash.width) + .aligned() + .left(), ) .with_child( Label::new(project_name, row.name.text.clone()) @@ -1196,7 +1240,7 @@ impl CollabPanel { }); if let Some(name) = channel_name { - Cow::Owned(format!("Current Call - #{}", name)) + Cow::Owned(format!("#{}", name)) } else { Cow::Borrowed("Current Call") } @@ -1332,7 +1376,7 @@ impl CollabPanel { .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, this, cx| { if can_collapse { - this.toggle_expanded(section, cx); + this.toggle_section_expanded(section, cx); } }) } @@ -1479,6 +1523,11 @@ impl CollabPanel { cx: &AppContext, ) -> AnyElement { Flex::row() + .with_child( + Empty::new() + .constrained() + .with_width(theme.collab_panel.disclosure.button_space()), + ) .with_child( Svg::new("icons/hash.svg") .with_color(theme.collab_panel.channel_hash.color) @@ -1537,6 +1586,10 @@ impl CollabPanel { cx: &mut ViewContext, ) -> AnyElement { let channel_id = channel.id; + let has_children = self.channel_store.read(cx).has_children(channel_id); + let disclosed = + has_children.then(|| !self.collapsed_channels.binary_search(&channel_id).is_ok()); + let is_active = iife!({ let call_channel = ActiveCall::global(cx) .read(cx) @@ -1550,7 +1603,7 @@ impl CollabPanel { const FACEPILE_LIMIT: usize = 3; MouseEventHandler::new::(channel.id as usize, cx, |state, cx| { - Flex::row() + Flex::::row() .with_child( Svg::new("icons/hash.svg") .with_color(theme.channel_hash.color) @@ -1599,6 +1652,11 @@ impl CollabPanel { } }) .align_children_center() + .styleable_component() + .disclosable(disclosed, Box::new(ToggleCollapse { channel_id })) + .with_id(channel_id as usize) + .with_style(theme.disclosure.clone()) + .element() .constrained() .with_height(theme.row_height) .contained() @@ -1618,6 +1676,61 @@ impl CollabPanel { .into_any() } + fn render_channel_notes( + &self, + channel_id: ChannelId, + theme: &theme::CollabPanel, + is_selected: bool, + cx: &mut ViewContext, + ) -> AnyElement { + enum ChannelNotes {} + let host_avatar_width = theme + .contact_avatar + .width + .or(theme.contact_avatar.height) + .unwrap_or(0.); + + MouseEventHandler::new::(channel_id as usize, cx, |state, cx| { + let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state); + let row = theme.project_row.in_state(is_selected).style_for(state); + + Flex::::row() + .with_child(render_tree_branch( + tree_branch, + &row.name.text, + true, + vec2f(host_avatar_width, theme.row_height), + cx.font_cache(), + )) + .with_child( + Svg::new("icons/radix/file.svg") + .with_color(theme.channel_hash.color) + .constrained() + .with_width(theme.channel_hash.width) + .aligned() + .left(), + ) + .with_child( + Label::new("notes", theme.channel_name.text.clone()) + .contained() + .with_style(theme.channel_name.container) + .aligned() + .left() + .flex(1., true), + ) + .constrained() + .with_height(theme.row_height) + .contained() + .with_style(*theme.channel_row.style_for(is_selected, state)) + .with_padding_left(theme.channel_row.default_style().padding.left) + }) + .on_click(MouseButton::Left, move |_, this, cx| { + this.open_channel_buffer(&OpenChannelBuffer { channel_id }, cx); + }) + .with_cursor_style(CursorStyle::PointingHand) + .into_any() + } + fn render_channel_invite( channel: Arc, channel_store: ModelHandle, @@ -1801,53 +1914,58 @@ impl CollabPanel { .into_any() } - fn include_channels_section(&self, cx: &AppContext) -> bool { - if cx.has_global::() { - cx.global::().0 - } else { - false - } - } - fn deploy_channel_context_menu( &mut self, position: Option, channel_id: u64, cx: &mut ViewContext, ) { - if self.channel_store.read(cx).is_user_admin(channel_id) { - self.context_menu_on_selected = position.is_none(); - - self.context_menu.update(cx, |context_menu, cx| { - context_menu.set_position_mode(if self.context_menu_on_selected { - OverlayPositionMode::Local - } else { - OverlayPositionMode::Window - }); + self.context_menu_on_selected = position.is_none(); - context_menu.show( - position.unwrap_or_default(), - if self.context_menu_on_selected { - gpui::elements::AnchorCorner::TopRight - } else { - gpui::elements::AnchorCorner::BottomLeft - }, - vec![ - ContextMenuItem::action("New Subchannel", NewChannel { channel_id }), - ContextMenuItem::Separator, - ContextMenuItem::action("Invite to Channel", InviteMembers { channel_id }), - ContextMenuItem::Separator, - ContextMenuItem::action("Rename", RenameChannel { channel_id }), - ContextMenuItem::action("Manage", ManageMembers { channel_id }), - ContextMenuItem::Separator, - ContextMenuItem::action("Delete", RemoveChannel { channel_id }), - ], - cx, - ); + self.context_menu.update(cx, |context_menu, cx| { + context_menu.set_position_mode(if self.context_menu_on_selected { + OverlayPositionMode::Local + } else { + OverlayPositionMode::Window }); - cx.notify(); - } + let expand_action_name = if self.is_channel_collapsed(channel_id) { + "Expand Subchannels" + } else { + "Collapse Subchannels" + }; + + let mut items = vec![ + ContextMenuItem::action(expand_action_name, ToggleCollapse { channel_id }), + ContextMenuItem::action("Open Notes", OpenChannelBuffer { channel_id }), + ]; + + if self.channel_store.read(cx).is_user_admin(channel_id) { + items.extend([ + ContextMenuItem::Separator, + ContextMenuItem::action("New Subchannel", NewChannel { channel_id }), + ContextMenuItem::action("Rename", RenameChannel { channel_id }), + ContextMenuItem::Separator, + ContextMenuItem::action("Invite Members", InviteMembers { channel_id }), + ContextMenuItem::action("Manage Members", ManageMembers { channel_id }), + ContextMenuItem::Separator, + ContextMenuItem::action("Delete", RemoveChannel { channel_id }), + ]); + } + + context_menu.show( + position.unwrap_or_default(), + if self.context_menu_on_selected { + gpui::elements::AnchorCorner::TopRight + } else { + gpui::elements::AnchorCorner::BottomLeft + }, + items, + cx, + ); + }); + + cx.notify(); } fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { @@ -1912,7 +2030,7 @@ impl CollabPanel { | Section::Online | Section::Offline | Section::ChannelInvites => { - self.toggle_expanded(*section, cx); + self.toggle_section_expanded(*section, cx); } }, ListEntry::Contact { contact, calling } => { @@ -2000,7 +2118,7 @@ impl CollabPanel { } } - fn toggle_expanded(&mut self, section: Section, cx: &mut ViewContext) { + fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext) { if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) { self.collapsed_sections.remove(ix); } else { @@ -2009,6 +2127,55 @@ impl CollabPanel { self.update_entries(false, cx); } + fn collapse_selected_channel( + &mut self, + _: &CollapseSelectedChannel, + cx: &mut ViewContext, + ) { + let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else { + return; + }; + + if self.is_channel_collapsed(channel_id) { + return; + } + + self.toggle_channel_collapsed(&ToggleCollapse { channel_id }, cx) + } + + fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext) { + let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else { + return; + }; + + if !self.is_channel_collapsed(channel_id) { + return; + } + + self.toggle_channel_collapsed(&ToggleCollapse { channel_id }, cx) + } + + fn toggle_channel_collapsed(&mut self, action: &ToggleCollapse, cx: &mut ViewContext) { + let channel_id = action.channel_id; + + match self.collapsed_channels.binary_search(&channel_id) { + Ok(ix) => { + self.collapsed_channels.remove(ix); + } + Err(ix) => { + self.collapsed_channels.insert(ix, channel_id); + } + }; + self.serialize(cx); + self.update_entries(true, cx); + cx.notify(); + cx.focus_self(); + } + + fn is_channel_collapsed(&self, channel: ChannelId) -> bool { + self.collapsed_channels.binary_search(&channel).is_ok() + } + fn leave_call(cx: &mut ViewContext) { ActiveCall::global(cx) .update(cx, |call, cx| call.hang_up(cx)) @@ -2048,6 +2215,8 @@ impl CollabPanel { } fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext) { + self.collapsed_channels + .retain(|&channel| channel != action.channel_id); self.channel_editing_state = Some(ChannelEditingState::Create { parent_id: Some(action.channel_id), pending_name: None, @@ -2103,6 +2272,21 @@ impl CollabPanel { } } + fn open_channel_buffer(&mut self, action: &OpenChannelBuffer, cx: &mut ViewContext) { + if let Some(workspace) = self.workspace.upgrade(cx) { + let pane = workspace.read(cx).active_pane().clone(); + let channel_view = ChannelView::open(action.channel_id, pane.clone(), workspace, cx); + cx.spawn(|_, mut cx| async move { + let channel_view = channel_view.await?; + pane.update(&mut cx, |pane, cx| { + pane.add_item(Box::new(channel_view), true, true, None, cx) + }); + anyhow::Ok(()) + }) + .detach(); + } + } + fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext) { let Some(channel) = self.selected_channel() else { return; @@ -2261,6 +2445,51 @@ impl CollabPanel { } } +fn render_tree_branch( + branch_style: theme::TreeBranch, + row_style: &TextStyle, + is_last: bool, + size: Vector2F, + font_cache: &FontCache, +) -> gpui::elements::ConstrainedBox { + let line_height = row_style.line_height(font_cache); + let cap_height = row_style.cap_height(font_cache); + let baseline_offset = row_style.baseline_offset(font_cache) + (size.y() - line_height) / 2.; + + Canvas::new(move |scene, bounds, _, _, _| { + scene.paint_layer(None, |scene| { + let start_x = bounds.min_x() + (bounds.width() / 2.) - (branch_style.width / 2.); + let end_x = bounds.max_x(); + let start_y = bounds.min_y(); + let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.); + + scene.push_quad(gpui::Quad { + bounds: RectF::from_points( + vec2f(start_x, start_y), + vec2f( + start_x + branch_style.width, + if is_last { end_y } else { bounds.max_y() }, + ), + ), + background: Some(branch_style.color), + border: gpui::Border::default(), + corner_radii: (0.).into(), + }); + scene.push_quad(gpui::Quad { + bounds: RectF::from_points( + vec2f(start_x, end_y), + vec2f(end_x, end_y + branch_style.width), + ), + background: Some(branch_style.color), + border: gpui::Border::default(), + corner_radii: (0.).into(), + }); + }) + }) + .constrained() + .with_width(size.x()) +} + impl View for CollabPanel { fn ui_name() -> &'static str { "CollabPanel" @@ -2470,6 +2699,14 @@ impl PartialEq for ListEntry { return channel_1.id == channel_2.id && depth_1 == depth_2; } } + ListEntry::ChannelNotes { channel_id } => { + if let ListEntry::ChannelNotes { + channel_id: other_id, + } = other + { + return channel_id == other_id; + } + } ListEntry::ChannelInvite(channel_1) => { if let ListEntry::ChannelInvite(channel_2) = other { return channel_1.id == channel_2.id; diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index 75ab40be85adb1e1df7678cf292c4c177237db0c..4c811a2df547dc78e0a602ae2002a4e9dbeb4e46 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -1,4 +1,5 @@ -use client::{proto, ChannelId, ChannelMembership, ChannelStore, User, UserId, UserStore}; +use channel::{ChannelId, ChannelMembership, ChannelStore}; +use client::{proto, User, UserId, UserStore}; use context_menu::{ContextMenu, ContextMenuItem}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ @@ -151,12 +152,9 @@ impl View for ChannelModal { let theme = &theme::current(cx).collab_panel.tabbed_modal; let mode = self.picker.read(cx).delegate().mode; - let Some(channel) = self - .channel_store - .read(cx) - .channel_for_id(self.channel_id) else { - return Empty::new().into_any() - }; + let Some(channel) = self.channel_store.read(cx).channel_for_id(self.channel_id) else { + return Empty::new().into_any(); + }; enum InviteMembers {} enum ManageMembers {} diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 5420dd1db5733882f8f08b939f2bc15ecaf83949..04644b62d985698dcebfdfea352cc3cb2e15f824 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -1,3 +1,4 @@ +pub mod channel_view; pub mod collab_panel; mod collab_titlebar_item; mod contact_notification; diff --git a/crates/component_test/Cargo.toml b/crates/component_test/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..d714f6f72f62bf0c869439d07e937592a6ea644b --- /dev/null +++ b/crates/component_test/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "component_test" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/component_test.rs" +doctest = false + +[dependencies] +anyhow.workspace = true +gpui = { path = "../gpui" } +settings = { path = "../settings" } +util = { path = "../util" } +theme = { path = "../theme" } +workspace = { path = "../workspace" } +project = { path = "../project" } diff --git a/crates/component_test/src/component_test.rs b/crates/component_test/src/component_test.rs new file mode 100644 index 0000000000000000000000000000000000000000..9f6b4918b9e065f6627e0f9765e0827ccfb54c9b --- /dev/null +++ b/crates/component_test/src/component_test.rs @@ -0,0 +1,121 @@ +use gpui::{ + actions, + elements::{Component, Flex, ParentElement, SafeStylable}, + AppContext, Element, Entity, ModelHandle, Task, View, ViewContext, ViewHandle, WeakViewHandle, +}; +use project::Project; +use theme::components::{action_button::Button, label::Label, ComponentExt}; +use workspace::{ + item::Item, register_deserializable_item, ItemId, Pane, PaneBackdrop, Workspace, WorkspaceId, +}; + +pub fn init(cx: &mut AppContext) { + cx.add_action(ComponentTest::toggle_disclosure); + cx.add_action(ComponentTest::toggle_toggle); + cx.add_action(ComponentTest::deploy); + register_deserializable_item::(cx); +} + +actions!( + test, + [NoAction, ToggleDisclosure, ToggleToggle, NewComponentTest] +); + +struct ComponentTest { + disclosed: bool, + toggled: bool, +} + +impl ComponentTest { + fn new() -> Self { + Self { + disclosed: false, + toggled: false, + } + } + + fn deploy(workspace: &mut Workspace, _: &NewComponentTest, cx: &mut ViewContext) { + workspace.add_item(Box::new(cx.add_view(|_| ComponentTest::new())), cx); + } + + fn toggle_disclosure(&mut self, _: &ToggleDisclosure, cx: &mut ViewContext) { + self.disclosed = !self.disclosed; + cx.notify(); + } + + fn toggle_toggle(&mut self, _: &ToggleToggle, cx: &mut ViewContext) { + self.toggled = !self.toggled; + cx.notify(); + } +} + +impl Entity for ComponentTest { + type Event = (); +} + +impl View for ComponentTest { + fn ui_name() -> &'static str { + "Component Test" + } + + fn render(&mut self, cx: &mut gpui::ViewContext) -> gpui::AnyElement { + let theme = theme::current(cx); + + PaneBackdrop::new( + cx.view_id(), + Flex::column() + .with_spacing(10.) + .with_child( + Button::action(NoAction) + .with_tooltip("Here's what a tooltip looks like", theme.tooltip.clone()) + .with_contents(Label::new("Click me!")) + .with_style(theme.component_test.button.clone()) + .element(), + ) + .with_child( + Button::action(ToggleToggle) + .with_tooltip("Here's what a tooltip looks like", theme.tooltip.clone()) + .with_contents(Label::new("Toggle me!")) + .toggleable(self.toggled) + .with_style(theme.component_test.toggle.clone()) + .element(), + ) + .with_child( + Label::new("A disclosure") + .disclosable(Some(self.disclosed), Box::new(ToggleDisclosure)) + .with_style(theme.component_test.disclosure.clone()) + .element(), + ) + .constrained() + .with_width(200.) + .aligned() + .into_any(), + ) + .into_any() + } +} + +impl Item for ComponentTest { + fn tab_content( + &self, + _: Option, + style: &theme::Tab, + _: &AppContext, + ) -> gpui::AnyElement { + gpui::elements::Label::new("Component test", style.label.clone()).into_any() + } + + fn serialized_item_kind() -> Option<&'static str> { + Some("ComponentTest") + } + + fn deserialize( + _project: ModelHandle, + _workspace: WeakViewHandle, + _workspace_id: WorkspaceId, + _item_id: ItemId, + cx: &mut ViewContext, + ) -> Task>> { + Task::ready(Ok(cx.add_view(|_| Self::new()))) + } +} diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index ab2d861190ff98fb7b4da954a7b92bfb43d75a9d..427134894f3a7383febb571357bf39083e9b06cc 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -980,7 +980,7 @@ mod tests { deterministic.forbid_parking(); let (copilot, mut lsp) = Copilot::fake(cx); - let buffer_1 = cx.add_model(|cx| Buffer::new(0, "Hello", cx)); + let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "Hello")); let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.id()).parse().unwrap(); copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx)); assert_eq!( @@ -996,7 +996,7 @@ mod tests { } ); - let buffer_2 = cx.add_model(|cx| Buffer::new(0, "Goodbye", cx)); + let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "Goodbye")); let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.id()).parse().unwrap(); copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx)); assert_eq!( diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index aee41e6c53f0c1e35e68e3716db55f609a44fb08..e8e15a927eaacabc37c88884094a97f105bf9bb7 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -4,7 +4,10 @@ mod inlay_map; mod tab_map; mod wrap_map; -use crate::{Anchor, AnchorRangeExt, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint}; +use crate::{ + link_go_to_definition::{DocumentRange, InlayRange}, + Anchor, AnchorRangeExt, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, +}; pub use block_map::{BlockMap, BlockPoint}; use collections::{HashMap, HashSet}; use fold_map::FoldMap; @@ -27,7 +30,8 @@ pub use block_map::{ BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock, }; -pub use self::inlay_map::Inlay; +pub use self::fold_map::FoldPoint; +pub use self::inlay_map::{Inlay, InlayOffset, InlayPoint}; #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum FoldStatus { @@ -39,7 +43,7 @@ pub trait ToDisplayPoint { fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint; } -type TextHighlights = TreeMap, Arc<(HighlightStyle, Vec>)>>; +type TextHighlights = TreeMap, Arc<(HighlightStyle, Vec)>>; pub struct DisplayMap { buffer: ModelHandle, @@ -211,11 +215,28 @@ impl DisplayMap { ranges: Vec>, style: HighlightStyle, ) { - self.text_highlights - .insert(Some(type_id), Arc::new((style, ranges))); + self.text_highlights.insert( + Some(type_id), + Arc::new((style, ranges.into_iter().map(DocumentRange::Text).collect())), + ); } - pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[Range])> { + pub fn highlight_inlays( + &mut self, + type_id: TypeId, + ranges: Vec, + style: HighlightStyle, + ) { + self.text_highlights.insert( + Some(type_id), + Arc::new(( + style, + ranges.into_iter().map(DocumentRange::Inlay).collect(), + )), + ); + } + + pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[DocumentRange])> { let highlights = self.text_highlights.get(&Some(type_id))?; Some((highlights.0, &highlights.1)) } @@ -223,7 +244,7 @@ impl DisplayMap { pub fn clear_text_highlights( &mut self, type_id: TypeId, - ) -> Option>)>> { + ) -> Option)>> { self.text_highlights.remove(&Some(type_id)) } @@ -290,7 +311,7 @@ impl DisplayMap { pub struct DisplaySnapshot { pub buffer_snapshot: MultiBufferSnapshot, - fold_snapshot: fold_map::FoldSnapshot, + pub fold_snapshot: fold_map::FoldSnapshot, inlay_snapshot: inlay_map::InlaySnapshot, tab_snapshot: tab_map::TabSnapshot, wrap_snapshot: wrap_map::WrapSnapshot, @@ -387,12 +408,49 @@ impl DisplaySnapshot { } fn display_point_to_point(&self, point: DisplayPoint, bias: Bias) -> Point { + self.inlay_snapshot + .to_buffer_point(self.display_point_to_inlay_point(point, bias)) + } + + pub fn display_point_to_inlay_offset(&self, point: DisplayPoint, bias: Bias) -> InlayOffset { + self.inlay_snapshot + .to_offset(self.display_point_to_inlay_point(point, bias)) + } + + pub fn anchor_to_inlay_offset(&self, anchor: Anchor) -> InlayOffset { + self.inlay_snapshot + .to_inlay_offset(anchor.to_offset(&self.buffer_snapshot)) + } + + pub fn inlay_offset_to_display_point(&self, offset: InlayOffset, bias: Bias) -> DisplayPoint { + let inlay_point = self.inlay_snapshot.to_point(offset); + let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias); + let tab_point = self.tab_snapshot.to_tab_point(fold_point); + let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point); + let block_point = self.block_snapshot.to_block_point(wrap_point); + DisplayPoint(block_point) + } + + fn display_point_to_inlay_point(&self, point: DisplayPoint, bias: Bias) -> InlayPoint { let block_point = point.0; let wrap_point = self.block_snapshot.to_wrap_point(block_point); let tab_point = self.wrap_snapshot.to_tab_point(wrap_point); let fold_point = self.tab_snapshot.to_fold_point(tab_point, bias).0; - let inlay_point = fold_point.to_inlay_point(&self.fold_snapshot); - self.inlay_snapshot.to_buffer_point(inlay_point) + fold_point.to_inlay_point(&self.fold_snapshot) + } + + pub fn display_point_to_fold_point(&self, point: DisplayPoint, bias: Bias) -> FoldPoint { + let block_point = point.0; + let wrap_point = self.block_snapshot.to_wrap_point(block_point); + let tab_point = self.wrap_snapshot.to_tab_point(wrap_point); + self.tab_snapshot.to_fold_point(tab_point, bias).0 + } + + pub fn fold_point_to_display_point(&self, fold_point: FoldPoint) -> DisplayPoint { + let tab_point = self.tab_snapshot.to_tab_point(fold_point); + let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point); + let block_point = self.block_snapshot.to_block_point(wrap_point); + DisplayPoint(block_point) } pub fn max_point(&self) -> DisplayPoint { @@ -428,15 +486,15 @@ impl DisplaySnapshot { &self, display_rows: Range, language_aware: bool, - hint_highlights: Option, - suggestion_highlights: Option, + hint_highlight_style: Option, + suggestion_highlight_style: Option, ) -> DisplayChunks<'_> { self.block_snapshot.chunks( display_rows, language_aware, Some(&self.text_highlights), - hint_highlights, - suggestion_highlights, + hint_highlight_style, + suggestion_highlight_style, ) } @@ -757,7 +815,7 @@ impl DisplaySnapshot { #[cfg(any(test, feature = "test-support"))] pub fn highlight_ranges( &self, - ) -> Option>)>> { + ) -> Option)>> { let type_id = TypeId::of::(); self.text_highlights.get(&Some(type_id)).cloned() } @@ -1319,7 +1377,8 @@ pub mod tests { cx.update(|cx| init_test(cx, |s| s.defaults.tab_size = Some(2.try_into().unwrap()))); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); + let buffer = cx + .add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); buffer.condition(cx, |buf, _| !buf.is_parsing()).await; let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); @@ -1408,7 +1467,8 @@ pub mod tests { cx.update(|cx| init_test(cx, |_| {})); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); + let buffer = cx + .add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); buffer.condition(cx, |buf, _| !buf.is_parsing()).await; let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); @@ -1480,7 +1540,8 @@ pub mod tests { let (text, highlighted_ranges) = marked_text_ranges(r#"constˇ «a»: B = "c «d»""#, false); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); + let buffer = cx + .add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); buffer.condition(cx, |buf, _| !buf.is_parsing()).await; let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 4b76ded3d50cc45d72385d70bbb424b139023f09..741507004cc9bc0064ba682701310b832111438f 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -589,8 +589,8 @@ impl BlockSnapshot { rows: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - hint_highlights: Option, - suggestion_highlights: Option, + hint_highlight_style: Option, + suggestion_highlight_style: Option, ) -> BlockChunks<'a> { let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows); let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(); @@ -623,8 +623,8 @@ impl BlockSnapshot { input_start..input_end, language_aware, text_highlights, - hint_highlights, - suggestion_highlights, + hint_highlight_style, + suggestion_highlight_style, ), input_chunk: Default::default(), transforms: cursor, diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 0b1523fe750326dea5c87f3ec4dcfa350f497185..d5473027a6b0145bad28f21c1e91ce7491f9eb63 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -652,8 +652,8 @@ impl FoldSnapshot { range: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - hint_highlights: Option, - suggestion_highlights: Option, + hint_highlight_style: Option, + suggestion_highlight_style: Option, ) -> FoldChunks<'a> { let mut transform_cursor = self.transforms.cursor::<(FoldOffset, InlayOffset)>(); @@ -675,8 +675,8 @@ impl FoldSnapshot { inlay_start..inlay_end, language_aware, text_highlights, - hint_highlights, - suggestion_highlights, + hint_highlight_style, + suggestion_highlight_style, ), inlay_chunk: None, inlay_offset: inlay_start, diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 9794ac45c1190ec88ccb471ee61630ec50d320ca..25b8d3aef6a28b959a6092e1cfba4adf031dd125 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1,4 +1,5 @@ use crate::{ + link_go_to_definition::DocumentRange, multi_buffer::{MultiBufferChunks, MultiBufferRows}, Anchor, InlayId, MultiBufferSnapshot, ToOffset, }; @@ -183,7 +184,7 @@ pub struct InlayBufferRows<'a> { max_buffer_row: u32, } -#[derive(Copy, Clone, Eq, PartialEq)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] struct HighlightEndpoint { offset: InlayOffset, is_start: bool, @@ -210,6 +211,7 @@ pub struct InlayChunks<'a> { buffer_chunks: MultiBufferChunks<'a>, buffer_chunk: Option>, inlay_chunks: Option>, + inlay_chunk: Option<&'a str>, output_offset: InlayOffset, max_output_offset: InlayOffset, hint_highlight_style: Option, @@ -297,13 +299,31 @@ impl<'a> Iterator for InlayChunks<'a> { - self.transforms.start().0; inlay.text.chunks_in_range(start.0..end.0) }); + let inlay_chunk = self + .inlay_chunk + .get_or_insert_with(|| inlay_chunks.next().unwrap()); + let (chunk, remainder) = inlay_chunk.split_at( + inlay_chunk + .len() + .min(next_highlight_endpoint.0 - self.output_offset.0), + ); + *inlay_chunk = remainder; + if inlay_chunk.is_empty() { + self.inlay_chunk = None; + } - let chunk = inlay_chunks.next().unwrap(); self.output_offset.0 += chunk.len(); - let highlight_style = match inlay.id { + let mut highlight_style = match inlay.id { InlayId::Suggestion(_) => self.suggestion_highlight_style, InlayId::Hint(_) => self.hint_highlight_style, }; + if !self.active_highlights.is_empty() { + for active_highlight in self.active_highlights.values() { + highlight_style + .get_or_insert(Default::default()) + .highlight(*active_highlight); + } + } Chunk { text: chunk, highlight_style, @@ -973,8 +993,8 @@ impl InlaySnapshot { range: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - hint_highlights: Option, - suggestion_highlights: Option, + hint_highlight_style: Option, + suggestion_highlight_style: Option, ) -> InlayChunks<'a> { let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(); cursor.seek(&range.start, Bias::Right, &()); @@ -983,52 +1003,56 @@ impl InlaySnapshot { if let Some(text_highlights) = text_highlights { if !text_highlights.is_empty() { while cursor.start().0 < range.end { - if true { - let transform_start = self.buffer.anchor_after( - self.to_buffer_offset(cmp::max(range.start, cursor.start().0)), - ); - - let transform_end = { - let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0); - self.buffer.anchor_before(self.to_buffer_offset(cmp::min( - cursor.end(&()).0, - cursor.start().0 + overshoot, - ))) + let transform_start = self.buffer.anchor_after( + self.to_buffer_offset(cmp::max(range.start, cursor.start().0)), + ); + let transform_start = + self.to_inlay_offset(transform_start.to_offset(&self.buffer)); + + let transform_end = { + let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0); + self.buffer.anchor_before(self.to_buffer_offset(cmp::min( + cursor.end(&()).0, + cursor.start().0 + overshoot, + ))) + }; + let transform_end = self.to_inlay_offset(transform_end.to_offset(&self.buffer)); + + for (tag, text_highlights) in text_highlights.iter() { + let style = text_highlights.0; + let ranges = &text_highlights.1; + + let start_ix = match ranges.binary_search_by(|probe| { + let cmp = self + .document_to_inlay_range(probe) + .end + .cmp(&transform_start); + if cmp.is_gt() { + cmp::Ordering::Greater + } else { + cmp::Ordering::Less + } + }) { + Ok(i) | Err(i) => i, }; - - for (tag, highlights) in text_highlights.iter() { - let style = highlights.0; - let ranges = &highlights.1; - - let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&transform_start, &self.buffer); - if cmp.is_gt() { - cmp::Ordering::Greater - } else { - cmp::Ordering::Less - } - }) { - Ok(i) | Err(i) => i, - }; - for range in &ranges[start_ix..] { - if range.start.cmp(&transform_end, &self.buffer).is_ge() { - break; - } - - highlight_endpoints.push(HighlightEndpoint { - offset: self - .to_inlay_offset(range.start.to_offset(&self.buffer)), - is_start: true, - tag: *tag, - style, - }); - highlight_endpoints.push(HighlightEndpoint { - offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)), - is_start: false, - tag: *tag, - style, - }); + for range in &ranges[start_ix..] { + let range = self.document_to_inlay_range(range); + if range.start.cmp(&transform_end).is_ge() { + break; } + + highlight_endpoints.push(HighlightEndpoint { + offset: range.start, + is_start: true, + tag: *tag, + style, + }); + highlight_endpoints.push(HighlightEndpoint { + offset: range.end, + is_start: false, + tag: *tag, + style, + }); } } @@ -1046,17 +1070,30 @@ impl InlaySnapshot { transforms: cursor, buffer_chunks, inlay_chunks: None, + inlay_chunk: None, buffer_chunk: None, output_offset: range.start, max_output_offset: range.end, - hint_highlight_style: hint_highlights, - suggestion_highlight_style: suggestion_highlights, + hint_highlight_style, + suggestion_highlight_style, highlight_endpoints: highlight_endpoints.into_iter().peekable(), active_highlights: Default::default(), snapshot: self, } } + fn document_to_inlay_range(&self, range: &DocumentRange) -> Range { + match range { + DocumentRange::Text(text_range) => { + self.to_inlay_offset(text_range.start.to_offset(&self.buffer)) + ..self.to_inlay_offset(text_range.end.to_offset(&self.buffer)) + } + DocumentRange::Inlay(inlay_range) => { + inlay_range.highlight_start..inlay_range.highlight_end + } + } + } + #[cfg(test)] pub fn text(&self) -> String { self.chunks(Default::default()..self.len(), false, None, None, None) @@ -1107,13 +1144,12 @@ fn push_isomorphic(sum_tree: &mut SumTree, summary: TextSummary) { #[cfg(test)] mod tests { use super::*; - use crate::{InlayId, MultiBuffer}; + use crate::{link_go_to_definition::InlayRange, InlayId, MultiBuffer}; use gpui::AppContext; - use project::{InlayHint, InlayHintLabel}; + use project::{InlayHint, InlayHintLabel, ResolveState}; use rand::prelude::*; use settings::SettingsStore; use std::{cmp::Reverse, env, sync::Arc}; - use sum_tree::TreeMap; use text::Patch; use util::post_inc; @@ -1125,12 +1161,12 @@ mod tests { 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, + resolve_state: ResolveState::Resolved, }, ) .text @@ -1145,12 +1181,12 @@ mod tests { 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, + resolve_state: ResolveState::Resolved, }, ) .text @@ -1165,12 +1201,12 @@ mod tests { 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, + resolve_state: ResolveState::Resolved, }, ) .text @@ -1185,12 +1221,12 @@ mod tests { 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, + resolve_state: ResolveState::Resolved, }, ) .text @@ -1542,26 +1578,6 @@ mod tests { let mut buffer_snapshot = buffer.read(cx).snapshot(cx); let mut next_inlay_id = 0; log::info!("buffer text: {:?}", buffer_snapshot.text()); - - let mut highlights = TreeMap::default(); - let highlight_count = rng.gen_range(0_usize..10); - let mut highlight_ranges = (0..highlight_count) - .map(|_| buffer_snapshot.random_byte_range(0, &mut rng)) - .collect::>(); - highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end))); - log::info!("highlighting ranges {:?}", highlight_ranges); - let highlight_ranges = highlight_ranges - .into_iter() - .map(|range| { - buffer_snapshot.anchor_before(range.start)..buffer_snapshot.anchor_after(range.end) - }) - .collect::>(); - - highlights.insert( - Some(TypeId::of::<()>()), - Arc::new((HighlightStyle::default(), highlight_ranges)), - ); - let (mut inlay_map, mut inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); for _ in 0..operations { let mut inlay_edits = Patch::default(); @@ -1624,6 +1640,38 @@ mod tests { ); } + let mut highlights = TextHighlights::default(); + let highlight_count = rng.gen_range(0_usize..10); + let mut highlight_ranges = (0..highlight_count) + .map(|_| buffer_snapshot.random_byte_range(0, &mut rng)) + .collect::>(); + highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end))); + log::info!("highlighting ranges {:?}", highlight_ranges); + let highlight_ranges = if rng.gen_bool(0.5) { + highlight_ranges + .into_iter() + .map(|range| InlayRange { + inlay_position: buffer_snapshot.anchor_before(range.start), + highlight_start: inlay_snapshot.to_inlay_offset(range.start), + highlight_end: inlay_snapshot.to_inlay_offset(range.end), + }) + .map(DocumentRange::Inlay) + .collect::>() + } else { + highlight_ranges + .into_iter() + .map(|range| { + buffer_snapshot.anchor_before(range.start) + ..buffer_snapshot.anchor_after(range.end) + }) + .map(DocumentRange::Text) + .collect::>() + }; + highlights.insert( + Some(TypeId::of::<()>()), + Arc::new((HighlightStyle::default(), highlight_ranges)), + ); + for _ in 0..5 { let mut end = rng.gen_range(0..=inlay_snapshot.len().0); end = expected_text.clip_offset(end, Bias::Right); diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index ca73f6a1a7a7e5bff4d19a32db548c9d2155f744..2cf0471b37889a5cf5d3db26cfe3d1de91dc8e20 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -224,8 +224,8 @@ impl TabSnapshot { range: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - hint_highlights: Option, - suggestion_highlights: Option, + hint_highlight_style: Option, + suggestion_highlight_style: Option, ) -> TabChunks<'a> { let (input_start, expanded_char_column, to_next_stop) = self.to_fold_point(range.start, Bias::Left); @@ -246,8 +246,8 @@ impl TabSnapshot { input_start..input_end, language_aware, text_highlights, - hint_highlights, - suggestion_highlights, + hint_highlight_style, + suggestion_highlight_style, ), input_column, column: expanded_char_column, diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index f21c7151ad695b2567dc9cea7da5a5007be1696f..f3600936f9bf77df6773ad14fd39f4e465398e15 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -576,8 +576,8 @@ impl WrapSnapshot { rows: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - hint_highlights: Option, - suggestion_highlights: Option, + hint_highlight_style: Option, + suggestion_highlight_style: Option, ) -> WrapChunks<'a> { let output_start = WrapPoint::new(rows.start, 0); let output_end = WrapPoint::new(rows.end, 0); @@ -595,8 +595,8 @@ impl WrapSnapshot { input_start..input_end, language_aware, text_highlights, - hint_highlights, - suggestion_highlights, + hint_highlight_style, + suggestion_highlight_style, ), input_chunk: Default::default(), output_position: output_start, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b51f7ae25541aad9df19c117552c48af50c5e396..fe8e4e338c6831e381ea788a68dd7315140529b0 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -23,7 +23,7 @@ pub mod test; use ::git::diff::DiffHunk; use aho_corasick::AhoCorasick; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; use blink_manager::BlinkManager; use client::{ClickhouseEvent, TelemetrySettings}; use clock::{Global, ReplicaId}; @@ -60,21 +60,24 @@ use itertools::Itertools; pub use language::{char_kind, CharKind}; use language::{ language_settings::{self, all_language_settings, InlayHintSettings}, - AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape, - Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, OffsetRangeExt, - OffsetUtf16, Point, Selection, SelectionGoal, TransactionId, + point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, + CursorShape, Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, + LanguageServerName, OffsetRangeExt, OffsetUtf16, Point, Selection, SelectionGoal, + TransactionId, }; use link_go_to_definition::{ - hide_link_definition, show_link_definition, LinkDefinitionKind, LinkGoToDefinitionState, + hide_link_definition, show_link_definition, DocumentRange, GoToDefinitionLink, InlayRange, + LinkGoToDefinitionState, }; use log::error; +use lsp::LanguageServerId; use multi_buffer::ToOffsetUtf16; pub use multi_buffer::{ Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, }; use ordered_float::OrderedFloat; -use project::{FormatTrigger, Location, LocationLink, Project, ProjectPath, ProjectTransaction}; +use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction}; use rand::{seq::SliceRandom, thread_rng}; use scroll::{ autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide, @@ -108,6 +111,8 @@ const MAX_LINE_LEN: usize = 1024; const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10; const MAX_SELECTION_HISTORY_LEN: usize = 1024; const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75); +pub const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250); +pub const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75); pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); @@ -535,6 +540,8 @@ type CompletionId = usize; type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor; type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option; +type BackgroundHighlight = (fn(&Theme) -> Color, Vec); + pub struct Editor { handle: WeakViewHandle, buffer: ModelHandle, @@ -559,12 +566,12 @@ pub struct Editor { blink_manager: ModelHandle, show_local_selections: bool, mode: EditorMode, + replica_id_mapping: Option>, show_gutter: bool, show_wrap_guides: Option, placeholder_text: Option>, highlighted_rows: Option>, - #[allow(clippy::type_complexity)] - background_highlights: BTreeMap Color, Vec>)>, + background_highlights: BTreeMap, nav_history: Option, context_menu: Option, mouse_context_menu: ViewHandle, @@ -1246,6 +1253,19 @@ enum InlayHintRefreshReason { NewLinesShown, BufferEdited(HashSet>), RefreshRequested, + ExcerptsRemoved(Vec), +} +impl InlayHintRefreshReason { + fn description(&self) -> &'static str { + match self { + Self::Toggle(_) => "toggle", + Self::SettingsChange(_) => "settings change", + Self::NewLinesShown => "new lines shown", + Self::BufferEdited(_) => "buffer edited", + Self::RefreshRequested => "refresh requested", + Self::ExcerptsRemoved(_) => "excerpts removed", + } + } } impl Editor { @@ -1253,7 +1273,7 @@ impl Editor { field_editor_style: Option>, cx: &mut ViewContext, ) -> Self { - let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, String::new())); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); Self::new(EditorMode::SingleLine, buffer, None, field_editor_style, cx) } @@ -1262,7 +1282,7 @@ impl Editor { field_editor_style: Option>, cx: &mut ViewContext, ) -> Self { - let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, String::new())); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); Self::new(EditorMode::Full, buffer, None, field_editor_style, cx) } @@ -1272,7 +1292,7 @@ impl Editor { field_editor_style: Option>, cx: &mut ViewContext, ) -> Self { - let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, String::new())); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); Self::new( EditorMode::AutoHeight { max_lines }, @@ -1394,6 +1414,7 @@ impl Editor { blink_manager: blink_manager.clone(), show_local_selections: true, mode, + replica_id_mapping: None, show_gutter: mode == EditorMode::Full, show_wrap_guides: None, placeholder_text: None, @@ -1604,6 +1625,19 @@ impl Editor { self.read_only = read_only; } + pub fn replica_id_map(&self) -> Option<&HashMap> { + self.replica_id_mapping.as_ref() + } + + pub fn set_replica_id_map( + &mut self, + mapping: Option>, + cx: &mut ViewContext, + ) { + self.replica_id_mapping = mapping; + cx.notify(); + } + fn selections_did_change( &mut self, local: bool, @@ -2722,6 +2756,7 @@ impl Editor { return; } + let reason_description = reason.description(); let (invalidate_cache, required_languages) = match reason { InlayHintRefreshReason::Toggle(enabled) => { self.inlay_hint_cache.enabled = enabled; @@ -2758,6 +2793,16 @@ impl Editor { ControlFlow::Continue(()) => (InvalidationStrategy::RefreshRequested, None), } } + InlayHintRefreshReason::ExcerptsRemoved(excerpts_removed) => { + if let Some(InlaySplice { + to_remove, + to_insert, + }) = self.inlay_hint_cache.remove_excerpts(excerpts_removed) + { + self.splice_inlay_hints(to_remove, to_insert, cx); + } + return; + } InlayHintRefreshReason::NewLinesShown => (InvalidationStrategy::None, None), InlayHintRefreshReason::BufferEdited(buffer_languages) => { (InvalidationStrategy::BufferEdited, Some(buffer_languages)) @@ -2771,6 +2816,7 @@ impl Editor { to_remove, to_insert, }) = self.inlay_hint_cache.spawn_hint_refresh( + reason_description, self.excerpt_visible_offsets(required_languages.as_ref(), cx), invalidate_cache, cx, @@ -3248,7 +3294,7 @@ impl Editor { } fn refresh_code_actions(&mut self, cx: &mut ViewContext) -> Option<()> { - let project = self.project.as_ref()?; + let project = self.project.clone()?; let buffer = self.buffer.read(cx); let newest_selection = self.selections.newest_anchor().clone(); let (start_buffer, start) = buffer.text_anchor_for_position(newest_selection.start, cx)?; @@ -3257,11 +3303,15 @@ impl Editor { return None; } - let actions = project.update(cx, |project, cx| { - project.code_actions(&start_buffer, start..end, cx) - }); self.code_actions_task = Some(cx.spawn(|this, mut cx| async move { - let actions = actions.await; + cx.background().timer(CODE_ACTIONS_DEBOUNCE_TIMEOUT).await; + + let actions = project + .update(&mut cx, |project, cx| { + project.code_actions(&start_buffer, start..end, cx) + }) + .await; + this.update(&mut cx, |this, cx| { this.available_code_actions = actions.log_err().and_then(|actions| { if actions.is_empty() { @@ -3282,7 +3332,7 @@ impl Editor { return None; } - let project = self.project.as_ref()?; + let project = self.project.clone()?; let buffer = self.buffer.read(cx); let newest_selection = self.selections.newest_anchor().clone(); let cursor_position = newest_selection.head(); @@ -3293,12 +3343,19 @@ impl Editor { return None; } - let highlights = project.update(cx, |project, cx| { - project.document_highlights(&cursor_buffer, cursor_buffer_position, cx) - }); - self.document_highlights_task = Some(cx.spawn(|this, mut cx| async move { - if let Some(highlights) = highlights.await.log_err() { + cx.background() + .timer(DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT) + .await; + + let highlights = project + .update(&mut cx, |project, cx| { + project.document_highlights(&cursor_buffer, cursor_buffer_position, cx) + }) + .await + .log_err(); + + if let Some(highlights) = highlights { this.update(&mut cx, |this, cx| { if this.pending_rename.is_some() { return; @@ -6227,7 +6284,9 @@ impl Editor { ) { self.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_offsets_with(|snapshot, selection| { - let Some(enclosing_bracket_ranges) = snapshot.enclosing_bracket_ranges(selection.start..selection.end) else { + let Some(enclosing_bracket_ranges) = + snapshot.enclosing_bracket_ranges(selection.start..selection.end) + else { return; }; @@ -6239,7 +6298,8 @@ impl Editor { let close = close.to_inclusive(); let length = close.end() - open.start; let inside = selection.start >= open.end && selection.end <= *close.start(); - let in_bracket_range = open.to_inclusive().contains(&selection.head()) || close.contains(&selection.head()); + let in_bracket_range = open.to_inclusive().contains(&selection.head()) + || close.contains(&selection.head()); // If best is next to a bracket and current isn't, skip if !in_bracket_range && best_in_bracket_range { @@ -6254,19 +6314,21 @@ impl Editor { best_length = length; best_inside = inside; best_in_bracket_range = in_bracket_range; - best_destination = Some(if close.contains(&selection.start) && close.contains(&selection.end) { - if inside { - open.end - } else { - open.start - } - } else { - if inside { - *close.start() + best_destination = Some( + if close.contains(&selection.start) && close.contains(&selection.end) { + if inside { + open.end + } else { + open.start + } } else { - *close.end() - } - }); + if inside { + *close.start() + } else { + *close.end() + } + }, + ); } if let Some(destination) = best_destination { @@ -6510,7 +6572,9 @@ impl Editor { split: bool, cx: &mut ViewContext, ) { - let Some(workspace) = self.workspace(cx) else { return }; + let Some(workspace) = self.workspace(cx) else { + return; + }; let buffer = self.buffer.read(cx); let head = self.selections.newest::(cx).head(); let (buffer, head) = if let Some(text_anchor) = buffer.text_anchor_for_position(head, cx) { @@ -6528,7 +6592,14 @@ impl Editor { cx.spawn_labeled("Fetching Definition...", |editor, mut cx| async move { let definitions = definitions.await?; editor.update(&mut cx, |editor, cx| { - editor.navigate_to_definitions(definitions, split, cx); + editor.navigate_to_definitions( + definitions + .into_iter() + .map(GoToDefinitionLink::Text) + .collect(), + split, + cx, + ); })?; Ok::<(), anyhow::Error>(()) }) @@ -6537,76 +6608,178 @@ impl Editor { pub fn navigate_to_definitions( &mut self, - mut definitions: Vec, + mut definitions: Vec, split: bool, cx: &mut ViewContext, ) { - let Some(workspace) = self.workspace(cx) else { return }; + let Some(workspace) = self.workspace(cx) else { + return; + }; let pane = workspace.read(cx).active_pane().clone(); // If there is one definition, just open it directly if definitions.len() == 1 { let definition = definitions.pop().unwrap(); - let range = definition - .target - .range - .to_offset(definition.target.buffer.read(cx)); - - let range = self.range_for_match(&range); - if Some(&definition.target.buffer) == self.buffer.read(cx).as_singleton().as_ref() { - self.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.select_ranges([range]); - }); - } else { - cx.window_context().defer(move |cx| { - let target_editor: ViewHandle = workspace.update(cx, |workspace, cx| { - if split { - workspace.split_project_item(definition.target.buffer.clone(), cx) + let target_task = match definition { + GoToDefinitionLink::Text(link) => Task::Ready(Some(Ok(Some(link.target)))), + GoToDefinitionLink::InlayHint(lsp_location, server_id) => { + self.compute_target_location(lsp_location, server_id, cx) + } + }; + cx.spawn(|editor, mut cx| async move { + let target = target_task.await.context("target resolution task")?; + if let Some(target) = target { + editor.update(&mut cx, |editor, cx| { + let range = target.range.to_offset(target.buffer.read(cx)); + let range = editor.range_for_match(&range); + if Some(&target.buffer) == editor.buffer.read(cx).as_singleton().as_ref() { + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges([range]); + }); } else { - workspace.open_project_item(definition.target.buffer.clone(), cx) + cx.window_context().defer(move |cx| { + let target_editor: ViewHandle = + workspace.update(cx, |workspace, cx| { + if split { + workspace.split_project_item(target.buffer.clone(), cx) + } else { + workspace.open_project_item(target.buffer.clone(), cx) + } + }); + target_editor.update(cx, |target_editor, cx| { + // When selecting a definition in a different buffer, disable the nav history + // to avoid creating a history entry at the previous cursor location. + pane.update(cx, |pane, _| pane.disable_history()); + target_editor.change_selections( + Some(Autoscroll::fit()), + cx, + |s| { + s.select_ranges([range]); + }, + ); + pane.update(cx, |pane, _| pane.enable_history()); + }); + }); } - }); - target_editor.update(cx, |target_editor, cx| { - // When selecting a definition in a different buffer, disable the nav history - // to avoid creating a history entry at the previous cursor location. - pane.update(cx, |pane, _| pane.disable_history()); - target_editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.select_ranges([range]); - }); - pane.update(cx, |pane, _| pane.enable_history()); - }); - }); - } + }) + } else { + Ok(()) + } + }) + .detach_and_log_err(cx); } else if !definitions.is_empty() { let replica_id = self.replica_id(cx); - cx.window_context().defer(move |cx| { - let title = definitions - .iter() - .find(|definition| definition.origin.is_some()) - .and_then(|definition| { - definition.origin.as_ref().map(|origin| { - let buffer = origin.buffer.read(cx); - format!( - "Definitions for {}", - buffer - .text_for_range(origin.range.clone()) - .collect::() - ) - }) + cx.spawn(|editor, mut cx| async move { + let (title, location_tasks) = editor + .update(&mut cx, |editor, cx| { + let title = definitions + .iter() + .find_map(|definition| match definition { + GoToDefinitionLink::Text(link) => { + link.origin.as_ref().map(|origin| { + let buffer = origin.buffer.read(cx); + format!( + "Definitions for {}", + buffer + .text_for_range(origin.range.clone()) + .collect::() + ) + }) + } + GoToDefinitionLink::InlayHint(_, _) => None, + }) + .unwrap_or("Definitions".to_string()); + let location_tasks = definitions + .into_iter() + .map(|definition| match definition { + GoToDefinitionLink::Text(link) => { + Task::Ready(Some(Ok(Some(link.target)))) + } + GoToDefinitionLink::InlayHint(lsp_location, server_id) => { + editor.compute_target_location(lsp_location, server_id, cx) + } + }) + .collect::>(); + (title, location_tasks) }) - .unwrap_or("Definitions".to_owned()); - let locations = definitions + .context("location tasks preparation")?; + + let locations = futures::future::join_all(location_tasks) + .await .into_iter() - .map(|definition| definition.target) - .collect(); - workspace.update(cx, |workspace, cx| { + .filter_map(|location| location.transpose()) + .collect::>() + .context("location tasks")?; + workspace.update(&mut cx, |workspace, cx| { Self::open_locations_in_multibuffer( workspace, locations, replica_id, title, split, cx, ) }); - }); + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); } } + fn compute_target_location( + &self, + lsp_location: lsp::Location, + server_id: LanguageServerId, + cx: &mut ViewContext, + ) -> Task>> { + let Some(project) = self.project.clone() else { + return Task::Ready(Some(Ok(None))); + }; + + cx.spawn(move |editor, mut cx| async move { + let location_task = editor.update(&mut cx, |editor, cx| { + project.update(cx, |project, cx| { + let language_server_name = + editor.buffer.read(cx).as_singleton().and_then(|buffer| { + project + .language_server_for_buffer(buffer.read(cx), server_id, cx) + .map(|(_, lsp_adapter)| { + LanguageServerName(Arc::from(lsp_adapter.name())) + }) + }); + language_server_name.map(|language_server_name| { + project.open_local_buffer_via_lsp( + lsp_location.uri.clone(), + server_id, + language_server_name, + cx, + ) + }) + }) + })?; + let location = match location_task { + Some(task) => Some({ + let target_buffer_handle = task.await.context("open local buffer")?; + let range = { + target_buffer_handle.update(&mut cx, |target_buffer, _| { + let target_start = target_buffer.clip_point_utf16( + point_from_lsp(lsp_location.range.start), + Bias::Left, + ); + let target_end = target_buffer.clip_point_utf16( + point_from_lsp(lsp_location.range.end), + Bias::Left, + ); + target_buffer.anchor_after(target_start) + ..target_buffer.anchor_before(target_end) + }) + }; + Location { + buffer: target_buffer_handle, + range, + } + }), + None => None, + }; + Ok(location) + }) + } + pub fn find_all_references( workspace: &mut Workspace, _: &FindAllReferences, @@ -6742,10 +6915,18 @@ impl Editor { let rename_range = if let Some(range) = prepare_rename.await? { Some(range) } else { - this.read_with(&cx, |this, cx| { + this.update(&mut cx, |this, cx| { let buffer = this.buffer.read(cx).snapshot(cx); + let display_snapshot = this + .display_map + .update(cx, |display_map, cx| display_map.snapshot(cx)); let mut buffer_highlights = this - .document_highlights_for_position(selection.head(), &buffer) + .document_highlights_for_position( + selection.head(), + &buffer, + &display_snapshot, + ) + .filter_map(|highlight| highlight.as_text_range()) .filter(|highlight| { highlight.start.excerpt_id() == selection.head().excerpt_id() && highlight.end.excerpt_id() == selection.head().excerpt_id() @@ -6800,11 +6981,15 @@ impl Editor { let ranges = this .clear_background_highlights::(cx) .into_iter() - .flat_map(|(_, ranges)| ranges) + .flat_map(|(_, ranges)| { + ranges.into_iter().filter_map(|range| range.as_text_range()) + }) .chain( this.clear_background_highlights::(cx) .into_iter() - .flat_map(|(_, ranges)| ranges), + .flat_map(|(_, ranges)| { + ranges.into_iter().filter_map(|range| range.as_text_range()) + }), ) .collect(); @@ -7161,7 +7346,7 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let selections = self.selections.all::(cx); + let selections = self.selections.all_adjusted(cx); for selection in selections { let range = selection.range().sorted(); let buffer_start_row = range.start.row; @@ -7237,7 +7422,17 @@ impl Editor { pub fn fold_selected_ranges(&mut self, _: &FoldSelectedRanges, cx: &mut ViewContext) { let selections = self.selections.all::(cx); - let ranges = selections.into_iter().map(|s| s.start..s.end); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let line_mode = self.selections.line_mode; + let ranges = selections.into_iter().map(|s| { + if line_mode { + let start = Point::new(s.start.row, 0); + let end = Point::new(s.end.row, display_map.buffer_snapshot.line_len(s.end.row)); + start..end + } else { + s.start..s.end + } + }); self.fold_ranges(ranges, true, cx); } @@ -7472,16 +7667,36 @@ impl Editor { color_fetcher: fn(&Theme) -> Color, cx: &mut ViewContext, ) { - self.background_highlights - .insert(TypeId::of::(), (color_fetcher, ranges)); + self.background_highlights.insert( + TypeId::of::(), + ( + color_fetcher, + ranges.into_iter().map(DocumentRange::Text).collect(), + ), + ); + cx.notify(); + } + + pub fn highlight_inlay_background( + &mut self, + ranges: Vec, + color_fetcher: fn(&Theme) -> Color, + cx: &mut ViewContext, + ) { + self.background_highlights.insert( + TypeId::of::(), + ( + color_fetcher, + ranges.into_iter().map(DocumentRange::Inlay).collect(), + ), + ); cx.notify(); } - #[allow(clippy::type_complexity)] pub fn clear_background_highlights( &mut self, cx: &mut ViewContext, - ) -> Option<(fn(&Theme) -> Color, Vec>)> { + ) -> Option { let highlights = self.background_highlights.remove(&TypeId::of::()); if highlights.is_some() { cx.notify(); @@ -7506,7 +7721,8 @@ impl Editor { &'a self, position: Anchor, buffer: &'a MultiBufferSnapshot, - ) -> impl 'a + Iterator> { + display_snapshot: &'a DisplaySnapshot, + ) -> impl 'a + Iterator { let read_highlights = self .background_highlights .get(&TypeId::of::()) @@ -7515,14 +7731,16 @@ impl Editor { .background_highlights .get(&TypeId::of::()) .map(|h| &h.1); - let left_position = position.bias_left(buffer); - let right_position = position.bias_right(buffer); + let left_position = display_snapshot.anchor_to_inlay_offset(position.bias_left(buffer)); + let right_position = display_snapshot.anchor_to_inlay_offset(position.bias_right(buffer)); read_highlights .into_iter() .chain(write_highlights) .flat_map(move |ranges| { let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&left_position, buffer); + let cmp = document_to_inlay_range(probe, display_snapshot) + .end + .cmp(&left_position); if cmp.is_ge() { Ordering::Greater } else { @@ -7533,9 +7751,12 @@ impl Editor { }; let right_position = right_position.clone(); - ranges[start_ix..] - .iter() - .take_while(move |range| range.start.cmp(&right_position, buffer).is_le()) + ranges[start_ix..].iter().take_while(move |range| { + document_to_inlay_range(range, display_snapshot) + .start + .cmp(&right_position) + .is_le() + }) }) } @@ -7545,12 +7766,15 @@ impl Editor { display_snapshot: &DisplaySnapshot, theme: &Theme, ) -> Vec<(Range, Color)> { + let search_range = display_snapshot.anchor_to_inlay_offset(search_range.start) + ..display_snapshot.anchor_to_inlay_offset(search_range.end); let mut results = Vec::new(); - let buffer = &display_snapshot.buffer_snapshot; for (color_fetcher, ranges) in self.background_highlights.values() { let color = color_fetcher(theme); let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&search_range.start, buffer); + let cmp = document_to_inlay_range(probe, display_snapshot) + .end + .cmp(&search_range.start); if cmp.is_gt() { Ordering::Greater } else { @@ -7560,61 +7784,16 @@ impl Editor { Ok(i) | Err(i) => i, }; for range in &ranges[start_ix..] { - if range.start.cmp(&search_range.end, buffer).is_ge() { + let range = document_to_inlay_range(range, display_snapshot); + if range.start.cmp(&search_range.end).is_ge() { break; } - let start = range - .start - .to_point(buffer) - .to_display_point(display_snapshot); - let end = range - .end - .to_point(buffer) - .to_display_point(display_snapshot); - results.push((start..end, color)) - } - } - results - } - pub fn background_highlights_in_range_for( - &self, - search_range: Range, - display_snapshot: &DisplaySnapshot, - theme: &Theme, - ) -> Vec<(Range, Color)> { - let mut results = Vec::new(); - let buffer = &display_snapshot.buffer_snapshot; - let Some((color_fetcher, ranges)) = self.background_highlights - .get(&TypeId::of::()) else { - return vec![]; - }; - let color = color_fetcher(theme); - let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&search_range.start, buffer); - if cmp.is_gt() { - Ordering::Greater - } else { - Ordering::Less - } - }) { - Ok(i) | Err(i) => i, - }; - for range in &ranges[start_ix..] { - if range.start.cmp(&search_range.end, buffer).is_ge() { - break; + let start = display_snapshot.inlay_offset_to_display_point(range.start, Bias::Left); + let end = display_snapshot.inlay_offset_to_display_point(range.end, Bias::Right); + results.push((start..end, color)) } - let start = range - .start - .to_point(buffer) - .to_display_point(display_snapshot); - let end = range - .end - .to_point(buffer) - .to_display_point(display_snapshot); - results.push((start..end, color)) } - results } @@ -7624,15 +7803,17 @@ impl Editor { display_snapshot: &DisplaySnapshot, count: usize, ) -> Vec> { + let search_range = display_snapshot.anchor_to_inlay_offset(search_range.start) + ..display_snapshot.anchor_to_inlay_offset(search_range.end); let mut results = Vec::new(); - let buffer = &display_snapshot.buffer_snapshot; - let Some((_, ranges)) = self.background_highlights - .get(&TypeId::of::()) else { - return vec![]; - }; + let Some((_, ranges)) = self.background_highlights.get(&TypeId::of::()) else { + return vec![]; + }; let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&search_range.start, buffer); + let cmp = document_to_inlay_range(probe, display_snapshot) + .end + .cmp(&search_range.start); if cmp.is_gt() { Ordering::Greater } else { @@ -7652,19 +7833,24 @@ impl Editor { let mut start_row: Option = None; let mut end_row: Option = None; if ranges.len() > count { - return vec![]; + return Vec::new(); } for range in &ranges[start_ix..] { - if range.start.cmp(&search_range.end, buffer).is_ge() { + let range = document_to_inlay_range(range, display_snapshot); + if range.start.cmp(&search_range.end).is_ge() { break; } - let end = range.end.to_point(buffer); + let end = display_snapshot + .inlay_offset_to_display_point(range.end, Bias::Right) + .to_point(display_snapshot); if let Some(current_row) = &end_row { if end.row == current_row.row { continue; } } - let start = range.start.to_point(buffer); + let start = display_snapshot + .inlay_offset_to_display_point(range.start, Bias::Left) + .to_point(display_snapshot); if start_row.is_none() { assert_eq!(end_row, None); @@ -7702,24 +7888,32 @@ impl Editor { cx.notify(); } + pub fn highlight_inlays( + &mut self, + ranges: Vec, + style: HighlightStyle, + cx: &mut ViewContext, + ) { + self.display_map.update(cx, |map, _| { + map.highlight_inlays(TypeId::of::(), ranges, style) + }); + cx.notify(); + } + pub fn text_highlights<'a, T: 'static>( &'a self, cx: &'a AppContext, - ) -> Option<(HighlightStyle, &'a [Range])> { + ) -> Option<(HighlightStyle, &'a [DocumentRange])> { self.display_map.read(cx).text_highlights(TypeId::of::()) } - pub fn clear_text_highlights( - &mut self, - cx: &mut ViewContext, - ) -> Option>)>> { - let highlights = self + pub fn clear_text_highlights(&mut self, cx: &mut ViewContext) { + let text_highlights = self .display_map .update(cx, |map, _| map.clear_text_highlights(TypeId::of::())); - if highlights.is_some() { + if text_highlights.is_some() { cx.notify(); } - highlights } pub fn show_local_cursors(&self, cx: &AppContext) -> bool { @@ -7737,7 +7931,9 @@ impl Editor { cx: &mut ViewContext, ) { match event { - multi_buffer::Event::Edited => { + multi_buffer::Event::Edited { + sigleton_buffer_edited, + } => { self.refresh_active_diagnostics(cx); self.refresh_code_actions(cx); if self.has_active_copilot_suggestion(cx) { @@ -7745,30 +7941,32 @@ impl Editor { } cx.emit(Event::BufferEdited); - 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_inlay_hints( - InlayHintRefreshReason::BufferEdited(languages_affected), - cx, - ); + if *sigleton_buffer_edited { + 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_inlay_hints( + InlayHintRefreshReason::BufferEdited(languages_affected), + cx, + ); + } } } } @@ -7776,12 +7974,16 @@ impl Editor { buffer, predecessor, excerpts, - } => cx.emit(Event::ExcerptsAdded { - buffer: buffer.clone(), - predecessor: *predecessor, - excerpts: excerpts.clone(), - }), + } => { + cx.emit(Event::ExcerptsAdded { + buffer: buffer.clone(), + predecessor: *predecessor, + excerpts: excerpts.clone(), + }); + self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); + } multi_buffer::Event::ExcerptsRemoved { ids } => { + self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx); cx.emit(Event::ExcerptsRemoved { ids: ids.clone() }) } multi_buffer::Event::Reparsed => cx.emit(Event::Reparsed), @@ -7926,6 +8128,7 @@ impl Editor { Some( ranges .iter() + .filter_map(|range| range.as_text_range()) .map(move |range| { range.start.to_offset_utf16(&snapshot)..range.end.to_offset_utf16(&snapshot) }) @@ -7964,9 +8167,7 @@ impl Editor { suggestion_accepted: bool, cx: &AppContext, ) { - let Some(project) = &self.project else { - return - }; + let Some(project) = &self.project else { return }; // If None, we are either getting suggestions in a new, unsaved file, or in a file without an extension let file_extension = self @@ -7995,9 +8196,7 @@ impl Editor { file_extension: Option, cx: &AppContext, ) { - let Some(project) = &self.project else { - return - }; + let Some(project) = &self.project else { return }; // If None, we are in a file without an extension let file = self @@ -8098,7 +8297,9 @@ impl Editor { } } - let Some(lines) = serde_json::to_string_pretty(&lines).log_err() else { return; }; + let Some(lines) = serde_json::to_string_pretty(&lines).log_err() else { + return; + }; cx.write_to_clipboard(ClipboardItem::new(lines)); } @@ -8107,6 +8308,19 @@ impl Editor { } } +fn document_to_inlay_range( + range: &DocumentRange, + snapshot: &DisplaySnapshot, +) -> Range { + match range { + DocumentRange::Text(text_range) => { + snapshot.anchor_to_inlay_offset(text_range.start) + ..snapshot.anchor_to_inlay_offset(text_range.end) + } + DocumentRange::Inlay(inlay_range) => inlay_range.highlight_start..inlay_range.highlight_end, + } +} + fn inlay_hint_settings( location: Anchor, snapshot: &MultiBufferSnapshot, @@ -8291,14 +8505,11 @@ impl View for Editor { ) -> bool { let pending_selection = self.has_pending_selection(); - if let Some(point) = self.link_go_to_definition_state.last_mouse_location.clone() { + if let Some(point) = &self.link_go_to_definition_state.last_trigger_point { if event.cmd && !pending_selection { + let point = point.clone(); let snapshot = self.snapshot(cx); - let kind = if event.shift { - LinkDefinitionKind::Type - } else { - LinkDefinitionKind::Symbol - }; + let kind = point.definition_kind(event.shift); show_link_definition(kind, self, point, snapshot, cx); return false; @@ -8382,6 +8593,7 @@ impl View for Editor { fn marked_text_range(&self, cx: &AppContext) -> Option> { let snapshot = self.buffer.read(cx).read(cx); let range = self.text_highlights::(cx)?.1.get(0)?; + let range = range.as_text_range()?; Some(range.start.to_offset_utf16(&snapshot).0..range.end.to_offset_utf16(&snapshot).0) } diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index f4499b5651cc158c66df3faad7f0ecf707e01bb6..b06f23429a15b17368c8da23a755ad2fe3c637c5 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -9,6 +9,7 @@ pub struct EditorSettings { pub show_completions_on_input: bool, pub use_on_type_format: bool, pub scrollbar: Scrollbar, + pub relative_line_numbers: bool, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] @@ -34,6 +35,7 @@ pub struct EditorSettingsContent { pub show_completions_on_input: Option, pub use_on_type_format: Option, pub scrollbar: Option, + pub relative_line_numbers: Option, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index e031edf538db18be53b79af0eb80ec820f499fa0..fbc8a0b23543e80716aa9986c3a15a1d9e4acad7 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -42,7 +42,7 @@ fn test_edit_events(cx: &mut TestAppContext) { init_test(cx, |_| {}); let buffer = cx.add_model(|cx| { - let mut buffer = language::Buffer::new(0, "123456", cx); + let mut buffer = language::Buffer::new(0, cx.model_id() as u64, "123456"); buffer.set_group_interval(Duration::from_secs(1)); buffer }); @@ -174,7 +174,7 @@ fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) { init_test(cx, |_| {}); let mut now = Instant::now(); - let buffer = cx.add_model(|cx| language::Buffer::new(0, "123456", cx)); + let buffer = cx.add_model(|cx| language::Buffer::new(0, cx.model_id() as u64, "123456")); let group_interval = buffer.read_with(cx, |buffer, _| buffer.transaction_group_interval()); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let editor = cx @@ -247,7 +247,7 @@ fn test_ime_composition(cx: &mut TestAppContext) { init_test(cx, |_| {}); let buffer = cx.add_model(|cx| { - let mut buffer = language::Buffer::new(0, "abcde", cx); + let mut buffer = language::Buffer::new(0, cx.model_id() as u64, "abcde"); // Ensure automatic grouping doesn't occur. buffer.set_group_interval(Duration::ZERO); buffer @@ -1434,6 +1434,74 @@ async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) { }); } +#[gpui::test] +async fn test_autoscroll(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + + let line_height = cx.update_editor(|editor, cx| { + editor.set_vertical_scroll_margin(2, cx); + editor.style(cx).text.line_height(cx.font_cache()) + }); + + let window = cx.window; + window.simulate_resize(vec2f(1000., 6.0 * line_height), &mut cx); + + cx.set_state( + &r#"ˇone + two + three + four + five + six + seven + eight + nine + ten + "#, + ); + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.0)); + }); + + // Add a cursor below the visible area. Since both cursors cannot fit + // on screen, the editor autoscrolls to reveal the newest cursor, and + // allows the vertical scroll margin below that cursor. + cx.update_editor(|editor, cx| { + editor.change_selections(Some(Autoscroll::fit()), cx, |selections| { + selections.select_ranges([ + Point::new(0, 0)..Point::new(0, 0), + Point::new(6, 0)..Point::new(6, 0), + ]); + }) + }); + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.0)); + }); + + // Move down. The editor cursor scrolls down to track the newest cursor. + cx.update_editor(|editor, cx| { + editor.move_down(&Default::default(), cx); + }); + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 4.0)); + }); + + // Add a cursor above the visible area. Since both cursors fit on screen, + // the editor scrolls to show both. + cx.update_editor(|editor, cx| { + editor.change_selections(Some(Autoscroll::fit()), cx, |selections| { + selections.select_ranges([ + Point::new(1, 0)..Point::new(1, 0), + Point::new(6, 0)..Point::new(6, 0), + ]); + }) + }); + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 1.0)); + }); +} + #[gpui::test] async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); @@ -2213,10 +2281,12 @@ fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) { None, )); - let toml_buffer = - cx.add_model(|cx| Buffer::new(0, "a = 1\nb = 2\n", cx).with_language(toml_language, cx)); + let toml_buffer = cx.add_model(|cx| { + Buffer::new(0, cx.model_id() as u64, "a = 1\nb = 2\n").with_language(toml_language, cx) + }); let rust_buffer = cx.add_model(|cx| { - Buffer::new(0, "const c: usize = 3;\n", cx).with_language(rust_language, cx) + Buffer::new(0, cx.model_id() as u64, "const c: usize = 3;\n") + .with_language(rust_language, cx) }); let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); @@ -3686,7 +3756,8 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { "# .unindent(); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) @@ -3849,7 +3920,8 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) { let text = "fn a() {}"; - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); editor @@ -4412,7 +4484,8 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { "# .unindent(); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) @@ -4560,7 +4633,8 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) { "# .unindent(); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); editor @@ -5766,7 +5840,7 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) { fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a'))); let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); multibuffer.push_excerpts( @@ -5850,7 +5924,7 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { primary: None, } }); - let buffer = cx.add_model(|cx| Buffer::new(0, initial_text, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, initial_text)); let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); multibuffer.push_excerpts(buffer, excerpt_ranges, cx); @@ -5908,7 +5982,7 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { fn test_refresh_selections(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a'))); let mut excerpt1_id = None; let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); @@ -5995,7 +6069,7 @@ fn test_refresh_selections(cx: &mut TestAppContext) { fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a'))); let mut excerpt1_id = None; let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); @@ -6092,7 +6166,8 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { "{{} }\n", // ); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) @@ -6384,7 +6459,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { .update(|cx| { Editor::from_state_proto( pane.clone(), - project.clone(), + workspace.clone(), ViewId { creator: Default::default(), id: 0, @@ -6479,7 +6554,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { .update(|cx| { Editor::from_state_proto( pane.clone(), - project.clone(), + workspace.clone(), ViewId { creator: Default::default(), id: 0, @@ -7092,8 +7167,8 @@ async fn test_copilot_multibuffer( let (copilot, copilot_lsp) = Copilot::fake(cx); cx.update(|cx| cx.set_global(copilot)); - let buffer_1 = cx.add_model(|cx| Buffer::new(0, "a = 1\nb = 2\n", cx)); - let buffer_2 = cx.add_model(|cx| Buffer::new(0, "c = 3\nd = 4\n", cx)); + let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "a = 1\nb = 2\n")); + let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "c = 3\nd = 4\n")); let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); multibuffer.push_excerpts( diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 0f26e5819ca48e9d68988200f778fa361ddd9590..62f4c8c8065e8eb24ef24e7b1c98e75168034f43 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -13,6 +13,7 @@ use crate::{ }, link_go_to_definition::{ go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link, + update_inlay_link_and_hover_points, GoToDefinitionTrigger, }, mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt, }; @@ -62,6 +63,7 @@ struct SelectionLayout { head: DisplayPoint, cursor_shape: CursorShape, is_newest: bool, + is_local: bool, range: Range, active_rows: Range, } @@ -73,6 +75,7 @@ impl SelectionLayout { cursor_shape: CursorShape, map: &DisplaySnapshot, is_newest: bool, + is_local: bool, ) -> Self { let point_selection = selection.map(|p| p.to_point(&map.buffer_snapshot)); let display_selection = point_selection.map(|p| p.to_display_point(map)); @@ -109,6 +112,7 @@ impl SelectionLayout { head, cursor_shape, is_newest, + is_local, range, active_rows, } @@ -284,13 +288,13 @@ impl EditorElement { return false; } - let (position, target_position) = position_map.point_for_position(text_bounds, position); - + let point_for_position = position_map.point_for_position(text_bounds, position); + let position = point_for_position.previous_valid; if shift && alt { editor.select( SelectPhase::BeginColumnar { position, - goal_column: target_position.column(), + goal_column: point_for_position.exact_unclipped.column(), }, cx, ); @@ -326,9 +330,13 @@ impl EditorElement { if !text_bounds.contains_point(position) { return false; } - - let (point, _) = position_map.point_for_position(text_bounds, position); - mouse_context_menu::deploy_context_menu(editor, position, point, cx); + let point_for_position = position_map.point_for_position(text_bounds, position); + mouse_context_menu::deploy_context_menu( + editor, + position, + point_for_position.previous_valid, + cx, + ); true } @@ -350,17 +358,15 @@ impl EditorElement { } if !pending_nonempty_selections && cmd && text_bounds.contains_point(position) { - let (point, target_point) = position_map.point_for_position(text_bounds, position); - - if point == target_point { - if shift { - go_to_fetched_type_definition(editor, point, alt, cx); - } else { - go_to_fetched_definition(editor, point, alt, cx); - } - - return true; + let point = position_map.point_for_position(text_bounds, position); + let could_be_inlay = point.as_valid().is_none(); + if shift || could_be_inlay { + go_to_fetched_type_definition(editor, point, alt, cx); + } else { + go_to_fetched_definition(editor, point, alt, cx); } + + return true; } end_selection @@ -380,17 +386,20 @@ impl EditorElement { // This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed // Don't trigger hover popover if mouse is hovering over context menu let point = if text_bounds.contains_point(position) { - let (point, target_point) = position_map.point_for_position(text_bounds, position); - if point == target_point { - Some(point) - } else { - None - } + position_map + .point_for_position(text_bounds, position) + .as_valid() } else { None }; - update_go_to_definition_link(editor, point, cmd, shift, cx); + update_go_to_definition_link( + editor, + point.map(GoToDefinitionTrigger::Text), + cmd, + shift, + cx, + ); if editor.has_pending_selection() { let mut scroll_delta = Vector2F::zero(); @@ -419,13 +428,12 @@ impl EditorElement { )) } - let (position, target_position) = - position_map.point_for_position(text_bounds, position); + let point_for_position = position_map.point_for_position(text_bounds, position); editor.select( SelectPhase::Update { - position, - goal_column: target_position.column(), + position: point_for_position.previous_valid, + goal_column: point_for_position.exact_unclipped.column(), scroll_position: (position_map.snapshot.scroll_position() + scroll_delta) .clamp(Vector2F::zero(), position_map.scroll_max), }, @@ -452,10 +460,34 @@ impl EditorElement { ) -> bool { // This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed // Don't trigger hover popover if mouse is hovering over context menu - let point = position_to_display_point(position, text_bounds, position_map); - - update_go_to_definition_link(editor, point, cmd, shift, cx); - hover_at(editor, point, cx); + if text_bounds.contains_point(position) { + let point_for_position = position_map.point_for_position(text_bounds, position); + match point_for_position.as_valid() { + Some(point) => { + update_go_to_definition_link( + editor, + Some(GoToDefinitionTrigger::Text(point)), + cmd, + shift, + cx, + ); + hover_at(editor, Some(point), cx); + } + None => { + update_inlay_link_and_hover_points( + &position_map.snapshot, + point_for_position, + editor, + cmd, + shift, + cx, + ); + } + } + } else { + update_go_to_definition_link(editor, None, cmd, shift, cx); + hover_at(editor, None, cx); + } true } @@ -763,7 +795,6 @@ impl EditorElement { cx: &mut PaintContext, ) { let style = &self.style; - let local_replica_id = editor.replica_id(cx); let scroll_position = layout.position_map.snapshot.scroll_position(); let start_row = layout.visible_display_row_range.start; let scroll_top = scroll_position.y() * layout.position_map.line_height; @@ -852,15 +883,13 @@ impl EditorElement { for (replica_id, selections) in &layout.selections { let replica_id = *replica_id; - let selection_style = style.replica_selection_style(replica_id); + let selection_style = if let Some(replica_id) = replica_id { + style.replica_selection_style(replica_id) + } else { + &style.absent_selection + }; for selection in selections { - if !selection.range.is_empty() - && (replica_id == local_replica_id - || Some(replica_id) == editor.leader_replica_id) - { - invisible_display_ranges.push(selection.range.clone()); - } self.paint_highlighted_range( scene, selection.range.clone(), @@ -874,7 +903,10 @@ impl EditorElement { bounds, ); - if editor.show_local_cursors(cx) || replica_id != local_replica_id { + if selection.is_local && !selection.range.is_empty() { + invisible_display_ranges.push(selection.range.clone()); + } + if !selection.is_local || editor.show_local_cursors(cx) { let cursor_position = selection.head; if layout .visible_display_row_range @@ -906,7 +938,7 @@ impl EditorElement { &text, cursor_row_layout.font_size(), &[( - text.len(), + text.chars().count(), RunStyle { font_id, color: style.background, @@ -1405,10 +1437,61 @@ impl EditorElement { .collect() } + fn calculate_relative_line_numbers( + &self, + snapshot: &EditorSnapshot, + rows: &Range, + relative_to: Option, + ) -> HashMap { + let mut relative_rows: HashMap = Default::default(); + let Some(relative_to) = relative_to else { + return relative_rows; + }; + + let start = rows.start.min(relative_to); + let end = rows.end.max(relative_to); + + let buffer_rows = snapshot + .buffer_rows(start) + .take(1 + (end - start) as usize) + .collect::>(); + + let head_idx = relative_to - start; + let mut delta = 1; + let mut i = head_idx + 1; + while i < buffer_rows.len() as u32 { + if buffer_rows[i as usize].is_some() { + if rows.contains(&(i + start)) { + relative_rows.insert(i + start, delta); + } + delta += 1; + } + i += 1; + } + delta = 1; + i = head_idx.min(buffer_rows.len() as u32 - 1); + while i > 0 && buffer_rows[i as usize].is_none() { + i -= 1; + } + + while i > 0 { + i -= 1; + if buffer_rows[i as usize].is_some() { + if rows.contains(&(i + start)) { + relative_rows.insert(i + start, delta); + } + delta += 1; + } + } + + relative_rows + } + fn layout_line_numbers( &self, rows: Range, active_rows: &BTreeMap, + newest_selection_head: DisplayPoint, is_singleton: bool, snapshot: &EditorSnapshot, cx: &ViewContext, @@ -1421,6 +1504,15 @@ impl EditorElement { let mut line_number_layouts = Vec::with_capacity(rows.len()); let mut fold_statuses = Vec::with_capacity(rows.len()); let mut line_number = String::new(); + let is_relative = settings::get::(cx).relative_line_numbers; + let relative_to = if is_relative { + Some(newest_selection_head.row()) + } else { + None + }; + + let relative_rows = self.calculate_relative_line_numbers(&snapshot, &rows, relative_to); + for (ix, row) in snapshot .buffer_rows(rows.start) .take((rows.end - rows.start) as usize) @@ -1435,7 +1527,11 @@ impl EditorElement { if let Some(buffer_row) = row { if include_line_numbers { line_number.clear(); - write!(&mut line_number, "{}", buffer_row + 1).unwrap(); + let default_number = buffer_row + 1; + let number = relative_rows + .get(&(ix as u32 + rows.start)) + .unwrap_or(&default_number); + write!(&mut line_number, "{}", number).unwrap(); line_number_layouts.push(Some(cx.text_layout_cache().layout_str( &line_number, style.text.font_size, @@ -2079,14 +2175,11 @@ impl Element for EditorElement { scroll_height .min(constraint.max_along(Axis::Vertical)) .max(constraint.min_along(Axis::Vertical)) + .max(line_height) .min(line_height * max_lines as f32), ) } else if let EditorMode::SingleLine = snapshot.mode { - size.set_y( - line_height - .min(constraint.max_along(Axis::Vertical)) - .max(constraint.min_along(Axis::Vertical)), - ) + size.set_y(line_height.max(constraint.min_along(Axis::Vertical))) } else if size.y().is_infinite() { size.set_y(scroll_height); } @@ -2124,7 +2217,7 @@ impl Element for EditorElement { .anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right)) }; - let mut selections: Vec<(ReplicaId, Vec)> = Vec::new(); + let mut selections: Vec<(Option, Vec)> = Vec::new(); let mut active_rows = BTreeMap::new(); let mut fold_ranges = Vec::new(); let is_singleton = editor.is_singleton(cx); @@ -2155,8 +2248,14 @@ impl Element for EditorElement { .buffer_snapshot .remote_selections_in_range(&(start_anchor..end_anchor)) { + let replica_id = if let Some(mapping) = &editor.replica_id_mapping { + mapping.get(&replica_id).copied() + } else { + None + }; + // The local selections match the leader's selections. - if Some(replica_id) == editor.leader_replica_id { + if replica_id.is_some() && replica_id == editor.leader_replica_id { continue; } remote_selections @@ -2168,6 +2267,7 @@ impl Element for EditorElement { cursor_shape, &snapshot.display_snapshot, false, + false, )); } selections.extend(remote_selections); @@ -2191,6 +2291,7 @@ impl Element for EditorElement { editor.cursor_shape, &snapshot.display_snapshot, is_newest, + true, ); if is_newest { newest_selection_head = Some(layout.head); @@ -2206,11 +2307,18 @@ impl Element for EditorElement { } // Render the local selections in the leader's color when following. - let local_replica_id = editor - .leader_replica_id - .unwrap_or_else(|| editor.replica_id(cx)); + let local_replica_id = if let Some(leader_replica_id) = editor.leader_replica_id { + leader_replica_id + } else { + let replica_id = editor.replica_id(cx); + if let Some(mapping) = &editor.replica_id_mapping { + mapping.get(&replica_id).copied().unwrap_or(replica_id) + } else { + replica_id + } + }; - selections.push((local_replica_id, layouts)); + selections.push((Some(local_replica_id), layouts)); } let scrollbar_settings = &settings::get::(cx).scrollbar; @@ -2244,9 +2352,23 @@ impl Element for EditorElement { }) .collect(); + let head_for_relative = newest_selection_head.unwrap_or_else(|| { + let newest = editor.selections.newest::(cx); + SelectionLayout::new( + newest, + editor.selections.line_mode, + editor.cursor_shape, + &snapshot.display_snapshot, + true, + true, + ) + .head + }); + let (line_number_layouts, fold_statuses) = self.layout_line_numbers( start_row..end_row, &active_rows, + head_for_relative, is_singleton, &snapshot, cx, @@ -2591,7 +2713,7 @@ pub struct LayoutState { blocks: Vec, highlighted_ranges: Vec<(Range, Color)>, fold_ranges: Vec<(BufferRow, Range, Color)>, - selections: Vec<(ReplicaId, Vec)>, + selections: Vec<(Option, Vec)>, scrollbar_row_range: Range, show_scrollbars: bool, is_singleton: bool, @@ -2614,22 +2736,42 @@ struct PositionMap { snapshot: EditorSnapshot, } +#[derive(Debug, Copy, Clone)] +pub struct PointForPosition { + pub previous_valid: DisplayPoint, + pub next_valid: DisplayPoint, + pub exact_unclipped: DisplayPoint, + pub column_overshoot_after_line_end: u32, +} + +impl PointForPosition { + #[cfg(test)] + pub fn valid(valid: DisplayPoint) -> Self { + Self { + previous_valid: valid, + next_valid: valid, + exact_unclipped: valid, + column_overshoot_after_line_end: 0, + } + } + + pub fn as_valid(&self) -> Option { + if self.previous_valid == self.exact_unclipped && self.next_valid == self.exact_unclipped { + Some(self.previous_valid) + } else { + None + } + } +} + impl PositionMap { - /// Returns two display points: - /// 1. The nearest *valid* position in the editor - /// 2. An unclipped, potentially *invalid* position that maps directly to - /// the given pixel position. - fn point_for_position( - &self, - text_bounds: RectF, - position: Vector2F, - ) -> (DisplayPoint, DisplayPoint) { + fn point_for_position(&self, text_bounds: RectF, position: Vector2F) -> PointForPosition { let scroll_position = self.snapshot.scroll_position(); let position = position - text_bounds.origin(); let y = position.y().max(0.0).min(self.size.y()); let x = position.x() + (scroll_position.x() * self.em_width); let row = (y / self.line_height + scroll_position.y()) as u32; - let (column, x_overshoot) = if let Some(line) = self + let (column, x_overshoot_after_line_end) = if let Some(line) = self .line_layouts .get(row as usize - scroll_position.y() as usize) .map(|line_with_spaces| &line_with_spaces.line) @@ -2643,11 +2785,18 @@ impl PositionMap { (0, x) }; - let mut target_point = DisplayPoint::new(row, column); - let point = self.snapshot.clip_point(target_point, Bias::Left); - *target_point.column_mut() += (x_overshoot / self.em_advance) as u32; - - (point, target_point) + let mut exact_unclipped = DisplayPoint::new(row, column); + let previous_valid = self.snapshot.clip_point(exact_unclipped, Bias::Left); + let next_valid = self.snapshot.clip_point(exact_unclipped, Bias::Right); + + let column_overshoot_after_line_end = (x_overshoot_after_line_end / self.em_advance) as u32; + *exact_unclipped.column_mut() += column_overshoot_after_line_end; + PointForPosition { + previous_valid, + next_valid, + exact_unclipped, + column_overshoot_after_line_end, + } } } @@ -2901,23 +3050,6 @@ impl HighlightedRange { } } -fn position_to_display_point( - position: Vector2F, - text_bounds: RectF, - position_map: &PositionMap, -) -> Option { - if text_bounds.contains_point(position) { - let (point, target_point) = position_map.point_for_position(text_bounds, position); - if point == target_point { - Some(point) - } else { - None - } - } else { - None - } -} - fn range_to_bounds( range: &Range, content_origin: Vector2F, @@ -2995,7 +3127,6 @@ mod tests { #[gpui::test] fn test_layout_line_numbers(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let editor = cx .add_window(|cx| { let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx); @@ -3007,10 +3138,50 @@ mod tests { let layouts = editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); element - .layout_line_numbers(0..6, &Default::default(), false, &snapshot, cx) + .layout_line_numbers( + 0..6, + &Default::default(), + DisplayPoint::new(0, 0), + false, + &snapshot, + cx, + ) .0 }); assert_eq!(layouts.len(), 6); + + let relative_rows = editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + element.calculate_relative_line_numbers(&snapshot, &(0..6), Some(3)) + }); + assert_eq!(relative_rows[&0], 3); + assert_eq!(relative_rows[&1], 2); + assert_eq!(relative_rows[&2], 1); + // current line has no relative number + assert_eq!(relative_rows[&4], 1); + assert_eq!(relative_rows[&5], 2); + + // works if cursor is before screen + let relative_rows = editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + + element.calculate_relative_line_numbers(&snapshot, &(3..6), Some(1)) + }); + assert_eq!(relative_rows.len(), 3); + assert_eq!(relative_rows[&3], 2); + assert_eq!(relative_rows[&4], 3); + assert_eq!(relative_rows[&5], 4); + + // works if cursor is after screen + let relative_rows = editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + + element.calculate_relative_line_numbers(&snapshot, &(0..3), Some(6)) + }); + assert_eq!(relative_rows.len(), 3); + assert_eq!(relative_rows[&0], 5); + assert_eq!(relative_rows[&1], 4); + assert_eq!(relative_rows[&2], 3); } #[gpui::test] diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index e4509a765cb82583188188eebd8a061e48feaf86..2f278ce262f6dd3e0910b022e6956f26c9bdfa6b 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -1,6 +1,8 @@ use crate::{ - display_map::ToDisplayPoint, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, - EditorSnapshot, EditorStyle, RangeToAnchorExt, + display_map::{InlayOffset, ToDisplayPoint}, + link_go_to_definition::{DocumentRange, InlayRange}, + Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle, + ExcerptId, RangeToAnchorExt, }; use futures::FutureExt; use gpui::{ @@ -11,7 +13,7 @@ use gpui::{ AnyElement, AppContext, CursorRegion, Element, ModelHandle, MouseRegion, Task, ViewContext, }; use language::{Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry}; -use project::{HoverBlock, HoverBlockKind, Project}; +use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project}; use std::{ops::Range, sync::Arc, time::Duration}; use util::TryFutureExt; @@ -46,6 +48,106 @@ pub fn hover_at(editor: &mut Editor, point: Option, cx: &mut ViewC } } +pub struct InlayHover { + pub excerpt: ExcerptId, + pub triggered_from: InlayOffset, + pub range: InlayRange, + pub tooltip: HoverBlock, +} + +pub fn find_hovered_hint_part( + label_parts: Vec, + hint_range: Range, + hovered_offset: InlayOffset, +) -> Option<(InlayHintLabelPart, Range)> { + if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end { + let mut hovered_character = (hovered_offset - hint_range.start).0; + let mut part_start = hint_range.start; + for part in label_parts { + let part_len = part.value.chars().count(); + if hovered_character > part_len { + hovered_character -= part_len; + part_start.0 += part_len; + } else { + let part_end = InlayOffset(part_start.0 + part_len); + return Some((part, part_start..part_end)); + } + } + } + None +} + +pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut ViewContext) { + if settings::get::(cx).hover_popover_enabled { + if editor.pending_rename.is_some() { + return; + } + + let Some(project) = editor.project.clone() else { + return; + }; + + if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover { + if let DocumentRange::Inlay(range) = symbol_range { + if (range.highlight_start..range.highlight_end) + .contains(&inlay_hover.triggered_from) + { + // Hover triggered from same location as last time. Don't show again. + return; + } + } + hide_hover(editor, cx); + } + + let snapshot = editor.snapshot(cx); + // Don't request again if the location is the same as the previous request + if let Some(triggered_from) = editor.hover_state.triggered_from { + if inlay_hover.triggered_from + == snapshot + .display_snapshot + .anchor_to_inlay_offset(triggered_from) + { + return; + } + } + + let task = cx.spawn(|this, mut cx| { + async move { + cx.background() + .timer(Duration::from_millis(HOVER_DELAY_MILLIS)) + .await; + this.update(&mut cx, |this, _| { + this.hover_state.diagnostic_popover = None; + })?; + + let hover_popover = InfoPopover { + project: project.clone(), + symbol_range: DocumentRange::Inlay(inlay_hover.range), + blocks: vec![inlay_hover.tooltip], + language: None, + rendered_content: None, + }; + + this.update(&mut cx, |this, cx| { + // Highlight the selected symbol using a background highlight + this.highlight_inlay_background::( + vec![inlay_hover.range], + |theme| theme.editor.hover_popover.highlight, + cx, + ); + this.hover_state.info_popover = Some(hover_popover); + cx.notify(); + })?; + + anyhow::Ok(()) + } + .log_err() + }); + + editor.hover_state.info_task = Some(task); + } +} + /// Hides the type information popup. /// Triggered by the `Hover` action when the cursor is not over a symbol or when the /// selections changed. @@ -110,8 +212,13 @@ fn show_hover( if !ignore_timeout { if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover { if symbol_range - .to_offset(&snapshot.buffer_snapshot) - .contains(&multibuffer_offset) + .as_text_range() + .map(|range| { + range + .to_offset(&snapshot.buffer_snapshot) + .contains(&multibuffer_offset) + }) + .unwrap_or(false) { // Hover triggered from same location as last time. Don't show again. return; @@ -219,7 +326,7 @@ fn show_hover( Some(InfoPopover { project: project.clone(), - symbol_range: range, + symbol_range: DocumentRange::Text(range), blocks: hover_result.contents, language: hover_result.language, rendered_content: None, @@ -227,10 +334,13 @@ fn show_hover( }); this.update(&mut cx, |this, cx| { - if let Some(hover_popover) = hover_popover.as_ref() { + if let Some(symbol_range) = hover_popover + .as_ref() + .and_then(|hover_popover| hover_popover.symbol_range.as_text_range()) + { // Highlight the selected symbol using a background highlight this.highlight_background::( - vec![hover_popover.symbol_range.clone()], + vec![symbol_range], |theme| theme.editor.hover_popover.highlight, cx, ); @@ -497,7 +607,10 @@ impl HoverState { .or_else(|| { self.info_popover .as_ref() - .map(|info_popover| &info_popover.symbol_range.start) + .map(|info_popover| match &info_popover.symbol_range { + DocumentRange::Text(range) => &range.start, + DocumentRange::Inlay(range) => &range.inlay_position, + }) })?; let point = anchor.to_display_point(&snapshot.display_snapshot); @@ -522,7 +635,7 @@ impl HoverState { #[derive(Debug, Clone)] pub struct InfoPopover { pub project: ModelHandle, - pub symbol_range: Range, + symbol_range: DocumentRange, pub blocks: Vec, language: Option>, rendered_content: Option, @@ -692,10 +805,17 @@ impl DiagnosticPopover { #[cfg(test)] mod tests { use super::*; - use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext}; + use crate::{ + editor_tests::init_test, + element::PointForPosition, + inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels}, + link_go_to_definition::update_inlay_link_and_hover_points, + test::editor_lsp_test_context::EditorLspTestContext, + }; + use collections::BTreeSet; use gpui::fonts::Weight; use indoc::indoc; - use language::{Diagnostic, DiagnosticSet}; + use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet}; use lsp::LanguageServerId; use project::{HoverBlock, HoverBlockKind}; use smol::stream::StreamExt; @@ -1131,4 +1251,327 @@ mod tests { editor }); } + + #[gpui::test] + async fn test_hover_inlay_label_parts(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 mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Right( + lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions { + resolve_provider: Some(true), + ..Default::default() + }), + )), + ..Default::default() + }, + cx, + ) + .await; + + cx.set_state(indoc! {" + struct TestStruct; + + // ================== + + struct TestNewType(T); + + fn main() { + let variableˇ = TestNewType(TestStruct); + } + "}); + + let hint_start_offset = cx.ranges(indoc! {" + struct TestStruct; + + // ================== + + struct TestNewType(T); + + fn main() { + let variableˇ = TestNewType(TestStruct); + } + "})[0] + .start; + let hint_position = cx.to_lsp(hint_start_offset); + let new_type_target_range = cx.lsp_range(indoc! {" + struct TestStruct; + + // ================== + + struct «TestNewType»(T); + + fn main() { + let variable = TestNewType(TestStruct); + } + "}); + let struct_target_range = cx.lsp_range(indoc! {" + struct «TestStruct»; + + // ================== + + struct TestNewType(T); + + fn main() { + let variable = TestNewType(TestStruct); + } + "}); + + let uri = cx.buffer_lsp_url.clone(); + let new_type_label = "TestNewType"; + let struct_label = "TestStruct"; + let entire_hint_label = ": TestNewType"; + let closure_uri = uri.clone(); + cx.lsp + .handle_request::(move |params, _| { + let task_uri = closure_uri.clone(); + async move { + assert_eq!(params.text_document.uri, task_uri); + Ok(Some(vec![lsp::InlayHint { + position: hint_position, + label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart { + value: entire_hint_label.to_string(), + ..Default::default() + }]), + kind: Some(lsp::InlayHintKind::TYPE), + text_edits: None, + tooltip: None, + padding_left: Some(false), + padding_right: Some(false), + data: None, + }])) + } + }) + .next() + .await; + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| { + let expected_layers = vec![entire_hint_label.to_string()]; + assert_eq!(expected_layers, cached_hint_labels(editor)); + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + }); + + let inlay_range = cx + .ranges(indoc! {" + struct TestStruct; + + // ================== + + struct TestNewType(T); + + fn main() { + let variable« »= TestNewType(TestStruct); + } + "}) + .get(0) + .cloned() + .unwrap(); + let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let previous_valid = inlay_range.start.to_display_point(&snapshot); + let next_valid = inlay_range.end.to_display_point(&snapshot); + assert_eq!(previous_valid.row(), next_valid.row()); + assert!(previous_valid.column() < next_valid.column()); + let exact_unclipped = DisplayPoint::new( + previous_valid.row(), + previous_valid.column() + + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2) + as u32, + ); + PointForPosition { + previous_valid, + next_valid, + exact_unclipped, + column_overshoot_after_line_end: 0, + } + }); + cx.update_editor(|editor, cx| { + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + new_type_hint_part_hover_position, + editor, + true, + false, + cx, + ); + }); + + let resolve_closure_uri = uri.clone(); + cx.lsp + .handle_request::( + move |mut hint_to_resolve, _| { + let mut resolved_hint_positions = BTreeSet::new(); + let task_uri = resolve_closure_uri.clone(); + async move { + let inserted = resolved_hint_positions.insert(hint_to_resolve.position); + assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice"); + + // `: TestNewType` + hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![ + lsp::InlayHintLabelPart { + value: ": ".to_string(), + ..Default::default() + }, + lsp::InlayHintLabelPart { + value: new_type_label.to_string(), + location: Some(lsp::Location { + uri: task_uri.clone(), + range: new_type_target_range, + }), + tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!( + "A tooltip for `{new_type_label}`" + ))), + ..Default::default() + }, + lsp::InlayHintLabelPart { + value: "<".to_string(), + ..Default::default() + }, + lsp::InlayHintLabelPart { + value: struct_label.to_string(), + location: Some(lsp::Location { + uri: task_uri, + range: struct_target_range, + }), + tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent( + lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: format!("A tooltip for `{struct_label}`"), + }, + )), + ..Default::default() + }, + lsp::InlayHintLabelPart { + value: ">".to_string(), + ..Default::default() + }, + ]); + + Ok(hint_to_resolve) + } + }, + ) + .next() + .await; + cx.foreground().run_until_parked(); + + cx.update_editor(|editor, cx| { + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + new_type_hint_part_hover_position, + editor, + true, + false, + cx, + ); + }); + cx.foreground() + .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let hover_state = &editor.hover_state; + assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); + let popover = hover_state.info_popover.as_ref().unwrap(); + let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); + let entire_inlay_start = snapshot.display_point_to_inlay_offset( + inlay_range.start.to_display_point(&snapshot), + Bias::Left, + ); + + let expected_new_type_label_start = InlayOffset(entire_inlay_start.0 + ": ".len()); + assert_eq!( + popover.symbol_range, + DocumentRange::Inlay(InlayRange { + inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), + highlight_start: expected_new_type_label_start, + highlight_end: InlayOffset( + expected_new_type_label_start.0 + new_type_label.len() + ), + }), + "Popover range should match the new type label part" + ); + assert_eq!( + popover + .rendered_content + .as_ref() + .expect("should have label text for new type hint") + .text, + format!("A tooltip for `{new_type_label}`"), + "Rendered text should not anyhow alter backticks" + ); + }); + + let struct_hint_part_hover_position = cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let previous_valid = inlay_range.start.to_display_point(&snapshot); + let next_valid = inlay_range.end.to_display_point(&snapshot); + assert_eq!(previous_valid.row(), next_valid.row()); + assert!(previous_valid.column() < next_valid.column()); + let exact_unclipped = DisplayPoint::new( + previous_valid.row(), + previous_valid.column() + + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2) + as u32, + ); + PointForPosition { + previous_valid, + next_valid, + exact_unclipped, + column_overshoot_after_line_end: 0, + } + }); + cx.update_editor(|editor, cx| { + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + struct_hint_part_hover_position, + editor, + true, + false, + cx, + ); + }); + cx.foreground() + .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let hover_state = &editor.hover_state; + assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); + let popover = hover_state.info_popover.as_ref().unwrap(); + let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); + let entire_inlay_start = snapshot.display_point_to_inlay_offset( + inlay_range.start.to_display_point(&snapshot), + Bias::Left, + ); + let expected_struct_label_start = + InlayOffset(entire_inlay_start.0 + ": ".len() + new_type_label.len() + "<".len()); + assert_eq!( + popover.symbol_range, + DocumentRange::Inlay(InlayRange { + inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), + highlight_start: expected_struct_label_start, + highlight_end: InlayOffset(expected_struct_label_start.0 + struct_label.len()), + }), + "Popover range should match the struct label part" + ); + assert_eq!( + popover + .rendered_content + .as_ref() + .expect("should have label text for struct hint") + .text, + format!("A tooltip for {struct_label}"), + "Rendered markdown element should remove backticks from text" + ); + }); + } } diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 70cccf21da391d9308ece455d0de09768f68f280..34898aea2efe7ec45229cdb4e63de13cd48217f9 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -2,6 +2,7 @@ use std::{ cmp, ops::{ControlFlow, Range}, sync::Arc, + time::Duration, }; use crate::{ @@ -9,15 +10,17 @@ use crate::{ }; use anyhow::Context; use clock::Global; +use futures::future; use gpui::{ModelContext, ModelHandle, Task, ViewContext}; use language::{language_settings::InlayHintKind, Buffer, BufferSnapshot}; -use log::error; use parking_lot::RwLock; -use project::InlayHint; +use project::{InlayHint, ResolveState}; use collections::{hash_map, HashMap, HashSet}; use language::language_settings::InlayHintSettings; +use smol::lock::Semaphore; use sum_tree::Bias; +use text::{ToOffset, ToPoint}; use util::post_inc; pub struct InlayHintCache { @@ -26,6 +29,7 @@ pub struct InlayHintCache { version: usize, pub(super) enabled: bool, update_tasks: HashMap, + lsp_request_limiter: Arc, } #[derive(Debug)] @@ -60,7 +64,7 @@ struct ExcerptHintsUpdate { excerpt_id: ExcerptId, remove_from_visible: Vec, remove_from_cache: HashSet, - add_to_cache: HashSet, + add_to_cache: Vec, } #[derive(Debug, Clone, Copy)] @@ -69,6 +73,7 @@ struct ExcerptQuery { excerpt_id: ExcerptId, cache_version: usize, invalidate: InvalidationStrategy, + reason: &'static str, } impl InvalidationStrategy { @@ -81,7 +86,11 @@ impl InvalidationStrategy { } impl TasksForRanges { - fn new(sorted_ranges: Vec>, task: Task<()>) -> Self { + fn new(query_ranges: QueryRanges, task: Task<()>) -> Self { + let mut sorted_ranges = Vec::new(); + sorted_ranges.extend(query_ranges.before_visible); + sorted_ranges.extend(query_ranges.visible); + sorted_ranges.extend(query_ranges.after_visible); Self { tasks: vec![task], sorted_ranges, @@ -91,82 +100,138 @@ impl TasksForRanges { fn update_cached_tasks( &mut self, buffer_snapshot: &BufferSnapshot, - query_range: Range, + query_ranges: QueryRanges, invalidate: InvalidationStrategy, - spawn_task: impl FnOnce(Vec>) -> Task<()>, + spawn_task: impl FnOnce(QueryRanges) -> Task<()>, ) { - let ranges_to_query = match invalidate { - InvalidationStrategy::None => { - let mut ranges_to_query = Vec::new(); - let mut latest_cached_range = None::<&mut Range>; - for cached_range in self - .sorted_ranges - .iter_mut() - .skip_while(|cached_range| { - cached_range - .end - .cmp(&query_range.start, buffer_snapshot) - .is_lt() - }) - .take_while(|cached_range| { - cached_range - .start - .cmp(&query_range.end, buffer_snapshot) - .is_le() - }) - { - match latest_cached_range { - Some(latest_cached_range) => { - if latest_cached_range.end.offset.saturating_add(1) - < cached_range.start.offset - { - ranges_to_query.push(latest_cached_range.end..cached_range.start); - cached_range.start = latest_cached_range.end; - } - } - None => { - if query_range - .start - .cmp(&cached_range.start, buffer_snapshot) - .is_lt() - { - ranges_to_query.push(query_range.start..cached_range.start); - cached_range.start = query_range.start; - } - } - } - latest_cached_range = Some(cached_range); - } + let query_ranges = if invalidate.should_invalidate() { + self.tasks.clear(); + self.sorted_ranges.clear(); + query_ranges + } else { + let mut non_cached_query_ranges = query_ranges; + non_cached_query_ranges.before_visible = non_cached_query_ranges + .before_visible + .into_iter() + .flat_map(|query_range| { + self.remove_cached_ranges_from_query(buffer_snapshot, query_range) + }) + .collect(); + non_cached_query_ranges.visible = non_cached_query_ranges + .visible + .into_iter() + .flat_map(|query_range| { + self.remove_cached_ranges_from_query(buffer_snapshot, query_range) + }) + .collect(); + non_cached_query_ranges.after_visible = non_cached_query_ranges + .after_visible + .into_iter() + .flat_map(|query_range| { + self.remove_cached_ranges_from_query(buffer_snapshot, query_range) + }) + .collect(); + non_cached_query_ranges + }; - match latest_cached_range { - Some(latest_cached_range) => { - if latest_cached_range.end.offset.saturating_add(1) < query_range.end.offset - { - ranges_to_query.push(latest_cached_range.end..query_range.end); - latest_cached_range.end = query_range.end; - } + if !query_ranges.is_empty() { + self.tasks.push(spawn_task(query_ranges)); + } + } + + fn remove_cached_ranges_from_query( + &mut self, + buffer_snapshot: &BufferSnapshot, + query_range: Range, + ) -> Vec> { + let mut ranges_to_query = Vec::new(); + let mut latest_cached_range = None::<&mut Range>; + for cached_range in self + .sorted_ranges + .iter_mut() + .skip_while(|cached_range| { + cached_range + .end + .cmp(&query_range.start, buffer_snapshot) + .is_lt() + }) + .take_while(|cached_range| { + cached_range + .start + .cmp(&query_range.end, buffer_snapshot) + .is_le() + }) + { + match latest_cached_range { + Some(latest_cached_range) => { + if latest_cached_range.end.offset.saturating_add(1) < cached_range.start.offset + { + ranges_to_query.push(latest_cached_range.end..cached_range.start); + cached_range.start = latest_cached_range.end; } - None => { - ranges_to_query.push(query_range.clone()); - self.sorted_ranges.push(query_range); - self.sorted_ranges.sort_by(|range_a, range_b| { - range_a.start.cmp(&range_b.start, buffer_snapshot) - }); + } + None => { + if query_range + .start + .cmp(&cached_range.start, buffer_snapshot) + .is_lt() + { + ranges_to_query.push(query_range.start..cached_range.start); + cached_range.start = query_range.start; } } + } + latest_cached_range = Some(cached_range); + } - ranges_to_query + match latest_cached_range { + Some(latest_cached_range) => { + if latest_cached_range.end.offset.saturating_add(1) < query_range.end.offset { + ranges_to_query.push(latest_cached_range.end..query_range.end); + latest_cached_range.end = query_range.end; + } } - InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited => { - self.tasks.clear(); - self.sorted_ranges.clear(); - vec![query_range] + None => { + ranges_to_query.push(query_range.clone()); + self.sorted_ranges.push(query_range); + self.sorted_ranges + .sort_by(|range_a, range_b| range_a.start.cmp(&range_b.start, buffer_snapshot)); } - }; - - if !ranges_to_query.is_empty() { - self.tasks.push(spawn_task(ranges_to_query)); } + + ranges_to_query + } + + fn invalidate_range(&mut self, buffer: &BufferSnapshot, range: &Range) { + self.sorted_ranges = self + .sorted_ranges + .drain(..) + .filter_map(|mut cached_range| { + if cached_range.start.cmp(&range.end, buffer).is_gt() + || cached_range.end.cmp(&range.start, buffer).is_lt() + { + Some(vec![cached_range]) + } else if cached_range.start.cmp(&range.start, buffer).is_ge() + && cached_range.end.cmp(&range.end, buffer).is_le() + { + None + } else if range.start.cmp(&cached_range.start, buffer).is_ge() + && range.end.cmp(&cached_range.end, buffer).is_le() + { + Some(vec![ + cached_range.start..range.start, + range.end..cached_range.end, + ]) + } else if cached_range.start.cmp(&range.start, buffer).is_ge() { + cached_range.start = range.end; + Some(vec![cached_range]) + } else { + cached_range.end = range.start; + Some(vec![cached_range]) + } + }) + .flatten() + .collect(); } } @@ -178,6 +243,7 @@ impl InlayHintCache { hints: HashMap::default(), update_tasks: HashMap::default(), version: 0, + lsp_request_limiter: Arc::new(Semaphore::new(MAX_CONCURRENT_LSP_REQUESTS)), } } @@ -234,6 +300,7 @@ impl InlayHintCache { pub fn spawn_hint_refresh( &mut self, + reason: &'static str, excerpts_to_query: HashMap, Global, Range)>, invalidate: InvalidationStrategy, cx: &mut ViewContext, @@ -262,7 +329,14 @@ impl InlayHintCache { cx.spawn(|editor, mut cx| async move { editor .update(&mut cx, |editor, cx| { - spawn_new_update_tasks(editor, excerpts_to_query, invalidate, cache_version, cx) + spawn_new_update_tasks( + editor, + reason, + excerpts_to_query, + invalidate, + cache_version, + cx, + ) }) .ok(); }) @@ -314,7 +388,10 @@ impl InlayHintCache { shown_excerpt_hints_to_remove.retain(|(shown_anchor, shown_hint_id)| { let Some(buffer) = shown_anchor .buffer_id - .and_then(|buffer_id| multi_buffer.buffer(buffer_id)) else { return false }; + .and_then(|buffer_id| multi_buffer.buffer(buffer_id)) + else { + return false; + }; let buffer_snapshot = buffer.read(cx).snapshot(); loop { match excerpt_cache.peek() { @@ -380,12 +457,45 @@ impl InlayHintCache { } } + pub fn remove_excerpts(&mut self, excerpts_removed: Vec) -> Option { + let mut to_remove = Vec::new(); + for excerpt_to_remove in excerpts_removed { + self.update_tasks.remove(&excerpt_to_remove); + if let Some(cached_hints) = self.hints.remove(&excerpt_to_remove) { + let cached_hints = cached_hints.read(); + to_remove.extend(cached_hints.hints.iter().map(|(id, _)| *id)); + } + } + if to_remove.is_empty() { + None + } else { + self.version += 1; + Some(InlaySplice { + to_remove, + to_insert: Vec::new(), + }) + } + } + pub fn clear(&mut self) { - self.version += 1; + if !self.update_tasks.is_empty() || !self.hints.is_empty() { + self.version += 1; + } self.update_tasks.clear(); self.hints.clear(); } + pub fn hint_by_id(&self, excerpt_id: ExcerptId, hint_id: InlayId) -> Option { + self.hints + .get(&excerpt_id)? + .read() + .hints + .iter() + .find(|&(id, _)| id == &hint_id) + .map(|(_, hint)| hint) + .cloned() + } + pub fn hints(&self) -> Vec { let mut hints = Vec::new(); for excerpt_hints in self.hints.values() { @@ -398,10 +508,80 @@ impl InlayHintCache { pub fn version(&self) -> usize { self.version } + + pub fn spawn_hint_resolve( + &self, + buffer_id: u64, + excerpt_id: ExcerptId, + id: InlayId, + cx: &mut ViewContext<'_, '_, Editor>, + ) { + if let Some(excerpt_hints) = self.hints.get(&excerpt_id) { + let mut guard = excerpt_hints.write(); + if let Some(cached_hint) = guard + .hints + .iter_mut() + .find(|(hint_id, _)| hint_id == &id) + .map(|(_, hint)| hint) + { + if let ResolveState::CanResolve(server_id, _) = &cached_hint.resolve_state { + let hint_to_resolve = cached_hint.clone(); + let server_id = *server_id; + cached_hint.resolve_state = ResolveState::Resolving; + drop(guard); + cx.spawn(|editor, mut cx| async move { + let resolved_hint_task = editor.update(&mut cx, |editor, cx| { + editor + .buffer() + .read(cx) + .buffer(buffer_id) + .and_then(|buffer| { + let project = editor.project.as_ref()?; + Some(project.update(cx, |project, cx| { + project.resolve_inlay_hint( + hint_to_resolve, + buffer, + server_id, + cx, + ) + })) + }) + })?; + if let Some(resolved_hint_task) = resolved_hint_task { + let mut resolved_hint = + resolved_hint_task.await.context("hint resolve task")?; + editor.update(&mut cx, |editor, _| { + if let Some(excerpt_hints) = + editor.inlay_hint_cache.hints.get(&excerpt_id) + { + let mut guard = excerpt_hints.write(); + if let Some(cached_hint) = guard + .hints + .iter_mut() + .find(|(hint_id, _)| hint_id == &id) + .map(|(_, hint)| hint) + { + if cached_hint.resolve_state == ResolveState::Resolving { + resolved_hint.resolve_state = ResolveState::Resolved; + *cached_hint = resolved_hint; + } + } + } + })?; + } + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + } + } + } } fn spawn_new_update_tasks( editor: &mut Editor, + reason: &'static str, excerpts_to_query: HashMap, Global, Range)>, invalidate: InvalidationStrategy, update_cache_version: usize, @@ -435,11 +615,11 @@ fn spawn_new_update_tasks( } }; - let (multi_buffer_snapshot, Some(query_range)) = + let (multi_buffer_snapshot, Some(query_ranges)) = editor.buffer.update(cx, |multi_buffer, cx| { ( multi_buffer.snapshot(cx), - determine_query_range( + determine_query_ranges( multi_buffer, excerpt_id, &excerpt_buffer, @@ -447,22 +627,27 @@ fn spawn_new_update_tasks( cx, ), ) - }) else { return; }; + }) + else { + return; + }; let query = ExcerptQuery { buffer_id, excerpt_id, cache_version: update_cache_version, invalidate, + reason, }; - let new_update_task = |fetch_ranges| { + let new_update_task = |query_ranges| { new_update_task( query, - fetch_ranges, + query_ranges, multi_buffer_snapshot, buffer_snapshot.clone(), Arc::clone(&visible_hints), cached_excerpt_hints, + Arc::clone(&editor.inlay_hint_cache.lsp_request_limiter), cx, ) }; @@ -471,82 +656,184 @@ fn spawn_new_update_tasks( hash_map::Entry::Occupied(mut o) => { o.get_mut().update_cached_tasks( &buffer_snapshot, - query_range, + query_ranges, invalidate, new_update_task, ); } hash_map::Entry::Vacant(v) => { v.insert(TasksForRanges::new( - vec![query_range.clone()], - new_update_task(vec![query_range]), + query_ranges.clone(), + new_update_task(query_ranges), )); } } } } -fn determine_query_range( +#[derive(Debug, Clone)] +struct QueryRanges { + before_visible: Vec>, + visible: Vec>, + after_visible: Vec>, +} + +impl QueryRanges { + fn is_empty(&self) -> bool { + self.before_visible.is_empty() && self.visible.is_empty() && self.after_visible.is_empty() + } +} + +fn determine_query_ranges( multi_buffer: &mut MultiBuffer, excerpt_id: ExcerptId, excerpt_buffer: &ModelHandle, excerpt_visible_range: Range, cx: &mut ModelContext<'_, MultiBuffer>, -) -> Option> { +) -> Option { let full_excerpt_range = multi_buffer .excerpts_for_buffer(excerpt_buffer, cx) .into_iter() .find(|(id, _)| id == &excerpt_id) .map(|(_, range)| range.context)?; - let buffer = excerpt_buffer.read(cx); + let snapshot = buffer.snapshot(); let excerpt_visible_len = excerpt_visible_range.end - excerpt_visible_range.start; - let start_offset = excerpt_visible_range - .start - .saturating_sub(excerpt_visible_len) - .max(full_excerpt_range.start.offset); - let start = buffer.anchor_before(buffer.clip_offset(start_offset, Bias::Left)); - let end_offset = excerpt_visible_range + + let visible_range = if excerpt_visible_range.start == excerpt_visible_range.end { + return None; + } else { + vec![ + buffer.anchor_before(snapshot.clip_offset(excerpt_visible_range.start, Bias::Left)) + ..buffer.anchor_after(snapshot.clip_offset(excerpt_visible_range.end, Bias::Right)), + ] + }; + + let full_excerpt_range_end_offset = full_excerpt_range.end.to_offset(&snapshot); + let after_visible_range_start = excerpt_visible_range .end - .saturating_add(excerpt_visible_len) - .min(full_excerpt_range.end.offset) + .saturating_add(1) + .min(full_excerpt_range_end_offset) .min(buffer.len()); - let end = buffer.anchor_after(buffer.clip_offset(end_offset, Bias::Right)); - if start.cmp(&end, buffer).is_eq() { - None + let after_visible_range = if after_visible_range_start == full_excerpt_range_end_offset { + Vec::new() } else { - Some(start..end) - } + let after_range_end_offset = after_visible_range_start + .saturating_add(excerpt_visible_len) + .min(full_excerpt_range_end_offset) + .min(buffer.len()); + vec![ + buffer.anchor_before(snapshot.clip_offset(after_visible_range_start, Bias::Left)) + ..buffer.anchor_after(snapshot.clip_offset(after_range_end_offset, Bias::Right)), + ] + }; + + let full_excerpt_range_start_offset = full_excerpt_range.start.to_offset(&snapshot); + let before_visible_range_end = excerpt_visible_range + .start + .saturating_sub(1) + .max(full_excerpt_range_start_offset); + let before_visible_range = if before_visible_range_end == full_excerpt_range_start_offset { + Vec::new() + } else { + let before_range_start_offset = before_visible_range_end + .saturating_sub(excerpt_visible_len) + .max(full_excerpt_range_start_offset); + vec![ + buffer.anchor_before(snapshot.clip_offset(before_range_start_offset, Bias::Left)) + ..buffer.anchor_after(snapshot.clip_offset(before_visible_range_end, Bias::Right)), + ] + }; + + Some(QueryRanges { + before_visible: before_visible_range, + visible: visible_range, + after_visible: after_visible_range, + }) } +const MAX_CONCURRENT_LSP_REQUESTS: usize = 5; +const INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS: u64 = 400; + fn new_update_task( query: ExcerptQuery, - hint_fetch_ranges: Vec>, + query_ranges: QueryRanges, multi_buffer_snapshot: MultiBufferSnapshot, buffer_snapshot: BufferSnapshot, visible_hints: Arc>, cached_excerpt_hints: Option>>, + lsp_request_limiter: Arc, cx: &mut ViewContext<'_, '_, Editor>, ) -> Task<()> { - cx.spawn(|editor, cx| async move { - let task_update_results = - futures::future::join_all(hint_fetch_ranges.into_iter().map(|range| { - fetch_and_update_hints( - editor.clone(), - multi_buffer_snapshot.clone(), - buffer_snapshot.clone(), - Arc::clone(&visible_hints), - cached_excerpt_hints.as_ref().map(Arc::clone), - query, - range, - cx.clone(), + cx.spawn(|editor, mut cx| async move { + let closure_cx = cx.clone(); + let fetch_and_update_hints = |invalidate, range| { + fetch_and_update_hints( + editor.clone(), + multi_buffer_snapshot.clone(), + buffer_snapshot.clone(), + Arc::clone(&visible_hints), + cached_excerpt_hints.as_ref().map(Arc::clone), + query, + invalidate, + range, + Arc::clone(&lsp_request_limiter), + closure_cx.clone(), + ) + }; + let visible_range_update_results = future::join_all(query_ranges.visible.into_iter().map( + |visible_range| async move { + ( + visible_range.clone(), + fetch_and_update_hints(query.invalidate.should_invalidate(), visible_range) + .await, ) - })) - .await; + }, + )) + .await; + + let hint_delay = cx.background().timer(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS, + )); + + let mut query_range_failed = |range: &Range, e: anyhow::Error| { + log::error!("inlay hint update task for range {range:?} failed: {e:#}"); + editor + .update(&mut cx, |editor, _| { + if let Some(task_ranges) = editor + .inlay_hint_cache + .update_tasks + .get_mut(&query.excerpt_id) + { + task_ranges.invalidate_range(&buffer_snapshot, &range); + } + }) + .ok() + }; - for result in task_update_results { + for (range, result) in visible_range_update_results { + if let Err(e) = result { + query_range_failed(&range, e); + } + } + + hint_delay.await; + let invisible_range_update_results = future::join_all( + query_ranges + .before_visible + .into_iter() + .chain(query_ranges.after_visible.into_iter()) + .map(|invisible_range| async move { + ( + invisible_range.clone(), + fetch_and_update_hints(false, invisible_range).await, + ) + }), + ) + .await; + for (range, result) in invisible_range_update_results { if let Err(e) = result { - error!("inlay hint update task failed: {e:#}"); + query_range_failed(&range, e); } } }) @@ -559,11 +846,53 @@ async fn fetch_and_update_hints( visible_hints: Arc>, cached_excerpt_hints: Option>>, query: ExcerptQuery, + invalidate: bool, fetch_range: Range, + lsp_request_limiter: Arc, mut cx: gpui::AsyncAppContext, ) -> anyhow::Result<()> { + let (lsp_request_guard, got_throttled) = if query.invalidate.should_invalidate() { + (None, false) + } else { + match lsp_request_limiter.try_acquire() { + Some(guard) => (Some(guard), false), + None => (Some(lsp_request_limiter.acquire().await), true), + } + }; + let fetch_range_to_log = + fetch_range.start.to_point(&buffer_snapshot)..fetch_range.end.to_point(&buffer_snapshot); let inlay_hints_fetch_task = editor .update(&mut cx, |editor, cx| { + if got_throttled { + let query_not_around_visible_range = match editor.excerpt_visible_offsets(None, cx).remove(&query.excerpt_id) { + Some((_, _, current_visible_range)) => { + let visible_offset_length = current_visible_range.len(); + let double_visible_range = current_visible_range + .start + .saturating_sub(visible_offset_length) + ..current_visible_range + .end + .saturating_add(visible_offset_length) + .min(buffer_snapshot.len()); + !double_visible_range + .contains(&fetch_range.start.to_offset(&buffer_snapshot)) + && !double_visible_range + .contains(&fetch_range.end.to_offset(&buffer_snapshot)) + }, + None => true, + }; + if query_not_around_visible_range { + log::trace!("Fetching inlay hints for range {fetch_range_to_log:?} got throttled and fell off the current visible range, skipping."); + if let Some(task_ranges) = editor + .inlay_hint_cache + .update_tasks + .get_mut(&query.excerpt_id) + { + task_ranges.invalidate_range(&buffer_snapshot, &fetch_range); + } + return None; + } + } editor .buffer() .read(cx) @@ -578,16 +907,34 @@ async fn fetch_and_update_hints( .ok() .flatten(); let new_hints = match inlay_hints_fetch_task { - Some(task) => task.await.context("inlay hint fetch task")?, + Some(fetch_task) => { + log::debug!( + "Fetching inlay hints for range {fetch_range_to_log:?}, reason: {query_reason}, invalidate: {invalidate}", + query_reason = query.reason, + ); + log::trace!( + "Currently visible hints: {visible_hints:?}, cached hints present: {}", + cached_excerpt_hints.is_some(), + ); + fetch_task.await.context("inlay hint fetch task")? + } None => return Ok(()), }; + drop(lsp_request_guard); + log::debug!( + "Fetched {} hints for range {fetch_range_to_log:?}", + new_hints.len() + ); + log::trace!("Fetched hints: {new_hints:?}"); + let background_task_buffer_snapshot = buffer_snapshot.clone(); let backround_fetch_range = fetch_range.clone(); let new_update = cx .background() .spawn(async move { calculate_hint_updates( - query, + query.excerpt_id, + invalidate, backround_fetch_range, new_hints, &background_task_buffer_snapshot, @@ -597,12 +944,20 @@ async fn fetch_and_update_hints( }) .await; if let Some(new_update) = new_update { + log::info!( + "Applying update for range {fetch_range_to_log:?}: remove from editor: {}, remove from cache: {}, add to cache: {}", + new_update.remove_from_visible.len(), + new_update.remove_from_cache.len(), + new_update.add_to_cache.len() + ); + log::trace!("New update: {new_update:?}"); editor .update(&mut cx, |editor, cx| { apply_hint_update( editor, new_update, query, + invalidate, buffer_snapshot, multi_buffer_snapshot, cx, @@ -614,14 +969,15 @@ async fn fetch_and_update_hints( } fn calculate_hint_updates( - query: ExcerptQuery, + excerpt_id: ExcerptId, + invalidate: bool, fetch_range: Range, new_excerpt_hints: Vec, buffer_snapshot: &BufferSnapshot, cached_excerpt_hints: Option>>, visible_hints: &[Inlay], ) -> Option { - let mut add_to_cache: HashSet = HashSet::default(); + let mut add_to_cache = Vec::::new(); let mut excerpt_hints_to_persist = HashMap::default(); for new_hint in new_excerpt_hints { if !contains_position(&fetch_range, new_hint.position, buffer_snapshot) { @@ -634,13 +990,21 @@ fn calculate_hint_updates( probe.1.position.cmp(&new_hint.position, buffer_snapshot) }) { Ok(ix) => { - let (cached_inlay_id, cached_hint) = &cached_excerpt_hints.hints[ix]; - if cached_hint == &new_hint { - excerpt_hints_to_persist.insert(*cached_inlay_id, cached_hint.kind); - false - } else { - true + let mut missing_from_cache = true; + for (cached_inlay_id, cached_hint) in &cached_excerpt_hints.hints[ix..] { + if new_hint + .position + .cmp(&cached_hint.position, buffer_snapshot) + .is_gt() + { + break; + } + if cached_hint == &new_hint { + excerpt_hints_to_persist.insert(*cached_inlay_id, cached_hint.kind); + missing_from_cache = false; + } } + missing_from_cache } Err(_) => true, } @@ -648,17 +1012,17 @@ fn calculate_hint_updates( None => true, }; if missing_from_cache { - add_to_cache.insert(new_hint); + add_to_cache.push(new_hint); } } let mut remove_from_visible = Vec::new(); let mut remove_from_cache = HashSet::default(); - if query.invalidate.should_invalidate() { + if invalidate { remove_from_visible.extend( visible_hints .iter() - .filter(|hint| hint.position.excerpt_id == query.excerpt_id) + .filter(|hint| hint.position.excerpt_id == excerpt_id) .map(|inlay_hint| inlay_hint.id) .filter(|hint_id| !excerpt_hints_to_persist.contains_key(hint_id)), ); @@ -681,7 +1045,7 @@ fn calculate_hint_updates( None } else { Some(ExcerptHintsUpdate { - excerpt_id: query.excerpt_id, + excerpt_id, remove_from_visible, remove_from_cache, add_to_cache, @@ -702,6 +1066,7 @@ fn apply_hint_update( editor: &mut Editor, new_update: ExcerptHintsUpdate, query: ExcerptQuery, + invalidate: bool, buffer_snapshot: BufferSnapshot, multi_buffer_snapshot: MultiBufferSnapshot, cx: &mut ViewContext<'_, '_, Editor>, @@ -740,11 +1105,21 @@ fn apply_hint_update( .binary_search_by(|probe| probe.1.position.cmp(&new_hint.position, &buffer_snapshot)) { Ok(i) => { - if cached_hints[i].1.text() == new_hint.text() { - None - } else { - Some(i) + let mut insert_position = Some(i); + for (_, cached_hint) in &cached_hints[i..] { + if new_hint + .position + .cmp(&cached_hint.position, &buffer_snapshot) + .is_gt() + { + break; + } + if cached_hint.text() == new_hint.text() { + insert_position = None; + break; + } } + insert_position } Err(i) => Some(i), }; @@ -769,7 +1144,7 @@ fn apply_hint_update( cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone(); drop(cached_excerpt_hints); - if query.invalidate.should_invalidate() { + if invalidate { let mut outdated_excerpt_caches = HashSet::default(); for (excerpt_id, excerpt_hints) in &editor.inlay_hint_cache().hints { let excerpt_hints = excerpt_hints.read(); @@ -806,8 +1181,8 @@ fn apply_hint_update( } #[cfg(test)] -mod tests { - use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; +pub mod tests { + use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; use crate::{ scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount}, @@ -885,13 +1260,13 @@ mod tests { let mut edits_made = 1; editor.update(cx, |editor, cx| { - let expected_layers = vec!["0".to_string()]; + let expected_hints = vec!["0".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Should get its first hints when opening the editor" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + 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, @@ -910,13 +1285,13 @@ mod tests { }); cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let expected_layers = vec!["0".to_string(), "1".to_string()]; + let expected_hints = vec!["0".to_string(), "1".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Should get new hints after an edit" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + 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, @@ -935,13 +1310,13 @@ mod tests { edits_made += 1; cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let expected_layers = vec!["0".to_string(), "1".to_string(), "2".to_string()]; + let expected_hints = vec!["0".to_string(), "1".to_string(), "2".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Should get new hints after hint refresh/ request" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + 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, @@ -995,13 +1370,13 @@ mod tests { let mut edits_made = 1; editor.update(cx, |editor, cx| { - let expected_layers = vec!["0".to_string()]; + let expected_hints = vec!["0".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Should get its first hints when opening the editor" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!( editor.inlay_hint_cache().version, edits_made, @@ -1026,13 +1401,13 @@ mod tests { cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let expected_layers = vec!["0".to_string()]; + let expected_hints = vec!["0".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Should not update hints while the work task is running" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!( editor.inlay_hint_cache().version, edits_made, @@ -1050,13 +1425,13 @@ mod tests { edits_made += 1; editor.update(cx, |editor, cx| { - let expected_layers = vec!["1".to_string()]; + let expected_hints = vec!["1".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "New hints should be queried after the work task is done" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!( editor.inlay_hint_cache().version, edits_made, @@ -1169,13 +1544,13 @@ mod tests { .await; cx.foreground().run_until_parked(); rs_editor.update(cx, |editor, cx| { - let expected_layers = vec!["0".to_string()]; + let expected_hints = vec!["0".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Should get its first hints when opening the editor" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!( editor.inlay_hint_cache().version, 1, @@ -1227,13 +1602,13 @@ mod tests { .await; cx.foreground().run_until_parked(); md_editor.update(cx, |editor, cx| { - let expected_layers = vec!["0".to_string()]; + let expected_hints = vec!["0".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Markdown editor should have a separate verison, repeating Rust editor rules" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!(editor.inlay_hint_cache().version, 1); }); @@ -1243,13 +1618,13 @@ mod tests { }); cx.foreground().run_until_parked(); rs_editor.update(cx, |editor, cx| { - let expected_layers = vec!["1".to_string()]; + let expected_hints = vec!["1".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Rust inlay cache should change after the edit" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!( editor.inlay_hint_cache().version, 2, @@ -1257,13 +1632,13 @@ mod tests { ); }); md_editor.update(cx, |editor, cx| { - let expected_layers = vec!["0".to_string()]; + let expected_hints = vec!["0".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Markdown editor should not be affected by Rust editor changes" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!(editor.inlay_hint_cache().version, 1); }); @@ -1273,23 +1648,23 @@ mod tests { }); cx.foreground().run_until_parked(); md_editor.update(cx, |editor, cx| { - let expected_layers = vec!["1".to_string()]; + let expected_hints = vec!["1".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Rust editor should not be affected by Markdown editor changes" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!(editor.inlay_hint_cache().version, 2); }); rs_editor.update(cx, |editor, cx| { - let expected_layers = vec!["1".to_string()]; + let expected_hints = vec!["1".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Markdown editor should also change independently" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!(editor.inlay_hint_cache().version, 2); }); } @@ -1718,7 +2093,7 @@ mod tests { }); })); } - let _ = futures::future::join_all(edits).await; + let _ = future::join_all(edits).await; cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { @@ -1815,7 +2190,7 @@ mod tests { .downcast::() .unwrap(); let lsp_request_ranges = Arc::new(Mutex::new(Vec::new())); - let lsp_request_count = Arc::new(AtomicU32::new(0)); + let lsp_request_count = Arc::new(AtomicUsize::new(0)); let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges); let closure_lsp_request_count = Arc::clone(&lsp_request_count); fake_server @@ -1829,10 +2204,9 @@ mod tests { ); task_lsp_request_ranges.lock().push(params.range); - let query_start = params.range.start; let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::Release) + 1; Ok(Some(vec![lsp::InlayHint { - position: query_start, + position: params.range.end, label: lsp::InlayHintLabel::String(i.to_string()), kind: None, text_edits: None, @@ -1869,28 +2243,51 @@ mod tests { }) } + // in large buffers, requests are made for more than visible range of a buffer. + // invisible parts are queried later, to avoid excessive requests on quick typing. + // wait the timeout needed to get all requests. + cx.foreground().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); + cx.foreground().run_until_parked(); let initial_visible_range = editor_visible_range(&editor, cx); + let lsp_initial_visible_range = lsp::Range::new( + lsp::Position::new( + initial_visible_range.start.row, + initial_visible_range.start.column, + ), + lsp::Position::new( + initial_visible_range.end.row, + initial_visible_range.end.column, + ), + ); let expected_initial_query_range_end = - lsp::Position::new(initial_visible_range.end.row * 2, 1); - cx.foreground().run_until_parked(); + lsp::Position::new(initial_visible_range.end.row * 2, 2); + let mut expected_invisible_query_start = lsp_initial_visible_range.end; + expected_invisible_query_start.character += 1; editor.update(cx, |editor, cx| { let ranges = lsp_request_ranges.lock().drain(..).collect::>(); - assert_eq!(ranges.len(), 1, - "When scroll is at the edge of a big document, double of its visible part range should be queried for hints in one single big request, but got: {ranges:?}"); - let query_range = &ranges[0]; - assert_eq!(query_range.start, lsp::Position::new(0, 0), "Should query initially from the beginning of the document"); - assert_eq!(query_range.end, expected_initial_query_range_end, "Should query initially for double lines of the visible part of the document"); + assert_eq!(ranges.len(), 2, + "When scroll is at the edge of a big document, its visible part and the same range further should be queried in order, but got: {ranges:?}"); + let visible_query_range = &ranges[0]; + assert_eq!(visible_query_range.start, lsp_initial_visible_range.start); + assert_eq!(visible_query_range.end, lsp_initial_visible_range.end); + let invisible_query_range = &ranges[1]; + + assert_eq!(invisible_query_range.start, expected_invisible_query_start, "Should initially query visible edge of the document"); + assert_eq!(invisible_query_range.end, expected_initial_query_range_end, "Should initially query visible edge of the document"); - assert_eq!(lsp_request_count.load(Ordering::Acquire), 1); - let expected_layers = vec!["1".to_string()]; + let requests_count = lsp_request_count.load(Ordering::Acquire); + assert_eq!(requests_count, 2, "Visible + invisible request"); + let expected_hints = vec!["1".to_string(), "2".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Should have hints from both LSP requests made for a big file" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx), "Should display only hints from the visible range"); assert_eq!( - editor.inlay_hint_cache().version, 1, + editor.inlay_hint_cache().version, requests_count, "LSP queries should've bumped the cache version" ); }); @@ -1899,11 +2296,13 @@ mod tests { editor.scroll_screen(&ScrollAmount::Page(1.0), cx); editor.scroll_screen(&ScrollAmount::Page(1.0), cx); }); - + cx.foreground().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); + cx.foreground().run_until_parked(); let visible_range_after_scrolls = editor_visible_range(&editor, cx); let visible_line_count = editor.update(cx, |editor, _| editor.visible_line_count().unwrap()); - cx.foreground().run_until_parked(); let selection_in_cached_range = editor.update(cx, |editor, cx| { let ranges = lsp_request_ranges .lock() @@ -1930,26 +2329,28 @@ mod tests { lsp::Position::new( visible_range_after_scrolls.end.row + visible_line_count.ceil() as u32, - 0 + 1, ), "Second scroll should query one more screen down after the end of the visible range" ); + let lsp_requests = lsp_request_count.load(Ordering::Acquire); + assert_eq!(lsp_requests, 4, "Should query for hints after every scroll"); + let expected_hints = vec![ + "1".to_string(), + "2".to_string(), + "3".to_string(), + "4".to_string(), + ]; assert_eq!( - lsp_request_count.load(Ordering::Acquire), - 3, - "Should query for hints after every scroll" - ); - let expected_layers = vec!["1".to_string(), "2".to_string(), "3".to_string()]; - assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Should have hints from the new LSP response after the edit" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!( editor.inlay_hint_cache().version, - 3, + lsp_requests, "Should update the cache for every LSP response with hints added" ); @@ -1963,6 +2364,9 @@ mod tests { s.select_ranges([selection_in_cached_range..selection_in_cached_range]) }); }); + cx.foreground().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); cx.foreground().run_until_parked(); editor.update(cx, |_, _| { let ranges = lsp_request_ranges @@ -1971,33 +2375,43 @@ mod tests { .sorted_by_key(|r| r.start) .collect::>(); assert!(ranges.is_empty(), "No new ranges or LSP queries should be made after returning to the selection with cached hints"); - assert_eq!(lsp_request_count.load(Ordering::Acquire), 3); + assert_eq!(lsp_request_count.load(Ordering::Acquire), 4); }); editor.update(cx, |editor, cx| { editor.handle_input("++++more text++++", cx); }); + cx.foreground().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { let ranges = lsp_request_ranges.lock().drain(..).collect::>(); - assert_eq!(ranges.len(), 1, - "On edit, should scroll to selection and query a range around it. Instead, got query ranges {ranges:?}"); - let query_range = &ranges[0]; - assert!(query_range.start.line < selection_in_cached_range.row, + assert_eq!(ranges.len(), 3, + "On edit, should scroll to selection and query a range around it: visible + same range above and below. Instead, got query ranges {ranges:?}"); + let visible_query_range = &ranges[0]; + let above_query_range = &ranges[1]; + let below_query_range = &ranges[2]; + assert!(above_query_range.end.character < visible_query_range.start.character || above_query_range.end.line + 1 == visible_query_range.start.line, + "Above range {above_query_range:?} should be before visible range {visible_query_range:?}"); + assert!(visible_query_range.end.character < below_query_range.start.character || visible_query_range.end.line + 1 == below_query_range.start.line, + "Visible range {visible_query_range:?} should be before below range {below_query_range:?}"); + assert!(above_query_range.start.line < selection_in_cached_range.row, "Hints should be queried with the selected range after the query range start"); - assert!(query_range.end.line > selection_in_cached_range.row, + assert!(below_query_range.end.line > selection_in_cached_range.row, "Hints should be queried with the selected range before the query range end"); - assert!(query_range.start.line <= selection_in_cached_range.row - (visible_line_count * 3.0 / 2.0) as u32, + assert!(above_query_range.start.line <= selection_in_cached_range.row - (visible_line_count * 3.0 / 2.0) as u32, "Hints query range should contain one more screen before"); - assert!(query_range.end.line >= selection_in_cached_range.row + (visible_line_count * 3.0 / 2.0) as u32, + assert!(below_query_range.end.line >= selection_in_cached_range.row + (visible_line_count * 3.0 / 2.0) as u32, "Hints query range should contain one more screen after"); - assert_eq!(lsp_request_count.load(Ordering::Acquire), 4, "Should query for hints once after the edit"); - let expected_layers = vec!["4".to_string()]; - assert_eq!(expected_layers, cached_hint_labels(editor), + let lsp_requests = lsp_request_count.load(Ordering::Acquire); + assert_eq!(lsp_requests, 7, "There should be a visible range and two ranges above and below it queried"); + let expected_hints = vec!["5".to_string(), "6".to_string(), "7".to_string()]; + assert_eq!(expected_hints, cached_hint_labels(editor), "Should have hints from the new LSP response after the edit"); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - assert_eq!(editor.inlay_hint_cache().version, 4, "Should update the cache for every LSP response with hints added"); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, lsp_requests, "Should update the cache for every LSP response with hints added"); }); } @@ -2208,19 +2622,19 @@ mod tests { cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let expected_layers = vec![ + let expected_hints = vec![ "main hint #0".to_string(), "main hint #1".to_string(), "main hint #2".to_string(), "main hint #3".to_string(), ]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - assert_eq!(editor.inlay_hint_cache().version, expected_layers.len(), "Every visible excerpt hints should bump the verison"); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Every visible excerpt hints should bump the verison"); }); editor.update(cx, |editor, cx| { @@ -2236,7 +2650,7 @@ mod tests { }); cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let expected_layers = vec![ + let expected_hints = vec![ "main hint #0".to_string(), "main hint #1".to_string(), "main hint #2".to_string(), @@ -2247,10 +2661,10 @@ mod tests { "other hint #1".to_string(), "other hint #2".to_string(), ]; - assert_eq!(expected_layers, cached_hint_labels(editor), + assert_eq!(expected_hints, cached_hint_labels(editor), "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)); - assert_eq!(editor.inlay_hint_cache().version, expected_layers.len(), + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Due to every excerpt having one hint, we update cache per new excerpt scrolled"); }); @@ -2259,9 +2673,12 @@ mod tests { s.select_ranges([Point::new(100, 0)..Point::new(100, 0)]) }); }); + cx.foreground().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); cx.foreground().run_until_parked(); let last_scroll_update_version = editor.update(cx, |editor, cx| { - let expected_layers = vec![ + let expected_hints = vec![ "main hint #0".to_string(), "main hint #1".to_string(), "main hint #2".to_string(), @@ -2275,11 +2692,11 @@ mod tests { "other hint #4".to_string(), "other hint #5".to_string(), ]; - assert_eq!(expected_layers, cached_hint_labels(editor), + assert_eq!(expected_hints, cached_hint_labels(editor), "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched"); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - assert_eq!(editor.inlay_hint_cache().version, expected_layers.len()); - expected_layers.len() + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, expected_hints.len()); + expected_hints.len() }); editor.update(cx, |editor, cx| { @@ -2289,7 +2706,7 @@ mod tests { }); cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let expected_layers = vec![ + let expected_hints = vec![ "main hint #0".to_string(), "main hint #1".to_string(), "main hint #2".to_string(), @@ -2303,9 +2720,9 @@ mod tests { "other hint #4".to_string(), "other hint #5".to_string(), ]; - assert_eq!(expected_layers, cached_hint_labels(editor), + assert_eq!(expected_hints, cached_hint_labels(editor), "After multibuffer was scrolled to the end, further scrolls up should not bring more hints"); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scolled buffer"); }); @@ -2318,7 +2735,7 @@ mod tests { }); cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let expected_layers = vec![ + let expected_hints = vec![ "main hint(edited) #0".to_string(), "main hint(edited) #1".to_string(), "main hint(edited) #2".to_string(), @@ -2329,15 +2746,15 @@ mod tests { "other hint(edited) #1".to_string(), ]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "After multibuffer edit, editor gets scolled back to the last selection; \ all hints should be invalidated and requeried for all of its visible excerpts" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); let current_cache_version = editor.inlay_hint_cache().version; - let minimum_expected_version = last_scroll_update_version + expected_layers.len(); + let minimum_expected_version = last_scroll_update_version + expected_hints.len(); assert!( current_cache_version == minimum_expected_version || current_cache_version == minimum_expected_version + 1, "Due to every excerpt having one hint, cache should update per new excerpt received + 1 potential sporadic update" @@ -2544,7 +2961,7 @@ all hints should be invalidated and requeried for all of its visible excerpts" ); assert_eq!( editor.inlay_hint_cache().version, - 2, + 3, "Excerpt removal should trigger a cache update" ); }); @@ -2572,7 +2989,7 @@ all hints should be invalidated and requeried for all of its visible excerpts" ); assert_eq!( editor.inlay_hint_cache().version, - 3, + 4, "Settings change should trigger a cache update" ); }); @@ -2678,9 +3095,9 @@ all hints should be invalidated and requeried for all of its visible excerpts" }); cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let expected_layers = vec!["1".to_string()]; - assert_eq!(expected_layers, cached_hint_labels(editor)); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + let expected_hints = vec!["1".to_string()]; + assert_eq!(expected_hints, cached_hint_labels(editor)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!(editor.inlay_hint_cache().version, 1); }); } @@ -2891,15 +3308,11 @@ all hints should be invalidated and requeried for all of its visible excerpts" ("/a/main.rs", editor, fake_server) } - fn cached_hint_labels(editor: &Editor) -> Vec { + pub fn cached_hint_labels(editor: &Editor) -> Vec { let mut labels = Vec::new(); for (_, excerpt_hints) in &editor.inlay_hint_cache().hints { - let excerpt_hints = excerpt_hints.read(); - for (_, inlay) in excerpt_hints.hints.iter() { - match &inlay.label { - project::InlayHintLabel::String(s) => labels.push(s.to_string()), - _ => unreachable!(), - } + for (_, inlay) in &excerpt_hints.read().hints { + labels.push(inlay.text()); } } @@ -2907,7 +3320,7 @@ all hints should be invalidated and requeried for all of its visible excerpts" labels } - fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec { + pub fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec { let mut hints = editor .visible_inlay_hints(cx) .into_iter() diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 668ea482035ed5731cc94e8b2e6c5dbe63e5919f..d9998725922f5154e299bb3bd7a32b04ff18c2d2 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -49,13 +49,18 @@ impl FollowableItem for Editor { fn from_state_proto( pane: ViewHandle, - project: ModelHandle, + workspace: ViewHandle, remote_id: ViewId, state: &mut Option, cx: &mut AppContext, ) -> Option>>> { - let Some(proto::view::Variant::Editor(_)) = state else { return None }; - let Some(proto::view::Variant::Editor(state)) = state.take() else { unreachable!() }; + let project = workspace.read(cx).project().to_owned(); + let Some(proto::view::Variant::Editor(_)) = state else { + return None; + }; + let Some(proto::view::Variant::Editor(state)) = state.take() else { + unreachable!() + }; let client = project.read(cx).client(); let replica_id = project.read(cx).replica_id(); @@ -340,10 +345,16 @@ async fn update_editor_from_message( let mut insertions = message.inserted_excerpts.into_iter().peekable(); while let Some(insertion) = insertions.next() { - let Some(excerpt) = insertion.excerpt else { continue }; - let Some(previous_excerpt_id) = insertion.previous_excerpt_id else { continue }; + let Some(excerpt) = insertion.excerpt else { + continue; + }; + let Some(previous_excerpt_id) = insertion.previous_excerpt_id else { + continue; + }; let buffer_id = excerpt.buffer_id; - let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { continue }; + let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { + continue; + }; let adjacent_excerpts = iter::from_fn(|| { let insertion = insertions.peek()?; @@ -614,7 +625,7 @@ impl Item for Editor { fn workspace_deactivated(&mut self, cx: &mut ViewContext) { hide_link_definition(self, cx); - self.link_go_to_definition_state.last_mouse_location = None; + self.link_go_to_definition_state.last_trigger_point = None; } fn is_dirty(&self, cx: &AppContext) -> bool { @@ -753,7 +764,7 @@ impl Item for Editor { Some(Box::new(handle.clone())) } - fn pixel_position_of_cursor(&self) -> Option { + fn pixel_position_of_cursor(&self, _: &AppContext) -> Option { self.pixel_position_of_newest_cursor } diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 31df11a01959e4795738658d813ce4b23bfcafe8..1f9a3aab730d4d6077df836e54ba09bc12f397d1 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -1,22 +1,108 @@ -use crate::{Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase}; +use crate::{ + display_map::{DisplaySnapshot, InlayOffset}, + element::PointForPosition, + hover_popover::{self, InlayHover}, + Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase, +}; use gpui::{Task, ViewContext}; use language::{Bias, ToOffset}; -use project::LocationLink; +use lsp::LanguageServerId; +use project::{ + HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, LocationLink, + ResolveState, +}; use std::ops::Range; use util::TryFutureExt; #[derive(Debug, Default)] pub struct LinkGoToDefinitionState { - pub last_mouse_location: Option, - pub symbol_range: Option>, + pub last_trigger_point: Option, + pub symbol_range: Option, pub kind: Option, - pub definitions: Vec, + pub definitions: Vec, pub task: Option>>, } +#[derive(Debug)] +pub enum GoToDefinitionTrigger { + Text(DisplayPoint), + InlayHint(InlayRange, lsp::Location, LanguageServerId), +} + +#[derive(Debug, Clone)] +pub enum GoToDefinitionLink { + Text(LocationLink), + InlayHint(lsp::Location, LanguageServerId), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct InlayRange { + pub inlay_position: Anchor, + pub highlight_start: InlayOffset, + pub highlight_end: InlayOffset, +} + +#[derive(Debug, Clone)] +pub enum TriggerPoint { + Text(Anchor), + InlayHint(InlayRange, lsp::Location, LanguageServerId), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DocumentRange { + Text(Range), + Inlay(InlayRange), +} + +impl DocumentRange { + pub fn as_text_range(&self) -> Option> { + match self { + Self::Text(range) => Some(range.clone()), + Self::Inlay(_) => None, + } + } + + fn point_within_range(&self, trigger_point: &TriggerPoint, snapshot: &EditorSnapshot) -> bool { + match (self, trigger_point) { + (DocumentRange::Text(range), TriggerPoint::Text(point)) => { + let point_after_start = range.start.cmp(point, &snapshot.buffer_snapshot).is_le(); + point_after_start && range.end.cmp(point, &snapshot.buffer_snapshot).is_ge() + } + (DocumentRange::Inlay(range), TriggerPoint::InlayHint(point, _, _)) => { + range.highlight_start.cmp(&point.highlight_end).is_le() + && range.highlight_end.cmp(&point.highlight_end).is_ge() + } + (DocumentRange::Inlay(_), TriggerPoint::Text(_)) + | (DocumentRange::Text(_), TriggerPoint::InlayHint(_, _, _)) => false, + } + } +} + +impl TriggerPoint { + fn anchor(&self) -> &Anchor { + match self { + TriggerPoint::Text(anchor) => anchor, + TriggerPoint::InlayHint(range, _, _) => &range.inlay_position, + } + } + + pub fn definition_kind(&self, shift: bool) -> LinkDefinitionKind { + match self { + TriggerPoint::Text(_) => { + if shift { + LinkDefinitionKind::Type + } else { + LinkDefinitionKind::Symbol + } + } + TriggerPoint::InlayHint(_, _, _) => LinkDefinitionKind::Type, + } + } +} + pub fn update_go_to_definition_link( editor: &mut Editor, - point: Option, + origin: Option, cmd_held: bool, shift_held: bool, cx: &mut ViewContext, @@ -25,23 +111,43 @@ pub fn update_go_to_definition_link( // Store new mouse point as an anchor let snapshot = editor.snapshot(cx); - let point = point.map(|point| { - snapshot - .buffer_snapshot - .anchor_before(point.to_offset(&snapshot.display_snapshot, Bias::Left)) - }); + let trigger_point = match origin { + Some(GoToDefinitionTrigger::Text(p)) => { + Some(TriggerPoint::Text(snapshot.buffer_snapshot.anchor_before( + p.to_offset(&snapshot.display_snapshot, Bias::Left), + ))) + } + Some(GoToDefinitionTrigger::InlayHint(p, lsp_location, language_server_id)) => { + Some(TriggerPoint::InlayHint(p, lsp_location, language_server_id)) + } + None => None, + }; // If the new point is the same as the previously stored one, return early if let (Some(a), Some(b)) = ( - &point, - &editor.link_go_to_definition_state.last_mouse_location, + &trigger_point, + &editor.link_go_to_definition_state.last_trigger_point, ) { - if a.cmp(b, &snapshot.buffer_snapshot).is_eq() { - return; + match (a, b) { + (TriggerPoint::Text(anchor_a), TriggerPoint::Text(anchor_b)) => { + if anchor_a.cmp(anchor_b, &snapshot.buffer_snapshot).is_eq() { + return; + } + } + (TriggerPoint::InlayHint(range_a, _, _), TriggerPoint::InlayHint(range_b, _, _)) => { + if range_a + .inlay_position + .cmp(&range_b.inlay_position, &snapshot.buffer_snapshot) + .is_eq() + { + return; + } + } + _ => {} } } - editor.link_go_to_definition_state.last_mouse_location = point.clone(); + editor.link_go_to_definition_state.last_trigger_point = trigger_point.clone(); if pending_nonempty_selection { hide_link_definition(editor, cx); @@ -49,14 +155,9 @@ pub fn update_go_to_definition_link( } if cmd_held { - if let Some(point) = point { - let kind = if shift_held { - LinkDefinitionKind::Type - } else { - LinkDefinitionKind::Symbol - }; - - show_link_definition(kind, editor, point, snapshot, cx); + if let Some(trigger_point) = trigger_point { + let kind = trigger_point.definition_kind(shift_held); + show_link_definition(kind, editor, trigger_point, snapshot, cx); return; } } @@ -64,6 +165,182 @@ pub fn update_go_to_definition_link( hide_link_definition(editor, cx); } +pub fn update_inlay_link_and_hover_points( + snapshot: &DisplaySnapshot, + point_for_position: PointForPosition, + editor: &mut Editor, + cmd_held: bool, + shift_held: bool, + cx: &mut ViewContext<'_, '_, Editor>, +) { + let hint_start_offset = + snapshot.display_point_to_inlay_offset(point_for_position.previous_valid, Bias::Left); + let hint_end_offset = + snapshot.display_point_to_inlay_offset(point_for_position.next_valid, Bias::Right); + let hovered_offset = if point_for_position.column_overshoot_after_line_end == 0 { + Some(snapshot.display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left)) + } else { + None + }; + let mut go_to_definition_updated = false; + let mut hover_updated = false; + if let Some(hovered_offset) = hovered_offset { + let buffer_snapshot = editor.buffer().read(cx).snapshot(cx); + let previous_valid_anchor = buffer_snapshot.anchor_at( + point_for_position.previous_valid.to_point(snapshot), + Bias::Left, + ); + let next_valid_anchor = buffer_snapshot.anchor_at( + point_for_position.next_valid.to_point(snapshot), + Bias::Right, + ); + if let Some(hovered_hint) = editor + .visible_inlay_hints(cx) + .into_iter() + .skip_while(|hint| { + hint.position + .cmp(&previous_valid_anchor, &buffer_snapshot) + .is_lt() + }) + .take_while(|hint| { + hint.position + .cmp(&next_valid_anchor, &buffer_snapshot) + .is_le() + }) + .max_by_key(|hint| hint.id) + { + let inlay_hint_cache = editor.inlay_hint_cache(); + let excerpt_id = previous_valid_anchor.excerpt_id; + if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) { + match cached_hint.resolve_state { + ResolveState::CanResolve(_, _) => { + if let Some(buffer_id) = previous_valid_anchor.buffer_id { + inlay_hint_cache.spawn_hint_resolve( + buffer_id, + excerpt_id, + hovered_hint.id, + cx, + ); + } + } + ResolveState::Resolved => { + let mut actual_hint_start = hint_start_offset; + let mut actual_hint_end = hint_end_offset; + if cached_hint.padding_left { + actual_hint_start.0 += 1; + actual_hint_end.0 += 1; + } + if cached_hint.padding_right { + actual_hint_start.0 += 1; + actual_hint_end.0 += 1; + } + match cached_hint.label { + project::InlayHintLabel::String(_) => { + if let Some(tooltip) = cached_hint.tooltip { + hover_popover::hover_at_inlay( + editor, + InlayHover { + excerpt: excerpt_id, + tooltip: match tooltip { + InlayHintTooltip::String(text) => HoverBlock { + text, + kind: HoverBlockKind::PlainText, + }, + InlayHintTooltip::MarkupContent(content) => { + HoverBlock { + text: content.value, + kind: content.kind, + } + } + }, + triggered_from: hovered_offset, + range: InlayRange { + inlay_position: hovered_hint.position, + highlight_start: actual_hint_start, + highlight_end: actual_hint_end, + }, + }, + cx, + ); + hover_updated = true; + } + } + project::InlayHintLabel::LabelParts(label_parts) => { + if let Some((hovered_hint_part, part_range)) = + hover_popover::find_hovered_hint_part( + label_parts, + actual_hint_start..actual_hint_end, + hovered_offset, + ) + { + if let Some(tooltip) = hovered_hint_part.tooltip { + hover_popover::hover_at_inlay( + editor, + InlayHover { + excerpt: excerpt_id, + tooltip: match tooltip { + InlayHintLabelPartTooltip::String(text) => { + HoverBlock { + text, + kind: HoverBlockKind::PlainText, + } + } + InlayHintLabelPartTooltip::MarkupContent( + content, + ) => HoverBlock { + text: content.value, + kind: content.kind, + }, + }, + triggered_from: hovered_offset, + range: InlayRange { + inlay_position: hovered_hint.position, + highlight_start: part_range.start, + highlight_end: part_range.end, + }, + }, + cx, + ); + hover_updated = true; + } + if let Some((language_server_id, location)) = + hovered_hint_part.location + { + go_to_definition_updated = true; + update_go_to_definition_link( + editor, + Some(GoToDefinitionTrigger::InlayHint( + InlayRange { + inlay_position: hovered_hint.position, + highlight_start: part_range.start, + highlight_end: part_range.end, + }, + location, + language_server_id, + )), + cmd_held, + shift_held, + cx, + ); + } + } + } + }; + } + ResolveState::Resolving => {} + } + } + } + } + + if !go_to_definition_updated { + update_go_to_definition_link(editor, None, cmd_held, shift_held, cx); + } + if !hover_updated { + hover_popover::hover_at(editor, None, cx); + } +} + #[derive(Debug, Clone, Copy, PartialEq)] pub enum LinkDefinitionKind { Symbol, @@ -73,7 +350,7 @@ pub enum LinkDefinitionKind { pub fn show_link_definition( definition_kind: LinkDefinitionKind, editor: &mut Editor, - trigger_point: Anchor, + trigger_point: TriggerPoint, snapshot: EditorSnapshot, cx: &mut ViewContext, ) { @@ -86,10 +363,11 @@ pub fn show_link_definition( return; } + let trigger_anchor = trigger_point.anchor(); let (buffer, buffer_position) = if let Some(output) = editor .buffer .read(cx) - .text_anchor_for_position(trigger_point.clone(), cx) + .text_anchor_for_position(trigger_anchor.clone(), cx) { output } else { @@ -99,7 +377,7 @@ pub fn show_link_definition( let excerpt_id = if let Some((excerpt_id, _, _)) = editor .buffer() .read(cx) - .excerpt_containing(trigger_point.clone(), cx) + .excerpt_containing(trigger_anchor.clone(), cx) { excerpt_id } else { @@ -114,52 +392,57 @@ pub fn show_link_definition( // Don't request again if the location is within the symbol region of a previous request with the same kind if let Some(symbol_range) = &editor.link_go_to_definition_state.symbol_range { - let point_after_start = symbol_range - .start - .cmp(&trigger_point, &snapshot.buffer_snapshot) - .is_le(); - - let point_before_end = symbol_range - .end - .cmp(&trigger_point, &snapshot.buffer_snapshot) - .is_ge(); - - let point_within_range = point_after_start && point_before_end; - if point_within_range && same_kind { + if same_kind && symbol_range.point_within_range(&trigger_point, &snapshot) { return; } } let task = cx.spawn(|this, mut cx| { async move { - // query the LSP for definition info - let definition_request = cx.update(|cx| { - project.update(cx, |project, cx| match definition_kind { - LinkDefinitionKind::Symbol => project.definition(&buffer, buffer_position, cx), - - LinkDefinitionKind::Type => { - project.type_definition(&buffer, buffer_position, cx) - } - }) - }); + let result = match &trigger_point { + TriggerPoint::Text(_) => { + // query the LSP for definition info + cx.update(|cx| { + project.update(cx, |project, cx| match definition_kind { + LinkDefinitionKind::Symbol => { + project.definition(&buffer, buffer_position, cx) + } - let result = definition_request.await.ok().map(|definition_result| { - ( - definition_result.iter().find_map(|link| { - link.origin.as_ref().map(|origin| { - let start = snapshot - .buffer_snapshot - .anchor_in_excerpt(excerpt_id.clone(), origin.range.start); - let end = snapshot - .buffer_snapshot - .anchor_in_excerpt(excerpt_id.clone(), origin.range.end); - - start..end + LinkDefinitionKind::Type => { + project.type_definition(&buffer, buffer_position, cx) + } }) - }), - definition_result, - ) - }); + }) + .await + .ok() + .map(|definition_result| { + ( + definition_result.iter().find_map(|link| { + link.origin.as_ref().map(|origin| { + let start = snapshot + .buffer_snapshot + .anchor_in_excerpt(excerpt_id.clone(), origin.range.start); + let end = snapshot + .buffer_snapshot + .anchor_in_excerpt(excerpt_id.clone(), origin.range.end); + DocumentRange::Text(start..end) + }) + }), + definition_result + .into_iter() + .map(GoToDefinitionLink::Text) + .collect(), + ) + }) + } + TriggerPoint::InlayHint(trigger_source, lsp_location, server_id) => Some(( + Some(DocumentRange::Inlay(*trigger_source)), + vec![GoToDefinitionLink::InlayHint( + lsp_location.clone(), + *server_id, + )], + )), + }; this.update(&mut cx, |this, cx| { // Clear any existing highlights @@ -178,43 +461,67 @@ pub fn show_link_definition( // the current location. let any_definition_does_not_contain_current_location = definitions.iter().any(|definition| { - let target = &definition.target; - if target.buffer == buffer { - let range = &target.range; - // Expand range by one character as lsp definition ranges include positions adjacent - // but not contained by the symbol range - let start = buffer_snapshot.clip_offset( - range.start.to_offset(&buffer_snapshot).saturating_sub(1), - Bias::Left, - ); - let end = buffer_snapshot.clip_offset( - range.end.to_offset(&buffer_snapshot) + 1, - Bias::Right, - ); - let offset = buffer_position.to_offset(&buffer_snapshot); - !(start <= offset && end >= offset) - } else { - true + match &definition { + GoToDefinitionLink::Text(link) => { + if link.target.buffer == buffer { + let range = &link.target.range; + // Expand range by one character as lsp definition ranges include positions adjacent + // but not contained by the symbol range + let start = buffer_snapshot.clip_offset( + range + .start + .to_offset(&buffer_snapshot) + .saturating_sub(1), + Bias::Left, + ); + let end = buffer_snapshot.clip_offset( + range.end.to_offset(&buffer_snapshot) + 1, + Bias::Right, + ); + let offset = buffer_position.to_offset(&buffer_snapshot); + !(start <= offset && end >= offset) + } else { + true + } + } + GoToDefinitionLink::InlayHint(_, _) => true, } }); if any_definition_does_not_contain_current_location { - // If no symbol range returned from language server, use the surrounding word. - let highlight_range = symbol_range.unwrap_or_else(|| { - let snapshot = &snapshot.buffer_snapshot; - let (offset_range, _) = snapshot.surrounding_word(trigger_point); - - snapshot.anchor_before(offset_range.start) - ..snapshot.anchor_after(offset_range.end) - }); - // Highlight symbol using theme link definition highlight style let style = theme::current(cx).editor.link_definition; - this.highlight_text::( - vec![highlight_range], - style, - cx, - ); + let highlight_range = + symbol_range.unwrap_or_else(|| match &trigger_point { + TriggerPoint::Text(trigger_anchor) => { + let snapshot = &snapshot.buffer_snapshot; + // If no symbol range returned from language server, use the surrounding word. + let (offset_range, _) = + snapshot.surrounding_word(*trigger_anchor); + DocumentRange::Text( + snapshot.anchor_before(offset_range.start) + ..snapshot.anchor_after(offset_range.end), + ) + } + TriggerPoint::InlayHint(inlay_coordinates, _, _) => { + DocumentRange::Inlay(*inlay_coordinates) + } + }); + + match highlight_range { + DocumentRange::Text(text_range) => this + .highlight_text::( + vec![text_range], + style, + cx, + ), + DocumentRange::Inlay(inlay_coordinates) => this + .highlight_inlays::( + vec![inlay_coordinates], + style, + cx, + ), + } } else { hide_link_definition(this, cx); } @@ -245,7 +552,7 @@ pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext) { pub fn go_to_fetched_definition( editor: &mut Editor, - point: DisplayPoint, + point: PointForPosition, split: bool, cx: &mut ViewContext, ) { @@ -254,7 +561,7 @@ pub fn go_to_fetched_definition( pub fn go_to_fetched_type_definition( editor: &mut Editor, - point: DisplayPoint, + point: PointForPosition, split: bool, cx: &mut ViewContext, ) { @@ -264,7 +571,7 @@ pub fn go_to_fetched_type_definition( fn go_to_fetched_definition_of_kind( kind: LinkDefinitionKind, editor: &mut Editor, - point: DisplayPoint, + point: PointForPosition, split: bool, cx: &mut ViewContext, ) { @@ -282,16 +589,18 @@ fn go_to_fetched_definition_of_kind( } else { editor.select( SelectPhase::Begin { - position: point, + position: point.next_valid, add: false, click_count: 1, }, cx, ); - match kind { - LinkDefinitionKind::Symbol => editor.go_to_definition(&Default::default(), cx), - LinkDefinitionKind::Type => editor.go_to_type_definition(&Default::default(), cx), + if point.as_valid().is_some() { + match kind { + LinkDefinitionKind::Symbol => editor.go_to_definition(&Default::default(), cx), + LinkDefinitionKind::Type => editor.go_to_type_definition(&Default::default(), cx), + } } } } @@ -299,14 +608,21 @@ fn go_to_fetched_definition_of_kind( #[cfg(test)] mod tests { use super::*; - use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext}; + use crate::{ + display_map::ToDisplayPoint, + editor_tests::init_test, + inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels}, + test::editor_lsp_test_context::EditorLspTestContext, + }; use futures::StreamExt; use gpui::{ platform::{self, Modifiers, ModifiersChangedEvent}, View, }; use indoc::indoc; + use language::language_settings::InlayHintSettings; use lsp::request::{GotoDefinition, GotoTypeDefinition}; + use util::assert_set_eq; #[gpui::test] async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) { @@ -355,7 +671,13 @@ mod tests { // Press cmd+shift to trigger highlight cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), true, true, cx); + update_go_to_definition_link( + editor, + Some(GoToDefinitionTrigger::Text(hover_point)), + true, + true, + cx, + ); }); requests.next().await; cx.foreground().run_until_parked(); @@ -406,7 +728,7 @@ mod tests { }); cx.update_editor(|editor, cx| { - go_to_fetched_type_definition(editor, hover_point, false, cx); + go_to_fetched_type_definition(editor, PointForPosition::valid(hover_point), false, cx); }); requests.next().await; cx.foreground().run_until_parked(); @@ -461,7 +783,13 @@ mod tests { }); cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), true, false, cx); + update_go_to_definition_link( + editor, + Some(GoToDefinitionTrigger::Text(hover_point)), + true, + false, + cx, + ); }); requests.next().await; cx.foreground().run_until_parked(); @@ -482,7 +810,7 @@ mod tests { "}); // Response without source range still highlights word - cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_mouse_location = None); + cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_trigger_point = None); let mut requests = cx.handle_request::(move |url, _, _| async move { Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ lsp::LocationLink { @@ -495,7 +823,13 @@ mod tests { ]))) }); cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), true, false, cx); + update_go_to_definition_link( + editor, + Some(GoToDefinitionTrigger::Text(hover_point)), + true, + false, + cx, + ); }); requests.next().await; cx.foreground().run_until_parked(); @@ -517,7 +851,13 @@ mod tests { Ok(Some(lsp::GotoDefinitionResponse::Link(vec![]))) }); cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), true, false, cx); + update_go_to_definition_link( + editor, + Some(GoToDefinitionTrigger::Text(hover_point)), + true, + false, + cx, + ); }); requests.next().await; cx.foreground().run_until_parked(); @@ -534,7 +874,13 @@ mod tests { fn do_work() { teˇst(); } "}); cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), false, false, cx); + update_go_to_definition_link( + editor, + Some(GoToDefinitionTrigger::Text(hover_point)), + false, + false, + cx, + ); }); cx.foreground().run_until_parked(); @@ -593,7 +939,13 @@ mod tests { // Moving the mouse restores the highlights. cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), true, false, cx); + update_go_to_definition_link( + editor, + Some(GoToDefinitionTrigger::Text(hover_point)), + true, + false, + cx, + ); }); cx.foreground().run_until_parked(); cx.assert_editor_text_highlights::(indoc! {" @@ -607,7 +959,13 @@ mod tests { fn do_work() { tesˇt(); } "}); cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), true, false, cx); + update_go_to_definition_link( + editor, + Some(GoToDefinitionTrigger::Text(hover_point)), + true, + false, + cx, + ); }); cx.foreground().run_until_parked(); cx.assert_editor_text_highlights::(indoc! {" @@ -617,7 +975,7 @@ mod tests { // Cmd click with existing definition doesn't re-request and dismisses highlight cx.update_editor(|editor, cx| { - go_to_fetched_definition(editor, hover_point, false, cx); + go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx); }); // Assert selection moved to to definition cx.lsp @@ -626,6 +984,7 @@ mod tests { // the cached location instead Ok(Some(lsp::GotoDefinitionResponse::Link(vec![]))) }); + cx.foreground().run_until_parked(); cx.assert_editor_state(indoc! {" fn «testˇ»() { do_work(); } fn do_work() { test(); } @@ -658,7 +1017,7 @@ mod tests { ]))) }); cx.update_editor(|editor, cx| { - go_to_fetched_definition(editor, hover_point, false, cx); + go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx); }); requests.next().await; cx.foreground().run_until_parked(); @@ -703,7 +1062,13 @@ mod tests { }); }); cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), true, false, cx); + update_go_to_definition_link( + editor, + Some(GoToDefinitionTrigger::Text(hover_point)), + true, + false, + cx, + ); }); cx.foreground().run_until_parked(); assert!(requests.try_next().is_err()); @@ -713,4 +1078,217 @@ mod tests { "}); cx.foreground().run_until_parked(); } + + #[gpui::test] + async fn test_link_go_to_inlay(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 mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + cx, + ) + .await; + cx.set_state(indoc! {" + struct TestStruct; + + fn main() { + let variableˇ = TestStruct; + } + "}); + let hint_start_offset = cx.ranges(indoc! {" + struct TestStruct; + + fn main() { + let variableˇ = TestStruct; + } + "})[0] + .start; + let hint_position = cx.to_lsp(hint_start_offset); + let target_range = cx.lsp_range(indoc! {" + struct «TestStruct»; + + fn main() { + let variable = TestStruct; + } + "}); + + let expected_uri = cx.buffer_lsp_url.clone(); + let hint_label = ": TestStruct"; + cx.lsp + .handle_request::(move |params, _| { + let expected_uri = expected_uri.clone(); + async move { + assert_eq!(params.text_document.uri, expected_uri); + Ok(Some(vec![lsp::InlayHint { + position: hint_position, + label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart { + value: hint_label.to_string(), + location: Some(lsp::Location { + uri: params.text_document.uri, + range: target_range, + }), + ..Default::default() + }]), + kind: Some(lsp::InlayHintKind::TYPE), + text_edits: None, + tooltip: None, + padding_left: Some(false), + padding_right: Some(false), + data: None, + }])) + } + }) + .next() + .await; + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| { + let expected_layers = vec![hint_label.to_string()]; + assert_eq!(expected_layers, cached_hint_labels(editor)); + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + }); + + let inlay_range = cx + .ranges(indoc! {" + struct TestStruct; + + fn main() { + let variable« »= TestStruct; + } + "}) + .get(0) + .cloned() + .unwrap(); + let hint_hover_position = cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let previous_valid = inlay_range.start.to_display_point(&snapshot); + let next_valid = inlay_range.end.to_display_point(&snapshot); + assert_eq!(previous_valid.row(), next_valid.row()); + assert!(previous_valid.column() < next_valid.column()); + let exact_unclipped = DisplayPoint::new( + previous_valid.row(), + previous_valid.column() + (hint_label.len() / 2) as u32, + ); + PointForPosition { + previous_valid, + next_valid, + exact_unclipped, + column_overshoot_after_line_end: 0, + } + }); + // Press cmd to trigger highlight + cx.update_editor(|editor, cx| { + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + hint_hover_position, + editor, + true, + false, + cx, + ); + }); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let actual_ranges = snapshot + .highlight_ranges::() + .map(|ranges| ranges.as_ref().clone().1) + .unwrap_or_default() + .into_iter() + .map(|range| match range { + DocumentRange::Text(range) => { + panic!("Unexpected regular text selection range {range:?}") + } + DocumentRange::Inlay(inlay_range) => inlay_range, + }) + .collect::>(); + + let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); + let expected_highlight_start = snapshot.display_point_to_inlay_offset( + inlay_range.start.to_display_point(&snapshot), + Bias::Left, + ); + let expected_ranges = vec![InlayRange { + inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), + highlight_start: expected_highlight_start, + highlight_end: InlayOffset(expected_highlight_start.0 + hint_label.len()), + }]; + assert_set_eq!(actual_ranges, expected_ranges); + }); + + // Unpress cmd causes highlight to go away + cx.update_editor(|editor, cx| { + editor.modifiers_changed( + &platform::ModifiersChangedEvent { + modifiers: Modifiers { + cmd: false, + ..Default::default() + }, + ..Default::default() + }, + cx, + ); + }); + // Assert no link highlights + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let actual_ranges = snapshot + .highlight_ranges::() + .map(|ranges| ranges.as_ref().clone().1) + .unwrap_or_default() + .into_iter() + .map(|range| match range { + DocumentRange::Text(range) => { + panic!("Unexpected regular text selection range {range:?}") + } + DocumentRange::Inlay(inlay_range) => inlay_range, + }) + .collect::>(); + + assert!(actual_ranges.is_empty(), "When no cmd is pressed, should have no hint label selected, but got: {actual_ranges:?}"); + }); + + // Cmd+click without existing definition requests and jumps + cx.update_editor(|editor, cx| { + editor.modifiers_changed( + &platform::ModifiersChangedEvent { + modifiers: Modifiers { + cmd: true, + ..Default::default() + }, + ..Default::default() + }, + cx, + ); + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + hint_hover_position, + editor, + true, + false, + cx, + ); + }); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| { + go_to_fetched_type_definition(editor, hint_hover_position, false, cx); + }); + cx.foreground().run_until_parked(); + cx.assert_editor_state(indoc! {" + struct «TestStructˇ»; + + fn main() { + let variable = TestStruct; + } + "}); + } } diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 6b3032b2a35ba9fd46ec145953e626a0f4914f98..def6340e389367c0e483c9648e377d3d92b68c57 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -756,7 +756,8 @@ mod tests { .select_font(family_id, &Default::default()) .unwrap(); - let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndefg\nhijkl\nmn", cx)); + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abc\ndefg\nhijkl\nmn")); let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); multibuffer.push_excerpts( diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 9dd40af8981dab6d82ba4dda237ca46804575519..3ace5adbc7f46a0d616fe2ebad554a62f6e95901 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -6,7 +6,7 @@ use clock::ReplicaId; use collections::{BTreeMap, Bound, HashMap, HashSet}; use futures::{channel::mpsc, SinkExt}; use git::diff::DiffHunk; -use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task}; +use gpui::{AppContext, Entity, ModelContext, ModelHandle}; pub use language::Completion; use language::{ char_kind, @@ -67,7 +67,9 @@ pub enum Event { ExcerptsEdited { ids: Vec, }, - Edited, + Edited { + sigleton_buffer_edited: bool, + }, Reloaded, DiffBaseChanged, LanguageChanged, @@ -788,59 +790,59 @@ impl MultiBuffer { pub fn stream_excerpts_with_context_lines( &mut self, - excerpts: Vec<(ModelHandle, Vec>)>, + buffer: ModelHandle, + ranges: Vec>, context_line_count: u32, cx: &mut ModelContext, - ) -> (Task<()>, mpsc::Receiver>) { + ) -> mpsc::Receiver> { let (mut tx, rx) = mpsc::channel(256); - let task = cx.spawn(|this, mut cx| async move { - for (buffer, ranges) in excerpts { - let (buffer_id, buffer_snapshot) = - buffer.read_with(&cx, |buffer, _| (buffer.remote_id(), buffer.snapshot())); - - let mut excerpt_ranges = Vec::new(); - let mut range_counts = Vec::new(); - cx.background() - .scoped(|scope| { - scope.spawn(async { - let (ranges, counts) = - build_excerpt_ranges(&buffer_snapshot, &ranges, context_line_count); - excerpt_ranges = ranges; - range_counts = counts; - }); - }) - .await; - - let mut ranges = ranges.into_iter(); - let mut range_counts = range_counts.into_iter(); - for excerpt_ranges in excerpt_ranges.chunks(100) { - let excerpt_ids = this.update(&mut cx, |this, cx| { - this.push_excerpts(buffer.clone(), excerpt_ranges.iter().cloned(), cx) + cx.spawn(|this, mut cx| async move { + let (buffer_id, buffer_snapshot) = + buffer.read_with(&cx, |buffer, _| (buffer.remote_id(), buffer.snapshot())); + + let mut excerpt_ranges = Vec::new(); + let mut range_counts = Vec::new(); + cx.background() + .scoped(|scope| { + scope.spawn(async { + let (ranges, counts) = + build_excerpt_ranges(&buffer_snapshot, &ranges, context_line_count); + excerpt_ranges = ranges; + range_counts = counts; }); + }) + .await; - for (excerpt_id, range_count) in - excerpt_ids.into_iter().zip(range_counts.by_ref()) - { - for range in ranges.by_ref().take(range_count) { - let start = Anchor { - buffer_id: Some(buffer_id), - excerpt_id: excerpt_id.clone(), - text_anchor: range.start, - }; - let end = Anchor { - buffer_id: Some(buffer_id), - excerpt_id: excerpt_id.clone(), - text_anchor: range.end, - }; - if tx.send(start..end).await.is_err() { - break; - } + let mut ranges = ranges.into_iter(); + let mut range_counts = range_counts.into_iter(); + for excerpt_ranges in excerpt_ranges.chunks(100) { + let excerpt_ids = this.update(&mut cx, |this, cx| { + this.push_excerpts(buffer.clone(), excerpt_ranges.iter().cloned(), cx) + }); + + for (excerpt_id, range_count) in excerpt_ids.into_iter().zip(range_counts.by_ref()) + { + for range in ranges.by_ref().take(range_count) { + let start = Anchor { + buffer_id: Some(buffer_id), + excerpt_id: excerpt_id.clone(), + text_anchor: range.start, + }; + let end = Anchor { + buffer_id: Some(buffer_id), + excerpt_id: excerpt_id.clone(), + text_anchor: range.end, + }; + if tx.send(start..end).await.is_err() { + break; } } } } - }); - (task, rx) + }) + .detach(); + + rx } pub fn push_excerpts( @@ -1022,7 +1024,9 @@ impl MultiBuffer { old: edit_start..edit_start, new: edit_start..edit_end, }]); - cx.emit(Event::Edited); + cx.emit(Event::Edited { + sigleton_buffer_edited: false, + }); cx.emit(Event::ExcerptsAdded { buffer, predecessor: prev_excerpt_id, @@ -1046,7 +1050,9 @@ impl MultiBuffer { old: 0..prev_len, new: 0..0, }]); - cx.emit(Event::Edited); + cx.emit(Event::Edited { + sigleton_buffer_edited: false, + }); cx.emit(Event::ExcerptsRemoved { ids }); cx.notify(); } @@ -1254,7 +1260,9 @@ impl MultiBuffer { } self.subscriptions.publish_mut(edits); - cx.emit(Event::Edited); + cx.emit(Event::Edited { + sigleton_buffer_edited: false, + }); cx.emit(Event::ExcerptsRemoved { ids }); cx.notify(); } @@ -1315,7 +1323,9 @@ impl MultiBuffer { cx: &mut ModelContext, ) { cx.emit(match event { - language::Event::Edited => Event::Edited, + language::Event::Edited => Event::Edited { + sigleton_buffer_edited: true, + }, language::Event::DirtyChanged => Event::DirtyChanged, language::Event::Saved => Event::Saved, language::Event::FileHandleChanged => Event::FileHandleChanged, @@ -1560,7 +1570,7 @@ impl MultiBuffer { #[cfg(any(test, feature = "test-support"))] impl MultiBuffer { pub fn build_simple(text: &str, cx: &mut gpui::AppContext) -> ModelHandle { - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text)); cx.add_model(|cx| Self::singleton(buffer, cx)) } @@ -1570,7 +1580,7 @@ impl MultiBuffer { ) -> ModelHandle { let multi = cx.add_model(|_| Self::new(0)); for (text, ranges) in excerpts { - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text)); let excerpt_ranges = ranges.into_iter().map(|range| ExcerptRange { context: range, primary: None, @@ -1662,7 +1672,7 @@ impl MultiBuffer { if excerpt_ids.is_empty() || (rng.gen() && excerpt_ids.len() < max_excerpts) { let buffer_handle = if rng.gen() || self.buffers.borrow().is_empty() { let text = RandomCharIter::new(&mut *rng).take(10).collect::(); - buffers.push(cx.add_model(|cx| Buffer::new(0, text, cx))); + buffers.push(cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text))); let buffer = buffers.last().unwrap().read(cx); log::info!( "Creating new buffer {} with text: {:?}", @@ -2756,7 +2766,9 @@ impl MultiBufferSnapshot { // Get the ranges of the innermost pair of brackets. let mut result: Option<(Range, Range)> = None; - let Some(enclosing_bracket_ranges) = self.enclosing_bracket_ranges(range.clone()) else { return None; }; + let Some(enclosing_bracket_ranges) = self.enclosing_bracket_ranges(range.clone()) else { + return None; + }; for (open, close) in enclosing_bracket_ranges { let len = close.end - open.start; @@ -4010,7 +4022,8 @@ mod tests { #[gpui::test] fn test_singleton(cx: &mut AppContext) { - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'a'), cx)); + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(6, 6, 'a'))); let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(buffer.clone(), cx)); let snapshot = multibuffer.read(cx).snapshot(cx); @@ -4037,7 +4050,7 @@ mod tests { #[gpui::test] fn test_remote(cx: &mut AppContext) { - let host_buffer = cx.add_model(|cx| Buffer::new(0, "a", cx)); + let host_buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "a")); let guest_buffer = cx.add_model(|cx| { let state = host_buffer.read(cx).to_proto(); let ops = cx @@ -4068,15 +4081,17 @@ mod tests { #[gpui::test] fn test_excerpt_boundaries_and_clipping(cx: &mut AppContext) { - let buffer_1 = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'a'), cx)); - let buffer_2 = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'g'), cx)); + let buffer_1 = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(6, 6, 'a'))); + let buffer_2 = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(6, 6, 'g'))); let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); let events = Rc::new(RefCell::new(Vec::::new())); multibuffer.update(cx, |_, cx| { let events = events.clone(); cx.subscribe(&multibuffer, move |_, _, event, _| { - if let Event::Edited = event { + if let Event::Edited { .. } = event { events.borrow_mut().push(event.clone()) } }) @@ -4131,7 +4146,17 @@ mod tests { // Adding excerpts emits an edited event. assert_eq!( events.borrow().as_slice(), - &[Event::Edited, Event::Edited, Event::Edited] + &[ + Event::Edited { + sigleton_buffer_edited: false + }, + Event::Edited { + sigleton_buffer_edited: false + }, + Event::Edited { + sigleton_buffer_edited: false + } + ] ); let snapshot = multibuffer.read(cx).snapshot(cx); @@ -4292,8 +4317,10 @@ mod tests { #[gpui::test] fn test_excerpt_events(cx: &mut AppContext) { - let buffer_1 = cx.add_model(|cx| Buffer::new(0, sample_text(10, 3, 'a'), cx)); - let buffer_2 = cx.add_model(|cx| Buffer::new(0, sample_text(10, 3, 'm'), cx)); + let buffer_1 = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(10, 3, 'a'))); + let buffer_2 = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(10, 3, 'm'))); let leader_multibuffer = cx.add_model(|_| MultiBuffer::new(0)); let follower_multibuffer = cx.add_model(|_| MultiBuffer::new(0)); @@ -4310,7 +4337,7 @@ mod tests { excerpts, } => follower.insert_excerpts_with_ids_after(predecessor, buffer, excerpts, cx), Event::ExcerptsRemoved { ids } => follower.remove_excerpts(ids, cx), - Event::Edited => { + Event::Edited { .. } => { *follower_edit_event_count.borrow_mut() += 1; } _ => {} @@ -4398,7 +4425,8 @@ mod tests { #[gpui::test] fn test_push_excerpts_with_context_lines(cx: &mut AppContext) { - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(20, 3, 'a'), cx)); + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(20, 3, 'a'))); let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| { multibuffer.push_excerpts_with_context_lines( @@ -4434,9 +4462,10 @@ mod tests { #[gpui::test] async fn test_stream_excerpts_with_context_lines(cx: &mut TestAppContext) { - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(20, 3, 'a'), cx)); + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(20, 3, 'a'))); let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); - let (task, anchor_ranges) = multibuffer.update(cx, |multibuffer, cx| { + let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| { let snapshot = buffer.read(cx); let ranges = vec![ snapshot.anchor_before(Point::new(3, 2))..snapshot.anchor_before(Point::new(4, 2)), @@ -4444,12 +4473,10 @@ mod tests { snapshot.anchor_before(Point::new(15, 0)) ..snapshot.anchor_before(Point::new(15, 0)), ]; - multibuffer.stream_excerpts_with_context_lines(vec![(buffer.clone(), ranges)], 2, cx) + multibuffer.stream_excerpts_with_context_lines(buffer.clone(), ranges, 2, cx) }); let anchor_ranges = anchor_ranges.collect::>().await; - // Ensure task is finished when stream completes. - task.await; let snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx)); assert_eq!( @@ -4482,7 +4509,7 @@ mod tests { #[gpui::test] fn test_singleton_multibuffer_anchors(cx: &mut AppContext) { - let buffer = cx.add_model(|cx| Buffer::new(0, "abcd", cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abcd")); let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(buffer.clone(), cx)); let old_snapshot = multibuffer.read(cx).snapshot(cx); buffer.update(cx, |buffer, cx| { @@ -4502,8 +4529,8 @@ mod tests { #[gpui::test] fn test_multibuffer_anchors(cx: &mut AppContext) { - let buffer_1 = cx.add_model(|cx| Buffer::new(0, "abcd", cx)); - let buffer_2 = cx.add_model(|cx| Buffer::new(0, "efghi", cx)); + let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abcd")); + let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "efghi")); let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); multibuffer.push_excerpts( @@ -4560,8 +4587,8 @@ mod tests { #[gpui::test] fn test_resolving_anchors_after_replacing_their_excerpts(cx: &mut AppContext) { - let buffer_1 = cx.add_model(|cx| Buffer::new(0, "abcd", cx)); - let buffer_2 = cx.add_model(|cx| Buffer::new(0, "ABCDEFGHIJKLMNOP", cx)); + let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abcd")); + let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "ABCDEFGHIJKLMNOP")); let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); // Create an insertion id in buffer 1 that doesn't exist in buffer 2. @@ -4956,7 +4983,9 @@ mod tests { let base_text = util::RandomCharIter::new(&mut rng) .take(10) .collect::(); - buffers.push(cx.add_model(|cx| Buffer::new(0, base_text, cx))); + buffers.push( + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, base_text)), + ); buffers.last().unwrap() } else { buffers.choose(&mut rng).unwrap() @@ -5297,8 +5326,8 @@ mod tests { fn test_history(cx: &mut AppContext) { cx.set_global(SettingsStore::test(cx)); - let buffer_1 = cx.add_model(|cx| Buffer::new(0, "1234", cx)); - let buffer_2 = cx.add_model(|cx| Buffer::new(0, "5678", cx)); + let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "1234")); + let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "5678")); let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); let group_interval = multibuffer.read(cx).history.group_interval; multibuffer.update(cx, |multibuffer, cx| { diff --git a/crates/editor/src/scroll/autoscroll.rs b/crates/editor/src/scroll/autoscroll.rs index e83e2286b1f4809d777c72257eda0e7471508ccf..ffada50179fa233b12e4a02b4fed6e52bcf137ca 100644 --- a/crates/editor/src/scroll/autoscroll.rs +++ b/crates/editor/src/scroll/autoscroll.rs @@ -65,47 +65,52 @@ impl Editor { self.set_scroll_position(scroll_position, cx); } - let (autoscroll, local) = - if let Some(autoscroll) = self.scroll_manager.autoscroll_request.take() { - autoscroll - } else { - return false; - }; - - let first_cursor_top; - let last_cursor_bottom; + let Some((autoscroll, local)) = self.scroll_manager.autoscroll_request.take() else { + return false; + }; + + let mut target_top; + let mut target_bottom; if let Some(highlighted_rows) = &self.highlighted_rows { - first_cursor_top = highlighted_rows.start as f32; - last_cursor_bottom = first_cursor_top + 1.; - } else if autoscroll == Autoscroll::newest() { - let newest_selection = self.selections.newest::(cx); - first_cursor_top = newest_selection.head().to_display_point(&display_map).row() as f32; - last_cursor_bottom = first_cursor_top + 1.; + target_top = highlighted_rows.start as f32; + target_bottom = target_top + 1.; } else { let selections = self.selections.all::(cx); - first_cursor_top = selections + target_top = selections .first() .unwrap() .head() .to_display_point(&display_map) .row() as f32; - last_cursor_bottom = selections + target_bottom = selections .last() .unwrap() .head() .to_display_point(&display_map) .row() as f32 + 1.0; + + // If the selections can't all fit on screen, scroll to the newest. + if autoscroll == Autoscroll::newest() + || autoscroll == Autoscroll::fit() && target_bottom - target_top > visible_lines + { + let newest_selection_top = selections + .iter() + .max_by_key(|s| s.id) + .unwrap() + .head() + .to_display_point(&display_map) + .row() as f32; + target_top = newest_selection_top; + target_bottom = newest_selection_top + 1.; + } } let margin = if matches!(self.mode, EditorMode::AutoHeight { .. }) { 0. } else { - ((visible_lines - (last_cursor_bottom - first_cursor_top)) / 2.0).floor() + ((visible_lines - (target_bottom - target_top)) / 2.0).floor() }; - if margin < 0.0 { - return false; - } let strategy = match autoscroll { Autoscroll::Strategy(strategy) => strategy, @@ -113,8 +118,8 @@ impl Editor { let last_autoscroll = &self.scroll_manager.last_autoscroll; if let Some(last_autoscroll) = last_autoscroll { if self.scroll_manager.anchor.offset == last_autoscroll.0 - && first_cursor_top == last_autoscroll.1 - && last_cursor_bottom == last_autoscroll.2 + && target_top == last_autoscroll.1 + && target_bottom == last_autoscroll.2 { last_autoscroll.3.next() } else { @@ -129,37 +134,41 @@ impl Editor { match strategy { AutoscrollStrategy::Fit | AutoscrollStrategy::Newest => { let margin = margin.min(self.scroll_manager.vertical_scroll_margin); - let target_top = (first_cursor_top - margin).max(0.0); - let target_bottom = last_cursor_bottom + margin; + let target_top = (target_top - margin).max(0.0); + let target_bottom = target_bottom + margin; let start_row = scroll_position.y(); let end_row = start_row + visible_lines; - if target_top < start_row { + let needs_scroll_up = target_top < start_row; + let needs_scroll_down = target_bottom >= end_row; + + if needs_scroll_up && !needs_scroll_down { scroll_position.set_y(target_top); self.set_scroll_position_internal(scroll_position, local, true, cx); - } else if target_bottom >= end_row { + } + if !needs_scroll_up && needs_scroll_down { scroll_position.set_y(target_bottom - visible_lines); self.set_scroll_position_internal(scroll_position, local, true, cx); } } AutoscrollStrategy::Center => { - scroll_position.set_y((first_cursor_top - margin).max(0.0)); + scroll_position.set_y((target_top - margin).max(0.0)); self.set_scroll_position_internal(scroll_position, local, true, cx); } AutoscrollStrategy::Top => { - scroll_position.set_y((first_cursor_top).max(0.0)); + scroll_position.set_y((target_top).max(0.0)); self.set_scroll_position_internal(scroll_position, local, true, cx); } AutoscrollStrategy::Bottom => { - scroll_position.set_y((last_cursor_bottom - visible_lines).max(0.0)); + scroll_position.set_y((target_bottom - visible_lines).max(0.0)); self.set_scroll_position_internal(scroll_position, local, true, cx); } } self.scroll_manager.last_autoscroll = Some(( self.scroll_manager.anchor.offset, - first_cursor_top, - last_cursor_bottom, + target_top, + target_bottom, strategy, )); diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index 118cddaa9226a543ca479f577428237d77539d5d..033525395e17f0db865fff79c225338f282ec889 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -225,6 +225,7 @@ impl<'a> EditorTestContext<'a> { .map(|h| h.1.clone()) .unwrap_or_default() .into_iter() + .filter_map(|range| range.as_text_range()) .map(|range| range.to_offset(&snapshot.buffer_snapshot)) .collect() }); @@ -240,6 +241,7 @@ impl<'a> EditorTestContext<'a> { .map(|ranges| ranges.as_ref().clone().1) .unwrap_or_default() .into_iter() + .filter_map(|range| range.as_text_range()) .map(|range| range.to_offset(&snapshot.buffer_snapshot)) .collect(); assert_set_eq!(actual_ranges, expected_ranges); diff --git a/crates/staff_mode/Cargo.toml b/crates/feature_flags/Cargo.toml similarity index 71% rename from crates/staff_mode/Cargo.toml rename to crates/feature_flags/Cargo.toml index 2193bd11b127d94840ed22c1bd7d4e0fb2b8310b..af273fe4033c7fbca36df2ccc8a2daae86eec19b 100644 --- a/crates/staff_mode/Cargo.toml +++ b/crates/feature_flags/Cargo.toml @@ -1,11 +1,11 @@ [package] -name = "staff_mode" +name = "feature_flags" version = "0.1.0" edition = "2021" publish = false [lib] -path = "src/staff_mode.rs" +path = "src/feature_flags.rs" [dependencies] gpui = { path = "../gpui" } diff --git a/crates/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs new file mode 100644 index 0000000000000000000000000000000000000000..d14152b04c6155b37091adabd32ab68bcdbf6cdd --- /dev/null +++ b/crates/feature_flags/src/feature_flags.rs @@ -0,0 +1,79 @@ +use gpui::{AppContext, Subscription, ViewContext}; + +#[derive(Default)] +struct FeatureFlags { + flags: Vec, + staff: bool, +} + +impl FeatureFlags { + fn has_flag(&self, flag: &str) -> bool { + self.staff || self.flags.iter().find(|f| f.as_str() == flag).is_some() + } +} + +pub trait FeatureFlag { + const NAME: &'static str; +} + +pub enum ChannelsAlpha {} + +impl FeatureFlag for ChannelsAlpha { + const NAME: &'static str = "channels_alpha"; +} + +pub trait FeatureFlagViewExt { + fn observe_flag(&mut self, callback: F) -> Subscription + where + F: Fn(bool, &mut V, &mut ViewContext) + 'static; +} + +impl FeatureFlagViewExt for ViewContext<'_, '_, V> { + fn observe_flag(&mut self, callback: F) -> Subscription + where + F: Fn(bool, &mut V, &mut ViewContext) + 'static, + { + self.observe_global::(move |v, cx| { + let feature_flags = cx.global::(); + callback(feature_flags.has_flag(::NAME), v, cx); + }) + } +} + +pub trait FeatureFlagAppExt { + fn update_flags(&mut self, staff: bool, flags: Vec); + fn set_staff(&mut self, staff: bool); + fn has_flag(&self) -> bool; + fn is_staff(&self) -> bool; +} + +impl FeatureFlagAppExt for AppContext { + fn update_flags(&mut self, staff: bool, flags: Vec) { + self.update_default_global::(|feature_flags, _| { + feature_flags.staff = staff; + feature_flags.flags = flags; + }) + } + + fn set_staff(&mut self, staff: bool) { + self.update_default_global::(|feature_flags, _| { + feature_flags.staff = staff; + }) + } + + fn has_flag(&self) -> bool { + if self.has_global::() { + self.global::().has_flag(T::NAME) + } else { + false + } + } + + fn is_staff(&self) -> bool { + if self.has_global::() { + return self.global::().staff; + } else { + false + } + } +} diff --git a/crates/gpui/examples/components.rs b/crates/gpui/examples/components.rs index ad38b5893c48a831c245ed61300d9c92d7319383..d3ca0d1eccaef343058968856a0e8715849357a8 100644 --- a/crates/gpui/examples/components.rs +++ b/crates/gpui/examples/components.rs @@ -2,7 +2,7 @@ use button_component::Button; use gpui::{ color::Color, - elements::{Component, ContainerStyle, Flex, Label, ParentElement}, + elements::{ContainerStyle, Flex, Label, ParentElement, StatefulComponent}, fonts::{self, TextStyle}, platform::WindowOptions, AnyElement, App, Element, Entity, View, ViewContext, @@ -114,7 +114,7 @@ mod theme { // Component creation: mod toggleable_button { use gpui::{ - elements::{Component, ContainerStyle, LabelStyle}, + elements::{ContainerStyle, LabelStyle, StatefulComponent}, scene::MouseClick, EventContext, View, }; @@ -156,7 +156,7 @@ mod toggleable_button { } } - impl Component for ToggleableButton { + impl StatefulComponent for ToggleableButton { fn render(self, v: &mut V, cx: &mut gpui::ViewContext) -> gpui::AnyElement { let button = if let Some(style) = self.style { self.button.with_style(*style.style_for(self.active)) @@ -171,7 +171,7 @@ mod toggleable_button { mod button_component { use gpui::{ - elements::{Component, ContainerStyle, Label, LabelStyle, MouseEventHandler}, + elements::{ContainerStyle, Label, LabelStyle, MouseEventHandler, StatefulComponent}, platform::MouseButton, scene::MouseClick, AnyElement, Element, EventContext, TypeTag, View, ViewContext, @@ -212,7 +212,7 @@ mod button_component { } } - impl Component for Button { + impl StatefulComponent for Button { fn render(self, _: &mut V, cx: &mut ViewContext) -> AnyElement { let click_handler = self.click_handler; diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 4750c71ea2f58c9832cda6a8fa59a005a882aa19..db6c19405573018fb509926196d035c8f4642399 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -3299,15 +3299,15 @@ impl<'a, 'b, V: 'static> ViewContext<'a, 'b, V> { let region_id = MouseRegionId::new(tag, self.view_id, region_id); MouseState { hovered: self.window.hovered_region_ids.contains(®ion_id), - clicked: if let Some((clicked_region_id, button)) = self.window.clicked_region { - if region_id == clicked_region_id { - Some(button) - } else { - None - } - } else { - None - }, + mouse_down: !self.window.clicked_region_ids.is_empty(), + clicked: self + .window + .clicked_region_ids + .iter() + .find(|click_region_id| **click_region_id == region_id) + // If we've gotten here, there should always be a clicked region. + // But let's be defensive and return None if there isn't. + .and_then(|_| self.window.clicked_region.map(|(_, button)| button)), accessed_hovered: false, accessed_clicked: false, } @@ -3798,14 +3798,20 @@ impl<'a, T> DerefMut for Reference<'a, T> { pub struct MouseState { pub(crate) hovered: bool, pub(crate) clicked: Option, + pub(crate) mouse_down: bool, pub(crate) accessed_hovered: bool, pub(crate) accessed_clicked: bool, } impl MouseState { + pub fn dragging(&mut self) -> bool { + self.accessed_hovered = true; + self.hovered && self.mouse_down + } + pub fn hovered(&mut self) -> bool { self.accessed_hovered = true; - self.hovered + self.hovered && (!self.mouse_down || self.clicked.is_some()) } pub fn clicked(&mut self) -> Option { @@ -4656,12 +4662,13 @@ impl AnyWeakModelHandle { } } -#[derive(Copy)] pub struct WeakViewHandle { any_handle: AnyWeakViewHandle, view_type: PhantomData, } +impl Copy for WeakViewHandle {} + impl Debug for WeakViewHandle { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct(&format!("WeakViewHandle<{}>", type_name::())) diff --git a/crates/gpui/src/app/window.rs b/crates/gpui/src/app/window.rs index e1811b6655b8e068fe8e9c962dd6b0e573460f24..f912ee46f56a9432d02949e6a070f176da410f62 100644 --- a/crates/gpui/src/app/window.rs +++ b/crates/gpui/src/app/window.rs @@ -624,10 +624,11 @@ impl<'a> WindowContext<'a> { } } - if self - .window - .platform_window - .is_topmost_for_position(*position) + if pressed_button.is_none() + && self + .window + .platform_window + .is_topmost_for_position(*position) { self.platform().set_cursor_style(style_to_assign); } @@ -791,6 +792,11 @@ impl<'a> WindowContext<'a> { if clicked_region_ids.contains(&mouse_region.id()) { if mouse_region.bounds.contains_point(self.mouse_position()) { valid_regions.push(mouse_region.clone()); + } else { + // Let the view know that it hasn't been clicked anymore + if mouse_region.notify_on_click { + notified_views.insert(mouse_region.id().view_id()); + } } } } diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index bde15068ef074bb5e1151001766db0c548d1fd6f..ca0a0b3586e279f7fa2c8308e0f1a00bf3e68bee 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -234,6 +234,27 @@ pub trait Element: 'static { { MouseEventHandler::for_child::(self.into_any(), region_id) } + + fn component(self) -> StatelessElementAdapter + where + Self: Sized, + { + StatelessElementAdapter::new(self.into_any()) + } + + fn stateful_component(self) -> StatefulElementAdapter + where + Self: Sized, + { + StatefulElementAdapter::new(self.into_any()) + } + + fn styleable_component(self) -> StylableAdapter + where + Self: Sized, + { + StatelessElementAdapter::new(self.into_any()).stylable() + } } trait AnyElementState { diff --git a/crates/gpui/src/elements/component.rs b/crates/gpui/src/elements/component.rs index e2770c014859c902cc86fe1f04590f81595c94c5..c8800d18a16aafde2a8841bbd60a640f907f981e 100644 --- a/crates/gpui/src/elements/component.rs +++ b/crates/gpui/src/elements/component.rs @@ -1,47 +1,96 @@ -use std::marker::PhantomData; +use std::{any::Any, marker::PhantomData}; use pathfinder_geometry::{rect::RectF, vector::Vector2F}; use crate::{ - AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View, - ViewContext, + AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, ViewContext, }; use super::Empty; -pub trait GeneralComponent { - fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement; - fn element(self) -> ComponentAdapter +/// The core stateless component trait, simply rendering an element tree +pub trait Component { + fn render(self, cx: &mut ViewContext) -> AnyElement; + + fn element(self) -> ComponentAdapter where Self: Sized, { ComponentAdapter::new(self) } + + fn stylable(self) -> StylableAdapter + where + Self: Sized, + { + StylableAdapter::new(self) + } + + fn stateful(self) -> StatefulAdapter + where + Self: Sized, + { + StatefulAdapter::new(self) + } } -pub trait StyleableComponent { +/// Allows a a component's styles to be rebound in a simple way. +pub trait Stylable: Component { type Style: Clone; - type Output: GeneralComponent; + + fn with_style(self, style: Self::Style) -> Self; +} + +/// This trait models the typestate pattern for a component's style, +/// enforcing at compile time that a component is only usable after +/// it has been styled while still allowing for late binding of the +/// styling information +pub trait SafeStylable { + type Style: Clone; + type Output: Component; fn with_style(self, style: Self::Style) -> Self::Output; } -impl GeneralComponent for () { - fn render(self, _: &mut V, _: &mut ViewContext) -> AnyElement { - Empty::new().into_any() +/// All stylable components can trivially implement SafeStylable +impl SafeStylable for C { + type Style = C::Style; + + type Output = C; + + fn with_style(self, style: Self::Style) -> Self::Output { + self.with_style(style) + } +} + +/// Allows converting an unstylable component into a stylable one +/// by using `()` as the style type +pub struct StylableAdapter { + component: C, +} + +impl StylableAdapter { + pub fn new(component: C) -> Self { + Self { component } } } -impl StyleableComponent for () { +impl SafeStylable for StylableAdapter { type Style = (); - type Output = (); + + type Output = C; fn with_style(self, _: Self::Style) -> Self::Output { - () + self.component } } -pub trait Component { +/// This is a secondary trait for components that can be styled +/// which rely on their view's state. This is useful for components that, for example, +/// want to take click handler callbacks Unfortunately, the generic bound on the +/// Component trait makes it incompatible with the stateless components above. +// So let's just replicate them for now +pub trait StatefulComponent { fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement; fn element(self) -> ComponentAdapter @@ -50,21 +99,63 @@ pub trait Component { { ComponentAdapter::new(self) } + + fn styleable(self) -> StatefulStylableAdapter + where + Self: Sized, + { + StatefulStylableAdapter::new(self) + } + + fn stateless(self) -> StatelessElementAdapter + where + Self: Sized + 'static, + { + StatelessElementAdapter::new(self.element().into_any()) + } } -impl Component for C { - fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement { - self.render(v, cx) +/// It is trivial to convert stateless components to stateful components, so lets +/// do so en masse. Note that the reverse is impossible without a helper. +impl StatefulComponent for C { + fn render(self, _: &mut V, cx: &mut ViewContext) -> AnyElement { + self.render(cx) } } -// StylableComponent -> GeneralComponent -pub struct StylableComponentAdapter, V: View> { +/// Same as stylable, but generic over a view type +pub trait StatefulStylable: StatefulComponent { + type Style: Clone; + + fn with_style(self, style: Self::Style) -> Self; +} + +/// Same as SafeStylable, but generic over a view type +pub trait StatefulSafeStylable { + type Style: Clone; + type Output: StatefulComponent; + + fn with_style(self, style: Self::Style) -> Self::Output; +} + +/// Converting from stateless to stateful +impl StatefulSafeStylable for C { + type Style = C::Style; + + type Output = C::Output; + + fn with_style(self, style: Self::Style) -> Self::Output { + self.with_style(style) + } +} + +// A helper for converting stateless components into stateful ones +pub struct StatefulAdapter { component: C, phantom: std::marker::PhantomData, } -impl, V: View> StylableComponentAdapter { +impl StatefulAdapter { pub fn new(component: C) -> Self { Self { component, @@ -73,7 +164,31 @@ impl, V: View> StylableComponentAdapter { } } -impl StyleableComponent for StylableComponentAdapter { +impl StatefulComponent for StatefulAdapter { + fn render(self, _: &mut V, cx: &mut ViewContext) -> AnyElement { + self.component.render(cx) + } +} + +// A helper for converting stateful but style-less components into stylable ones +// by using `()` as the style type +pub struct StatefulStylableAdapter, V: 'static> { + component: C, + phantom: std::marker::PhantomData, +} + +impl, V: 'static> StatefulStylableAdapter { + pub fn new(component: C) -> Self { + Self { + component, + phantom: std::marker::PhantomData, + } + } +} + +impl, V: 'static> StatefulSafeStylable + for StatefulStylableAdapter +{ type Style = (); type Output = C; @@ -83,13 +198,37 @@ impl StyleableComponent for StylableComponentAdapt } } -// Element -> Component -pub struct ElementAdapter { +/// A way of erasing the view generic from an element, useful +/// for wrapping up an explicit element tree into stateless +/// components +pub struct StatelessElementAdapter { + element: Box, +} + +impl StatelessElementAdapter { + pub fn new(element: AnyElement) -> Self { + StatelessElementAdapter { + element: Box::new(element) as Box, + } + } +} + +impl Component for StatelessElementAdapter { + fn render(self, _: &mut ViewContext) -> AnyElement { + *self + .element + .downcast::>() + .expect("Don't move elements out of their view :(") + } +} + +// For converting elements into stateful components +pub struct StatefulElementAdapter { element: AnyElement, _phantom: std::marker::PhantomData, } -impl ElementAdapter { +impl StatefulElementAdapter { pub fn new(element: AnyElement) -> Self { Self { element, @@ -98,20 +237,35 @@ impl ElementAdapter { } } -impl Component for ElementAdapter { +impl StatefulComponent for StatefulElementAdapter { fn render(self, _: &mut V, _: &mut ViewContext) -> AnyElement { self.element } } -// Component -> Element -pub struct ComponentAdapter { +/// A convenient shorthand for creating an empty component. +impl Component for () { + fn render(self, _: &mut ViewContext) -> AnyElement { + Empty::new().into_any() + } +} + +impl Stylable for () { + type Style = (); + + fn with_style(self, _: Self::Style) -> Self { + () + } +} + +// For converting components back into Elements +pub struct ComponentAdapter { component: Option, element: Option>, phantom: PhantomData, } -impl ComponentAdapter { +impl ComponentAdapter { pub fn new(e: E) -> Self { Self { component: Some(e), @@ -121,7 +275,7 @@ impl ComponentAdapter { } } -impl + 'static> Element for ComponentAdapter { +impl + 'static> Element for ComponentAdapter { type LayoutState = (); type PaintState = (); @@ -184,6 +338,7 @@ impl + 'static> Element for ComponentAdapter { ) -> serde_json::Value { serde_json::json!({ "type": "ComponentAdapter", + "component": std::any::type_name::(), "child": self.element.as_ref().map(|el| el.debug(view, cx)), }) } diff --git a/crates/gpui/src/elements/container.rs b/crates/gpui/src/elements/container.rs index 03c2f1d046462a15b92bbb539b9715e8362d9895..9a590f3a5a5bedae21273011f237a2cce0c9eea3 100644 --- a/crates/gpui/src/elements/container.rs +++ b/crates/gpui/src/elements/container.rs @@ -44,6 +44,14 @@ impl ContainerStyle { ..Default::default() } } + + pub fn additional_length(&self) -> f32 { + self.padding.left + + self.padding.right + + self.border.width * 2. + + self.margin.left + + self.margin.right + } } pub struct Container { diff --git a/crates/gpui/src/elements/flex.rs b/crates/gpui/src/elements/flex.rs index d9c43ca4c818a2ba7ad453f58106253aee328174..80dfb0625cd581407801148a5df7ee2046eae596 100644 --- a/crates/gpui/src/elements/flex.rs +++ b/crates/gpui/src/elements/flex.rs @@ -22,6 +22,7 @@ pub struct Flex { children: Vec>, scroll_state: Option<(ElementStateHandle>, usize)>, child_alignment: f32, + spacing: f32, } impl Flex { @@ -31,6 +32,7 @@ impl Flex { children: Default::default(), scroll_state: None, child_alignment: -1., + spacing: 0., } } @@ -51,6 +53,11 @@ impl Flex { self } + pub fn with_spacing(mut self, spacing: f32) -> Self { + self.spacing = spacing; + self + } + pub fn scrollable( mut self, element_id: usize, @@ -81,7 +88,7 @@ impl Flex { cx: &mut LayoutContext, ) { let cross_axis = self.axis.invert(); - for child in &mut self.children { + for child in self.children.iter_mut() { if let Some(metadata) = child.metadata::() { if let Some((flex, expanded)) = metadata.flex { if expanded != layout_expanded { @@ -132,12 +139,12 @@ impl Element for Flex { cx: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState) { let mut total_flex = None; - let mut fixed_space = 0.0; + let mut fixed_space = self.children.len().saturating_sub(1) as f32 * self.spacing; let mut contains_float = false; let cross_axis = self.axis.invert(); let mut cross_axis_max: f32 = 0.0; - for child in &mut self.children { + for child in self.children.iter_mut() { let metadata = child.metadata::(); contains_float |= metadata.map_or(false, |metadata| metadata.float); @@ -315,7 +322,7 @@ impl Element for Flex { } } - for child in &mut self.children { + for child in self.children.iter_mut() { if remaining_space > 0. { if let Some(metadata) = child.metadata::() { if metadata.float { @@ -354,8 +361,8 @@ impl Element for Flex { child.paint(scene, aligned_child_origin, visible_bounds, view, cx); match self.axis { - Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0), - Axis::Vertical => child_origin += vec2f(0.0, child.size().y()), + Axis::Horizontal => child_origin += vec2f(child.size().x() + self.spacing, 0.0), + Axis::Vertical => child_origin += vec2f(0.0, child.size().y() + self.spacing), } } diff --git a/crates/gpui/src/keymap_matcher/keymap_context.rs b/crates/gpui/src/keymap_matcher/keymap_context.rs index fd60a8f4b5d385eb94b7edf0bfeb407a9dce8c20..d9c54dbc8e6fe71a7567ac4d6908a2d53689943d 100644 --- a/crates/gpui/src/keymap_matcher/keymap_context.rs +++ b/crates/gpui/src/keymap_matcher/keymap_context.rs @@ -67,7 +67,9 @@ impl KeymapContextPredicate { } pub fn eval(&self, contexts: &[KeymapContext]) -> bool { - let Some(context) = contexts.first() else { return false }; + let Some(context) = contexts.first() else { + return false; + }; match self { Self::Identifier(name) => (&context.set).contains(name.as_str()), Self::Equal(left, right) => context diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index d032e8e0253c81681f3c9c11576185f5f04ce5e9..902ed26b57471dce6a9dadcdca0a00dd0a8b9051 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -14,7 +14,7 @@ use crate::{ CodeLabel, LanguageScope, Outline, }; use anyhow::{anyhow, Result}; -use clock::ReplicaId; +pub use clock::ReplicaId; use fs::LineEnding; use futures::FutureExt as _; use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, Task}; @@ -347,13 +347,17 @@ impl CharKind { } impl Buffer { - pub fn new>( - replica_id: ReplicaId, - base_text: T, - cx: &mut ModelContext, - ) -> Self { + pub fn new>(replica_id: ReplicaId, id: u64, base_text: T) -> Self { Self::build( - TextBuffer::new(replica_id, cx.model_id() as u64, base_text.into()), + TextBuffer::new(replica_id, id, base_text.into()), + None, + None, + ) + } + + pub fn remote(remote_id: u64, replica_id: ReplicaId, base_text: String) -> Self { + Self::build( + TextBuffer::new(replica_id, remote_id, base_text), None, None, ) @@ -2476,7 +2480,9 @@ impl BufferSnapshot { matches.advance(); - let Some((open, close)) = open.zip(close) else { continue }; + let Some((open, close)) = open.zip(close) else { + continue; + }; let bracket_range = open.start..=close.end; if !bracket_range.overlaps(&range) { diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 9d4b9c38fe287596144fecc731bd59398ec10c0b..db3749aa251517c690c49d25167a640534941a21 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -43,8 +43,8 @@ fn test_line_endings(cx: &mut gpui::AppContext) { init_settings(cx, |_| {}); cx.add_model(|cx| { - let mut buffer = - Buffer::new(0, "one\r\ntwo\rthree", cx).with_language(Arc::new(rust_lang()), cx); + let mut buffer = Buffer::new(0, cx.model_id() as u64, "one\r\ntwo\rthree") + .with_language(Arc::new(rust_lang()), cx); assert_eq!(buffer.text(), "one\ntwo\nthree"); assert_eq!(buffer.line_ending(), LineEnding::Windows); @@ -138,8 +138,8 @@ fn test_edit_events(cx: &mut gpui::AppContext) { let buffer_1_events = Rc::new(RefCell::new(Vec::new())); let buffer_2_events = Rc::new(RefCell::new(Vec::new())); - let buffer1 = cx.add_model(|cx| Buffer::new(0, "abcdef", cx)); - let buffer2 = cx.add_model(|cx| Buffer::new(1, "abcdef", cx)); + let buffer1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abcdef")); + let buffer2 = cx.add_model(|cx| Buffer::new(1, cx.model_id() as u64, "abcdef")); let buffer1_ops = Rc::new(RefCell::new(Vec::new())); buffer1.update(cx, { let buffer1_ops = buffer1_ops.clone(); @@ -222,7 +222,7 @@ fn test_edit_events(cx: &mut gpui::AppContext) { #[gpui::test] async fn test_apply_diff(cx: &mut gpui::TestAppContext) { let text = "a\nbb\nccc\ndddd\neeeee\nffffff\n"; - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text)); let anchor = buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(3, 3))); let text = "a\nccc\ndddd\nffffff\n"; @@ -254,7 +254,7 @@ async fn test_normalize_whitespace(cx: &mut gpui::TestAppContext) { ] .join("\n"); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text)); // Spawn a task to format the buffer's whitespace. // Pause so that the foratting task starts running. @@ -318,8 +318,9 @@ async fn test_normalize_whitespace(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_reparse(cx: &mut gpui::TestAppContext) { let text = "fn a() {}"; - let buffer = - cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.add_model(|cx| { + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx) + }); // Wait for the initial text to parse buffer.condition(cx, |buffer, _| !buffer.is_parsing()).await; @@ -443,7 +444,8 @@ async fn test_reparse(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_resetting_language(cx: &mut gpui::TestAppContext) { let buffer = cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "{}", cx).with_language(Arc::new(rust_lang()), cx); + let mut buffer = + Buffer::new(0, cx.model_id() as u64, "{}").with_language(Arc::new(rust_lang()), cx); buffer.set_sync_parse_timeout(Duration::ZERO); buffer }); @@ -491,8 +493,9 @@ async fn test_outline(cx: &mut gpui::TestAppContext) { "# .unindent(); - let buffer = - cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.add_model(|cx| { + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx) + }); let outline = buffer .read_with(cx, |buffer, _| buffer.snapshot().outline(None)) .unwrap(); @@ -576,8 +579,9 @@ async fn test_outline_nodes_with_newlines(cx: &mut gpui::TestAppContext) { "# .unindent(); - let buffer = - cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.add_model(|cx| { + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx) + }); let outline = buffer .read_with(cx, |buffer, _| buffer.snapshot().outline(None)) .unwrap(); @@ -613,7 +617,9 @@ async fn test_outline_with_extra_context(cx: &mut gpui::TestAppContext) { "# .unindent(); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(language), cx)); + let buffer = cx.add_model(|cx| { + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(language), cx) + }); let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); // extra context nodes are included in the outline. @@ -655,8 +661,9 @@ async fn test_symbols_containing(cx: &mut gpui::TestAppContext) { "# .unindent(); - let buffer = - cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.add_model(|cx| { + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx) + }); let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); // point is at the start of an item @@ -877,7 +884,8 @@ fn test_enclosing_bracket_ranges_where_brackets_are_not_outermost_children(cx: & fn test_range_for_syntax_ancestor(cx: &mut AppContext) { cx.add_model(|cx| { let text = "fn a() { b(|c| {}) }"; - let buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); + let buffer = + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); let snapshot = buffer.snapshot(); assert_eq!( @@ -917,7 +925,8 @@ fn test_autoindent_with_soft_tabs(cx: &mut AppContext) { cx.add_model(|cx| { let text = "fn a() {}"; - let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); + let mut buffer = + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "fn a() {\n \n}"); @@ -959,7 +968,8 @@ fn test_autoindent_with_hard_tabs(cx: &mut AppContext) { cx.add_model(|cx| { let text = "fn a() {}"; - let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); + let mut buffer = + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "fn a() {\n\t\n}"); @@ -1000,6 +1010,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppC cx.add_model(|cx| { let mut buffer = Buffer::new( 0, + cx.model_id() as u64, " fn a() { c; @@ -1007,7 +1018,6 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppC } " .unindent(), - cx, ) .with_language(Arc::new(rust_lang()), cx); @@ -1073,6 +1083,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppC cx.add_model(|cx| { let mut buffer = Buffer::new( 0, + cx.model_id() as u64, " fn a() { b(); @@ -1080,7 +1091,6 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppC " .replace("|", "") // marker to preserve trailing whitespace .unindent(), - cx, ) .with_language(Arc::new(rust_lang()), cx); @@ -1136,13 +1146,13 @@ fn test_autoindent_does_not_adjust_lines_within_newly_created_errors(cx: &mut Ap cx.add_model(|cx| { let mut buffer = Buffer::new( 0, + cx.model_id() as u64, " fn a() { i } " .unindent(), - cx, ) .with_language(Arc::new(rust_lang()), cx); @@ -1198,11 +1208,11 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut AppContext) { cx.add_model(|cx| { let mut buffer = Buffer::new( 0, + cx.model_id() as u64, " fn a() {} " .unindent(), - cx, ) .with_language(Arc::new(rust_lang()), cx); @@ -1254,7 +1264,8 @@ fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut AppContext) { cx.add_model(|cx| { let text = "a\nb"; - let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); + let mut buffer = + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); buffer.edit( [(0..1, "\n"), (2..3, "\n")], Some(AutoindentMode::EachLine), @@ -1280,7 +1291,8 @@ fn test_autoindent_multi_line_insertion(cx: &mut AppContext) { " .unindent(); - let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); + let mut buffer = + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); buffer.edit( [(Point::new(3, 0)..Point::new(3, 0), "e(\n f()\n);\n")], Some(AutoindentMode::EachLine), @@ -1317,7 +1329,8 @@ fn test_autoindent_block_mode(cx: &mut AppContext) { } "# .unindent(); - let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); + let mut buffer = + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); // When this text was copied, both of the quotation marks were at the same // indent level, but the indentation of the first line was not included in @@ -1402,7 +1415,8 @@ fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut AppContex } "# .unindent(); - let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); + let mut buffer = + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); // The original indent columns are not known, so this text is // auto-indented in a block as if the first line was copied in @@ -1481,7 +1495,7 @@ fn test_autoindent_language_without_indents_query(cx: &mut AppContext) { " .unindent(); - let mut buffer = Buffer::new(0, text, cx).with_language( + let mut buffer = Buffer::new(0, cx.model_id() as u64, text).with_language( Arc::new(Language::new( LanguageConfig { name: "Markdown".into(), @@ -1557,7 +1571,7 @@ fn test_autoindent_with_injected_languages(cx: &mut AppContext) { false, ); - let mut buffer = Buffer::new(0, text, cx); + let mut buffer = Buffer::new(0, cx.model_id() as u64, text); buffer.set_language_registry(language_registry); buffer.set_language(Some(html_language), cx); buffer.edit( @@ -1593,7 +1607,8 @@ fn test_autoindent_query_with_outdent_captures(cx: &mut AppContext) { }); cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(ruby_lang()), cx); + let mut buffer = + Buffer::new(0, cx.model_id() as u64, "").with_language(Arc::new(ruby_lang()), cx); let text = r#" class C @@ -1683,7 +1698,8 @@ fn test_language_scope_at_with_javascript(cx: &mut AppContext) { let text = r#"a["b"] = ;"#; - let buffer = Buffer::new(0, text, cx).with_language(Arc::new(language), cx); + let buffer = + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(language), cx); let snapshot = buffer.snapshot(); let config = snapshot.language_scope_at(0).unwrap(); @@ -1762,7 +1778,8 @@ fn test_language_scope_at_with_rust(cx: &mut AppContext) { "# .unindent(); - let buffer = Buffer::new(0, text.clone(), cx).with_language(Arc::new(language), cx); + let buffer = Buffer::new(0, cx.model_id() as u64, text.clone()) + .with_language(Arc::new(language), cx); let snapshot = buffer.snapshot(); // By default, all brackets are enabled @@ -1806,7 +1823,7 @@ fn test_language_scope_at_with_combined_injections(cx: &mut AppContext) { language_registry.add(Arc::new(html_lang())); language_registry.add(Arc::new(erb_lang())); - let mut buffer = Buffer::new(0, text, cx); + let mut buffer = Buffer::new(0, cx.model_id() as u64, text); buffer.set_language_registry(language_registry.clone()); buffer.set_language( language_registry @@ -1838,7 +1855,7 @@ fn test_serialization(cx: &mut gpui::AppContext) { let mut now = Instant::now(); let buffer1 = cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "abc", cx); + let mut buffer = Buffer::new(0, cx.model_id() as u64, "abc"); buffer.edit([(3..3, "D")], None, cx); now += Duration::from_secs(1); @@ -1893,7 +1910,7 @@ fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) { let mut replica_ids = Vec::new(); let mut buffers = Vec::new(); let network = Rc::new(RefCell::new(Network::new(rng.clone()))); - let base_buffer = cx.add_model(|cx| Buffer::new(0, base_text.as_str(), cx)); + let base_buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, base_text.as_str())); for i in 0..rng.gen_range(min_peers..=max_peers) { let buffer = cx.add_model(|cx| { @@ -2394,7 +2411,8 @@ fn assert_bracket_pairs( ) { let (expected_text, selection_ranges) = marked_text_ranges(selection_text, false); let buffer = cx.add_model(|cx| { - Buffer::new(0, expected_text.clone(), cx).with_language(Arc::new(language), cx) + Buffer::new(0, cx.model_id() as u64, expected_text.clone()) + .with_language(Arc::new(language), cx) }); let buffer = buffer.update(cx, |buffer, _cx| buffer.snapshot()); diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 82245d67ca0487adb33aac8ec124f658948c3509..7a9e6b83ceb48584211792239fe2a802ec2e886f 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -18,7 +18,7 @@ use futures::{ FutureExt, TryFutureExt as _, }; use gpui::{executor::Background, AppContext, AsyncAppContext, Task}; -use highlight_map::HighlightMap; +pub use highlight_map::HighlightMap; use lazy_static::lazy_static; use lsp::{CodeActionKind, LanguageServerBinary}; use parking_lot::{Mutex, RwLock}; diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index 0de3f704c7f29e4d1cff8f3fe371d0fad36d1f42..09c5ec7fc3214290b39eeb3585455839a883dbde 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -207,6 +207,7 @@ pub fn serialize_anchor(anchor: &Anchor) -> proto::Anchor { } } +// This behavior is currently copied in the collab database, for snapshotting channel notes pub fn deserialize_operation(message: proto::Operation) -> Result { Ok( match message diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index b6e1d16e18beac7c9b1282a639a619b57038867e..18f2e9b264159299b92148019ffcfe5de7006dca 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -310,7 +310,9 @@ impl SyntaxSnapshot { // Ignore edits that end before the start of this layer, and don't consider them // for any subsequent layers at this same depth. loop { - let Some((_, edit_range)) = edits.get(first_edit_ix_for_depth) else { continue 'outer }; + let Some((_, edit_range)) = edits.get(first_edit_ix_for_depth) else { + continue 'outer; + }; if edit_range.end.cmp(&layer.range.start, text).is_le() { first_edit_ix_for_depth += 1; } else { @@ -391,7 +393,9 @@ impl SyntaxSnapshot { .filter::<_, ()>(|summary| summary.contains_unknown_injections); cursor.next(text); while let Some(layer) = cursor.item() { - let SyntaxLayerContent::Pending { language_name } = &layer.content else { unreachable!() }; + let SyntaxLayerContent::Pending { language_name } = &layer.content else { + unreachable!() + }; if registry .language_for_name_or_extension(language_name) .now_or_never() @@ -533,7 +537,9 @@ impl SyntaxSnapshot { let content = match step.language { ParseStepLanguage::Loaded { language } => { - let Some(grammar) = language.grammar() else { continue }; + let Some(grammar) = language.grammar() else { + continue; + }; let tree; let changed_ranges; diff --git a/crates/language/src/syntax_map/syntax_map_tests.rs b/crates/language/src/syntax_map/syntax_map_tests.rs index c7babf207efcb2fdb30ec19c65adc7589f193ec4..bd50608122b80e9dd3ceba0a20d6b29dbb9f07c4 100644 --- a/crates/language/src/syntax_map/syntax_map_tests.rs +++ b/crates/language/src/syntax_map/syntax_map_tests.rs @@ -932,8 +932,12 @@ fn check_interpolation( .zip(new_syntax_map.layers.iter()) { assert_eq!(old_layer.range, new_layer.range); - let Some(old_tree) = old_layer.content.tree() else { continue }; - let Some(new_tree) = new_layer.content.tree() else { continue }; + let Some(old_tree) = old_layer.content.tree() else { + continue; + }; + let Some(new_tree) = new_layer.content.tree() else { + continue; + }; let old_start_byte = old_layer.range.start.to_offset(old_buffer); let new_start_byte = new_layer.range.start.to_offset(new_buffer); let old_start_point = old_layer.range.start.to_point(old_buffer).to_ts_point(); diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 16fb019c62a81bfcbf0c37332ef0723347fb5ad8..51bdb4c5cece790604a31d962fc7e2cef0297f98 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -176,7 +176,9 @@ impl LogStore { cx.notify(); LanguageServerState { rpc_state: None, - log_buffer: cx.add_model(|cx| Buffer::new(0, "", cx)).clone(), + log_buffer: cx + .add_model(|cx| Buffer::new(0, cx.model_id() as u64, "")) + .clone(), } }) .log_buffer @@ -241,7 +243,7 @@ impl LogStore { let rpc_state = server_state.rpc_state.get_or_insert_with(|| { let io_tx = self.io_tx.clone(); let language = project.read(cx).languages().language_for_name("JSON"); - let buffer = cx.add_model(|cx| Buffer::new(0, "", cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "")); cx.spawn_weak({ let buffer = buffer.clone(); |_, mut cx| async move { @@ -327,7 +329,7 @@ impl LspLogView { .projects .get(&project.downgrade()) .and_then(|project| project.servers.keys().copied().next()); - let buffer = cx.add_model(|cx| Buffer::new(0, "", cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "")); let mut this = Self { editor: Self::editor_for_buffer(project.clone(), buffer, cx), project, @@ -549,7 +551,9 @@ impl View for LspLogToolbarItemView { fn render(&mut self, cx: &mut ViewContext) -> AnyElement { let theme = theme::current(cx).clone(); - let Some(log_view) = self.log_view.as_ref() else { return Empty::new().into_any() }; + let Some(log_view) = self.log_view.as_ref() else { + return Empty::new().into_any(); + }; let log_view = log_view.read(cx); let menu_rows = log_view.menu_items(cx).unwrap_or_default(); diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index e0ae64d8069c08b12e11b8b12155892dc974ae0d..d49dafff2f99fd1c132c01349a363096cd63183a 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -77,7 +77,7 @@ pub enum Subscription { } #[derive(Serialize, Deserialize)] -struct Request<'a, T> { +pub struct Request<'a, T> { jsonrpc: &'static str, id: usize, method: &'a str, @@ -435,7 +435,13 @@ impl LanguageServer { }), inlay_hint: Some(InlayHintClientCapabilities { resolve_support: Some(InlayHintResolveClientCapabilities { - properties: vec!["textEdits".to_string(), "tooltip".to_string()], + properties: vec![ + "textEdits".to_string(), + "tooltip".to_string(), + "label.tooltip".to_string(), + "label.location".to_string(), + "label.command".to_string(), + ], }), dynamic_registration: Some(false), }), diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index a8692257d8032fdca5667c2089249e806b241e34..8239cf869067043d25549c5cfa74337e5211271d 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1,21 +1,23 @@ use crate::{ DocumentHighlight, Hover, HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel, InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink, - MarkupContent, Project, ProjectTransaction, + MarkupContent, Project, ProjectTransaction, ResolveState, }; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use client::proto::{self, PeerId}; use fs::LineEnding; +use futures::future; use gpui::{AppContext, AsyncAppContext, ModelHandle}; use language::{ language_settings::{language_settings, InlayHintKind}, point_from_lsp, point_to_lsp, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, - range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CharKind, CodeAction, - Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, Unclipped, + range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, + CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, + Unclipped, }; -use lsp::{DocumentHighlightKind, LanguageServer, LanguageServerId, ServerCapabilities}; +use lsp::{DocumentHighlightKind, LanguageServer, LanguageServerId, OneOf, ServerCapabilities}; use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc}; pub fn lsp_formatting_options(tab_size: u32) -> lsp::FormattingOptions { @@ -1431,7 +1433,7 @@ impl LspCommand for GetCompletions { }) }); - Ok(futures::future::join_all(completions).await) + Ok(future::join_all(completions).await) } fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetCompletions { @@ -1499,7 +1501,7 @@ impl LspCommand for GetCompletions { let completions = message.completions.into_iter().map(|completion| { language::proto::deserialize_completion(completion, language.clone()) }); - futures::future::try_join_all(completions).await + future::try_join_all(completions).await } fn buffer_id_from_proto(message: &proto::GetCompletions) -> u64 { @@ -1653,7 +1655,11 @@ impl LspCommand for OnTypeFormatting { type ProtoRequest = proto::OnTypeFormatting; fn check_capabilities(&self, server_capabilities: &lsp::ServerCapabilities) -> bool { - let Some(on_type_formatting_options) = &server_capabilities.document_on_type_formatting_provider else { return false }; + let Some(on_type_formatting_options) = + &server_capabilities.document_on_type_formatting_provider + else { + return false; + }; on_type_formatting_options .first_trigger_character .contains(&self.trigger) @@ -1767,7 +1773,9 @@ impl LspCommand for OnTypeFormatting { _: ModelHandle, _: AsyncAppContext, ) -> Result> { - let Some(transaction) = message.transaction else { return Ok(None) }; + let Some(transaction) = message.transaction else { + return Ok(None); + }; Ok(Some(language::proto::deserialize_transaction(transaction)?)) } @@ -1776,6 +1784,377 @@ impl LspCommand for OnTypeFormatting { } } +impl InlayHints { + pub async fn lsp_to_project_hint( + lsp_hint: lsp::InlayHint, + buffer_handle: &ModelHandle, + server_id: LanguageServerId, + resolve_state: ResolveState, + force_no_type_left_padding: bool, + cx: &mut AsyncAppContext, + ) -> anyhow::Result { + let kind = lsp_hint.kind.and_then(|kind| match kind { + lsp::InlayHintKind::TYPE => Some(InlayHintKind::Type), + lsp::InlayHintKind::PARAMETER => Some(InlayHintKind::Parameter), + _ => None, + }); + + let position = cx.update(|cx| { + let buffer = buffer_handle.read(cx); + let position = buffer.clip_point_utf16(point_from_lsp(lsp_hint.position), Bias::Left); + if kind == Some(InlayHintKind::Parameter) { + buffer.anchor_before(position) + } else { + buffer.anchor_after(position) + } + }); + let label = Self::lsp_inlay_label_to_project(lsp_hint.label, server_id) + .await + .context("lsp to project inlay hint conversion")?; + let padding_left = if force_no_type_left_padding && kind == Some(InlayHintKind::Type) { + false + } else { + lsp_hint.padding_left.unwrap_or(false) + }; + + Ok(InlayHint { + position, + padding_left, + padding_right: lsp_hint.padding_right.unwrap_or(false), + label, + kind, + tooltip: lsp_hint.tooltip.map(|tooltip| match tooltip { + lsp::InlayHintTooltip::String(s) => InlayHintTooltip::String(s), + lsp::InlayHintTooltip::MarkupContent(markup_content) => { + InlayHintTooltip::MarkupContent(MarkupContent { + kind: match markup_content.kind { + lsp::MarkupKind::PlainText => HoverBlockKind::PlainText, + lsp::MarkupKind::Markdown => HoverBlockKind::Markdown, + }, + value: markup_content.value, + }) + } + }), + resolve_state, + }) + } + + async fn lsp_inlay_label_to_project( + lsp_label: lsp::InlayHintLabel, + server_id: LanguageServerId, + ) -> anyhow::Result { + let label = match lsp_label { + lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s), + lsp::InlayHintLabel::LabelParts(lsp_parts) => { + let mut parts = Vec::with_capacity(lsp_parts.len()); + for lsp_part in lsp_parts { + parts.push(InlayHintLabelPart { + value: lsp_part.value, + tooltip: lsp_part.tooltip.map(|tooltip| match tooltip { + lsp::InlayHintLabelPartTooltip::String(s) => { + InlayHintLabelPartTooltip::String(s) + } + lsp::InlayHintLabelPartTooltip::MarkupContent(markup_content) => { + InlayHintLabelPartTooltip::MarkupContent(MarkupContent { + kind: match markup_content.kind { + lsp::MarkupKind::PlainText => HoverBlockKind::PlainText, + lsp::MarkupKind::Markdown => HoverBlockKind::Markdown, + }, + value: markup_content.value, + }) + } + }), + location: Some(server_id).zip(lsp_part.location), + }); + } + InlayHintLabel::LabelParts(parts) + } + }; + + Ok(label) + } + + pub fn project_to_proto_hint(response_hint: InlayHint) -> proto::InlayHint { + let (state, lsp_resolve_state) = match response_hint.resolve_state { + ResolveState::Resolved => (0, None), + ResolveState::CanResolve(server_id, resolve_data) => ( + 1, + resolve_data + .map(|json_data| { + serde_json::to_string(&json_data) + .expect("failed to serialize resolve json data") + }) + .map(|value| proto::resolve_state::LspResolveState { + server_id: server_id.0 as u64, + value, + }), + ), + ResolveState::Resolving => (2, None), + }; + let resolve_state = Some(proto::ResolveState { + state, + lsp_resolve_state, + }); + proto::InlayHint { + position: Some(language::proto::serialize_anchor(&response_hint.position)), + padding_left: response_hint.padding_left, + padding_right: response_hint.padding_right, + label: Some(proto::InlayHintLabel { + label: Some(match response_hint.label { + InlayHintLabel::String(s) => proto::inlay_hint_label::Label::Value(s), + InlayHintLabel::LabelParts(label_parts) => { + proto::inlay_hint_label::Label::LabelParts(proto::InlayHintLabelParts { + parts: label_parts.into_iter().map(|label_part| { + let location_url = label_part.location.as_ref().map(|(_, location)| location.uri.to_string()); + let location_range_start = label_part.location.as_ref().map(|(_, location)| point_from_lsp(location.range.start).0).map(|point| proto::PointUtf16 { row: point.row, column: point.column }); + let location_range_end = label_part.location.as_ref().map(|(_, location)| point_from_lsp(location.range.end).0).map(|point| proto::PointUtf16 { row: point.row, column: point.column }); + proto::InlayHintLabelPart { + value: label_part.value, + tooltip: label_part.tooltip.map(|tooltip| { + let proto_tooltip = match tooltip { + InlayHintLabelPartTooltip::String(s) => proto::inlay_hint_label_part_tooltip::Content::Value(s), + InlayHintLabelPartTooltip::MarkupContent(markup_content) => proto::inlay_hint_label_part_tooltip::Content::MarkupContent(proto::MarkupContent { + is_markdown: markup_content.kind == HoverBlockKind::Markdown, + value: markup_content.value, + }), + }; + proto::InlayHintLabelPartTooltip {content: Some(proto_tooltip)} + }), + location_url, + location_range_start, + location_range_end, + language_server_id: label_part.location.as_ref().map(|(server_id, _)| server_id.0 as u64), + }}).collect() + }) + } + }), + }), + kind: response_hint.kind.map(|kind| kind.name().to_string()), + tooltip: response_hint.tooltip.map(|response_tooltip| { + let proto_tooltip = match response_tooltip { + InlayHintTooltip::String(s) => proto::inlay_hint_tooltip::Content::Value(s), + InlayHintTooltip::MarkupContent(markup_content) => { + proto::inlay_hint_tooltip::Content::MarkupContent(proto::MarkupContent { + is_markdown: markup_content.kind == HoverBlockKind::Markdown, + value: markup_content.value, + }) + } + }; + proto::InlayHintTooltip { + content: Some(proto_tooltip), + } + }), + resolve_state, + } + } + + pub fn proto_to_project_hint(message_hint: proto::InlayHint) -> anyhow::Result { + let resolve_state = message_hint.resolve_state.as_ref().unwrap_or_else(|| { + panic!("incorrect proto inlay hint message: no resolve state in hint {message_hint:?}",) + }); + let resolve_state_data = resolve_state + .lsp_resolve_state.as_ref() + .map(|lsp_resolve_state| { + serde_json::from_str::>(&lsp_resolve_state.value) + .with_context(|| format!("incorrect proto inlay hint message: non-json resolve state {lsp_resolve_state:?}")) + .map(|state| (LanguageServerId(lsp_resolve_state.server_id as usize), state)) + }) + .transpose()?; + let resolve_state = match resolve_state.state { + 0 => ResolveState::Resolved, + 1 => { + let (server_id, lsp_resolve_state) = resolve_state_data.with_context(|| { + format!( + "No lsp resolve data for the hint that can be resolved: {message_hint:?}" + ) + })?; + ResolveState::CanResolve(server_id, lsp_resolve_state) + } + 2 => ResolveState::Resolving, + invalid => { + anyhow::bail!("Unexpected resolve state {invalid} for hint {message_hint:?}") + } + }; + Ok(InlayHint { + position: message_hint + .position + .and_then(language::proto::deserialize_anchor) + .context("invalid position")?, + label: match message_hint + .label + .and_then(|label| label.label) + .context("missing label")? + { + proto::inlay_hint_label::Label::Value(s) => InlayHintLabel::String(s), + proto::inlay_hint_label::Label::LabelParts(parts) => { + let mut label_parts = Vec::new(); + for part in parts.parts { + label_parts.push(InlayHintLabelPart { + value: part.value, + tooltip: part.tooltip.map(|tooltip| match tooltip.content { + Some(proto::inlay_hint_label_part_tooltip::Content::Value(s)) => { + InlayHintLabelPartTooltip::String(s) + } + Some( + proto::inlay_hint_label_part_tooltip::Content::MarkupContent( + markup_content, + ), + ) => InlayHintLabelPartTooltip::MarkupContent(MarkupContent { + kind: if markup_content.is_markdown { + HoverBlockKind::Markdown + } else { + HoverBlockKind::PlainText + }, + value: markup_content.value, + }), + None => InlayHintLabelPartTooltip::String(String::new()), + }), + location: { + match part + .location_url + .zip( + part.location_range_start.and_then(|start| { + Some(start..part.location_range_end?) + }), + ) + .zip(part.language_server_id) + { + Some(((uri, range), server_id)) => Some(( + LanguageServerId(server_id as usize), + lsp::Location { + uri: lsp::Url::parse(&uri) + .context("invalid uri in hint part {part:?}")?, + range: lsp::Range::new( + point_to_lsp(PointUtf16::new( + range.start.row, + range.start.column, + )), + point_to_lsp(PointUtf16::new( + range.end.row, + range.end.column, + )), + ), + }, + )), + None => None, + } + }, + }); + } + + InlayHintLabel::LabelParts(label_parts) + } + }, + padding_left: message_hint.padding_left, + padding_right: message_hint.padding_right, + kind: message_hint + .kind + .as_deref() + .and_then(InlayHintKind::from_name), + tooltip: message_hint.tooltip.and_then(|tooltip| { + Some(match tooltip.content? { + proto::inlay_hint_tooltip::Content::Value(s) => InlayHintTooltip::String(s), + proto::inlay_hint_tooltip::Content::MarkupContent(markup_content) => { + InlayHintTooltip::MarkupContent(MarkupContent { + kind: if markup_content.is_markdown { + HoverBlockKind::Markdown + } else { + HoverBlockKind::PlainText + }, + value: markup_content.value, + }) + } + }) + }), + resolve_state, + }) + } + + pub fn project_to_lsp_hint(hint: InlayHint, snapshot: &BufferSnapshot) -> lsp::InlayHint { + lsp::InlayHint { + position: point_to_lsp(hint.position.to_point_utf16(snapshot)), + kind: hint.kind.map(|kind| match kind { + InlayHintKind::Type => lsp::InlayHintKind::TYPE, + InlayHintKind::Parameter => lsp::InlayHintKind::PARAMETER, + }), + text_edits: None, + tooltip: hint.tooltip.and_then(|tooltip| { + Some(match tooltip { + InlayHintTooltip::String(s) => lsp::InlayHintTooltip::String(s), + InlayHintTooltip::MarkupContent(markup_content) => { + lsp::InlayHintTooltip::MarkupContent(lsp::MarkupContent { + kind: match markup_content.kind { + HoverBlockKind::PlainText => lsp::MarkupKind::PlainText, + HoverBlockKind::Markdown => lsp::MarkupKind::Markdown, + HoverBlockKind::Code { .. } => return None, + }, + value: markup_content.value, + }) + } + }) + }), + label: match hint.label { + InlayHintLabel::String(s) => lsp::InlayHintLabel::String(s), + InlayHintLabel::LabelParts(label_parts) => lsp::InlayHintLabel::LabelParts( + label_parts + .into_iter() + .map(|part| lsp::InlayHintLabelPart { + value: part.value, + tooltip: part.tooltip.and_then(|tooltip| { + Some(match tooltip { + InlayHintLabelPartTooltip::String(s) => { + lsp::InlayHintLabelPartTooltip::String(s) + } + InlayHintLabelPartTooltip::MarkupContent(markup_content) => { + lsp::InlayHintLabelPartTooltip::MarkupContent( + lsp::MarkupContent { + kind: match markup_content.kind { + HoverBlockKind::PlainText => { + lsp::MarkupKind::PlainText + } + HoverBlockKind::Markdown => { + lsp::MarkupKind::Markdown + } + HoverBlockKind::Code { .. } => return None, + }, + value: markup_content.value, + }, + ) + } + }) + }), + location: part.location.map(|(_, location)| location), + command: None, + }) + .collect(), + ), + }, + padding_left: Some(hint.padding_left), + padding_right: Some(hint.padding_right), + data: match hint.resolve_state { + ResolveState::CanResolve(_, data) => data, + ResolveState::Resolving | ResolveState::Resolved => None, + }, + } + } + + pub fn can_resolve_inlays(capabilities: &ServerCapabilities) -> bool { + capabilities + .inlay_hint_provider + .as_ref() + .and_then(|options| match options { + OneOf::Left(_is_supported) => None, + OneOf::Right(capabilities) => match capabilities { + lsp::InlayHintServerCapabilities::Options(o) => o.resolve_provider, + lsp::InlayHintServerCapabilities::RegistrationOptions(o) => { + o.inlay_hint_options.resolve_provider + } + }, + }) + .unwrap_or(false) + } +} + #[async_trait(?Send)] impl LspCommand for InlayHints { type Response = Vec; @@ -1783,7 +2162,9 @@ impl LspCommand for InlayHints { type ProtoRequest = proto::InlayHints; fn check_capabilities(&self, server_capabilities: &lsp::ServerCapabilities) -> bool { - let Some(inlay_hint_provider) = &server_capabilities.inlay_hint_provider else { return false }; + let Some(inlay_hint_provider) = &server_capabilities.inlay_hint_provider else { + return false; + }; match inlay_hint_provider { lsp::OneOf::Left(enabled) => *enabled, lsp::OneOf::Right(inlay_hint_capabilities) => match inlay_hint_capabilities { @@ -1816,8 +2197,9 @@ impl LspCommand for InlayHints { buffer: ModelHandle, server_id: LanguageServerId, mut cx: AsyncAppContext, - ) -> Result> { - let (lsp_adapter, _) = language_server_for_buffer(&project, &buffer, server_id, &mut cx)?; + ) -> anyhow::Result> { + let (lsp_adapter, lsp_server) = + 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. @@ -1827,93 +2209,32 @@ impl LspCommand for InlayHints { // 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 - .unwrap_or_default() - .into_iter() - .map(|lsp_hint| { - let kind = lsp_hint.kind.and_then(|kind| match kind { - lsp::InlayHintKind::TYPE => Some(InlayHintKind::Type), - lsp::InlayHintKind::PARAMETER => Some(InlayHintKind::Parameter), - _ => None, - }); - 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) { - origin_buffer.anchor_before(position) - } else { - origin_buffer.anchor_after(position) - }, - padding_left, - padding_right: lsp_hint.padding_right.unwrap_or(false), - label: match lsp_hint.label { - lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s), - lsp::InlayHintLabel::LabelParts(lsp_parts) => { - InlayHintLabel::LabelParts( - lsp_parts - .into_iter() - .map(|label_part| InlayHintLabelPart { - value: label_part.value, - tooltip: label_part.tooltip.map( - |tooltip| { - match tooltip { - lsp::InlayHintLabelPartTooltip::String(s) => { - InlayHintLabelPartTooltip::String(s) - } - lsp::InlayHintLabelPartTooltip::MarkupContent( - markup_content, - ) => InlayHintLabelPartTooltip::MarkupContent( - MarkupContent { - kind: format!("{:?}", markup_content.kind), - value: markup_content.value, - }, - ), - } - }, - ), - location: label_part.location.map(|lsp_location| { - let target_start = origin_buffer.clip_point_utf16( - point_from_lsp(lsp_location.range.start), - Bias::Left, - ); - let target_end = origin_buffer.clip_point_utf16( - point_from_lsp(lsp_location.range.end), - Bias::Left, - ); - Location { - buffer: buffer.clone(), - range: origin_buffer.anchor_after(target_start) - ..origin_buffer.anchor_before(target_end), - } - }), - }) - .collect(), - ) - } - }, - kind, - tooltip: lsp_hint.tooltip.map(|tooltip| match tooltip { - lsp::InlayHintTooltip::String(s) => InlayHintTooltip::String(s), - lsp::InlayHintTooltip::MarkupContent(markup_content) => { - InlayHintTooltip::MarkupContent(MarkupContent { - kind: format!("{:?}", markup_content.kind), - value: markup_content.value, - }) - } - }), - } - }) - .collect()) - }) + + let hints = message.unwrap_or_default().into_iter().map(|lsp_hint| { + let resolve_state = if InlayHints::can_resolve_inlays(lsp_server.capabilities()) { + ResolveState::CanResolve(lsp_server.server_id(), lsp_hint.data.clone()) + } else { + ResolveState::Resolved + }; + + let buffer = buffer.clone(); + cx.spawn(|mut cx| async move { + InlayHints::lsp_to_project_hint( + lsp_hint, + &buffer, + server_id, + resolve_state, + force_no_type_left_padding, + &mut cx, + ) + .await + }) + }); + future::join_all(hints) + .await + .into_iter() + .collect::>() + .context("lsp to project inlay hints conversion") } fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::InlayHints { @@ -1959,23 +2280,7 @@ impl LspCommand for InlayHints { proto::InlayHintsResponse { hints: response .into_iter() - .map(|response_hint| proto::InlayHint { - position: Some(language::proto::serialize_anchor(&response_hint.position)), - padding_left: response_hint.padding_left, - padding_right: response_hint.padding_right, - kind: response_hint.kind.map(|kind| kind.name().to_string()), - // Do not pass extra data such as tooltips to clients: host can put tooltip data from the cache during resolution. - tooltip: None, - // Similarly, do not pass label parts to clients: host can return a detailed list during resolution. - label: Some(proto::InlayHintLabel { - label: Some(proto::inlay_hint_label::Label::Value( - match response_hint.label { - InlayHintLabel::String(s) => s, - InlayHintLabel::LabelParts(_) => response_hint.text(), - }, - )), - }), - }) + .map(|response_hint| InlayHints::project_to_proto_hint(response_hint)) .collect(), version: serialize_version(buffer_version), } @@ -1984,10 +2289,10 @@ impl LspCommand for InlayHints { async fn response_from_proto( self, message: proto::InlayHintsResponse, - project: ModelHandle, + _: ModelHandle, buffer: ModelHandle, mut cx: AsyncAppContext, - ) -> Result> { + ) -> anyhow::Result> { buffer .update(&mut cx, |buffer, _| { buffer.wait_for_version(deserialize_version(&message.version)) @@ -1996,82 +2301,7 @@ impl LspCommand for InlayHints { let mut hints = Vec::new(); for message_hint in message.hints { - let buffer_id = message_hint - .position - .as_ref() - .and_then(|location| location.buffer_id) - .context("missing buffer id")?; - let hint = InlayHint { - buffer_id, - position: message_hint - .position - .and_then(language::proto::deserialize_anchor) - .context("invalid position")?, - label: match message_hint - .label - .and_then(|label| label.label) - .context("missing label")? - { - proto::inlay_hint_label::Label::Value(s) => InlayHintLabel::String(s), - proto::inlay_hint_label::Label::LabelParts(parts) => { - let mut label_parts = Vec::new(); - for part in parts.parts { - label_parts.push(InlayHintLabelPart { - value: part.value, - tooltip: part.tooltip.map(|tooltip| match tooltip.content { - Some(proto::inlay_hint_label_part_tooltip::Content::Value(s)) => InlayHintLabelPartTooltip::String(s), - Some(proto::inlay_hint_label_part_tooltip::Content::MarkupContent(markup_content)) => InlayHintLabelPartTooltip::MarkupContent(MarkupContent { - kind: markup_content.kind, - value: markup_content.value, - }), - None => InlayHintLabelPartTooltip::String(String::new()), - }), - location: match part.location { - Some(location) => { - let target_buffer = project - .update(&mut cx, |this, cx| { - this.wait_for_remote_buffer(location.buffer_id, cx) - }) - .await?; - Some(Location { - range: location - .start - .and_then(language::proto::deserialize_anchor) - .context("invalid start")? - ..location - .end - .and_then(language::proto::deserialize_anchor) - .context("invalid end")?, - buffer: target_buffer, - })}, - None => None, - }, - }); - } - - InlayHintLabel::LabelParts(label_parts) - } - }, - padding_left: message_hint.padding_left, - padding_right: message_hint.padding_right, - kind: message_hint - .kind - .as_deref() - .and_then(InlayHintKind::from_name), - tooltip: message_hint.tooltip.and_then(|tooltip| { - Some(match tooltip.content? { - proto::inlay_hint_tooltip::Content::Value(s) => InlayHintTooltip::String(s), - proto::inlay_hint_tooltip::Content::MarkupContent(markup_content) => { - InlayHintTooltip::MarkupContent(MarkupContent { - kind: markup_content.kind, - value: markup_content.value, - }) - } - }) - }), - }; - - hints.push(hint); + hints.push(InlayHints::proto_to_project_hint(message_hint)?); } Ok(hints) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index b120baa9512c5610e0a1e217e9c88ac536e7eec7..f839c8d5c504b93f6b11e31d6e39a6f0284f1255 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -11,7 +11,7 @@ mod project_tests; mod worktree_tests; use anyhow::{anyhow, Context, Result}; -use client::{proto, Client, TypedEnvelope, UserStore}; +use client::{proto, Client, TypedEnvelope, UserId, UserStore}; use clock::ReplicaId; use collections::{hash_map, BTreeMap, HashMap, HashSet}; use copilot::Copilot; @@ -26,8 +26,8 @@ use futures::{ }; use globset::{Glob, GlobSet, GlobSetBuilder}; use gpui::{ - AnyModelHandle, AppContext, AsyncAppContext, BorrowAppContext, Entity, ModelContext, - ModelHandle, Task, WeakModelHandle, + executor::Background, AnyModelHandle, AppContext, AsyncAppContext, BorrowAppContext, Entity, + ModelContext, ModelHandle, Task, WeakModelHandle, }; use itertools::Itertools; use language::{ @@ -37,11 +37,11 @@ use language::{ deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version, serialize_anchor, serialize_version, }, - range_from_lsp, range_to_lsp, Bias, Buffer, CachedLspAdapter, CodeAction, CodeLabel, - Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent, File as _, - Language, LanguageRegistry, LanguageServerName, LocalFile, LspAdapterDelegate, OffsetRangeExt, - Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot, ToOffset, - ToPointUtf16, Transaction, Unclipped, + range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CodeAction, + CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent, + File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, LspAdapterDelegate, + OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot, + ToOffset, ToPointUtf16, Transaction, Unclipped, }; use log::error; use lsp::{ @@ -57,8 +57,8 @@ use serde::Serialize; use settings::SettingsStore; use sha2::{Digest, Sha256}; use similar::{ChangeTag, TextDiff}; +use smol::channel::{Receiver, Sender}; use std::{ - cell::RefCell, cmp::{self, Ordering}, convert::TryInto, hash::Hash, @@ -67,7 +67,6 @@ use std::{ ops::Range, path::{self, Component, Path, PathBuf}, process::Stdio, - rc::Rc, str, sync::{ atomic::{AtomicUsize, Ordering::SeqCst}, @@ -250,6 +249,7 @@ enum ProjectClientState { pub struct Collaborator { pub peer_id: proto::PeerId, pub replica_id: ReplicaId, + pub user_id: UserId, } #[derive(Clone, Debug, PartialEq)] @@ -281,6 +281,7 @@ pub enum Event { old_peer_id: proto::PeerId, new_peer_id: proto::PeerId, }, + CollaboratorJoined(proto::PeerId), CollaboratorLeft(proto::PeerId), RefreshInlayHints, } @@ -331,15 +332,22 @@ pub struct Location { pub range: Range, } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct InlayHint { - pub buffer_id: u64, pub position: language::Anchor, pub label: InlayHintLabel, pub kind: Option, pub padding_left: bool, pub padding_right: bool, pub tooltip: Option, + pub resolve_state: ResolveState, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ResolveState { + Resolved, + CanResolve(LanguageServerId, Option), + Resolving, } impl InlayHint { @@ -351,34 +359,34 @@ impl InlayHint { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum InlayHintLabel { String(String), LabelParts(Vec), } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct InlayHintLabelPart { pub value: String, pub tooltip: Option, - pub location: Option, + pub location: Option<(LanguageServerId, lsp::Location)>, } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum InlayHintTooltip { String(String), MarkupContent(MarkupContent), } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum InlayHintLabelPartTooltip { String(String), MarkupContent(MarkupContent), } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct MarkupContent { - pub kind: String, + pub kind: HoverBlockKind, pub value: String, } @@ -412,7 +420,7 @@ pub struct HoverBlock { pub kind: HoverBlockKind, } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum HoverBlockKind { PlainText, Markdown, @@ -516,6 +524,28 @@ impl FormatTrigger { } } } +#[derive(Clone, Debug, PartialEq)] +enum SearchMatchCandidate { + OpenBuffer { + buffer: ModelHandle, + // This might be an unnamed file without representation on filesystem + path: Option>, + }, + Path { + worktree_id: WorktreeId, + path: Arc, + }, +} + +type SearchMatchCandidateIndex = usize; +impl SearchMatchCandidate { + fn path(&self) -> Option> { + match self { + SearchMatchCandidate::OpenBuffer { path, .. } => path.clone(), + SearchMatchCandidate::Path { path, .. } => Some(path.clone()), + } + } +} impl Project { pub fn init_settings(cx: &mut AppContext) { @@ -549,6 +579,7 @@ impl Project { client.add_model_request_handler(Self::handle_apply_code_action); client.add_model_request_handler(Self::handle_on_type_formatting); client.add_model_request_handler(Self::handle_inlay_hints); + client.add_model_request_handler(Self::handle_resolve_inlay_hint); client.add_model_request_handler(Self::handle_refresh_inlay_hints); client.add_model_request_handler(Self::handle_reload_buffers); client.add_model_request_handler(Self::handle_synchronize_buffers); @@ -1537,9 +1568,9 @@ impl Project { if self.is_remote() { return Err(anyhow!("creating buffers as a guest is not supported yet")); } - + let id = post_inc(&mut self.next_buffer_id); let buffer = cx.add_model(|cx| { - Buffer::new(self.replica_id(), text, cx) + Buffer::new(self.replica_id(), id, text) .with_language(language.unwrap_or_else(|| language::PLAIN_TEXT.clone()), cx) }); self.register_buffer(&buffer, cx)?; @@ -1677,7 +1708,7 @@ impl Project { } /// LanguageServerName is owned, because it is inserted into a map - fn open_local_buffer_via_lsp( + pub fn open_local_buffer_via_lsp( &mut self, abs_path: lsp::Url, language_server_id: LanguageServerId, @@ -4967,7 +4998,7 @@ impl Project { buffer_handle: ModelHandle, range: Range, cx: &mut ModelContext, - ) -> Task>> { + ) -> Task>> { let buffer = buffer_handle.read(cx); let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end); let range_start = range.start; @@ -5017,192 +5048,79 @@ impl Project { } } - #[allow(clippy::type_complexity)] - pub fn search( + pub fn resolve_inlay_hint( &self, - query: SearchQuery, + hint: InlayHint, + buffer_handle: ModelHandle, + server_id: LanguageServerId, cx: &mut ModelContext, - ) -> Task, Vec>>>> { + ) -> Task> { if self.is_local() { - let snapshots = self - .visible_worktrees(cx) - .filter_map(|tree| { - let tree = tree.read(cx).as_local()?; - Some(tree.snapshot()) - }) - .collect::>(); - - let background = cx.background().clone(); - let path_count: usize = snapshots.iter().map(|s| s.visible_file_count()).sum(); - if path_count == 0 { - return Task::ready(Ok(Default::default())); + let buffer = buffer_handle.read(cx); + let (_, lang_server) = if let Some((adapter, server)) = + self.language_server_for_buffer(buffer, server_id, cx) + { + (adapter.clone(), server.clone()) + } else { + return Task::ready(Ok(hint)); + }; + if !InlayHints::can_resolve_inlays(lang_server.capabilities()) { + return Task::ready(Ok(hint)); } - let workers = background.num_cpus().min(path_count); - let (matching_paths_tx, mut matching_paths_rx) = smol::channel::bounded(1024); - cx.background() - .spawn({ - let fs = self.fs.clone(); - let background = cx.background().clone(); - let query = query.clone(); - async move { - let fs = &fs; - let query = &query; - let matching_paths_tx = &matching_paths_tx; - let paths_per_worker = (path_count + workers - 1) / workers; - let snapshots = &snapshots; - background - .scoped(|scope| { - for worker_ix in 0..workers { - let worker_start_ix = worker_ix * paths_per_worker; - let worker_end_ix = worker_start_ix + paths_per_worker; - scope.spawn(async move { - let mut snapshot_start_ix = 0; - let mut abs_path = PathBuf::new(); - for snapshot in snapshots { - let snapshot_end_ix = - snapshot_start_ix + snapshot.visible_file_count(); - if worker_end_ix <= snapshot_start_ix { - break; - } else if worker_start_ix > snapshot_end_ix { - snapshot_start_ix = snapshot_end_ix; - continue; - } else { - let start_in_snapshot = worker_start_ix - .saturating_sub(snapshot_start_ix); - let end_in_snapshot = - cmp::min(worker_end_ix, snapshot_end_ix) - - snapshot_start_ix; - - for entry in snapshot - .files(false, start_in_snapshot) - .take(end_in_snapshot - start_in_snapshot) - { - if matching_paths_tx.is_closed() { - break; - } - let matches = if query - .file_matches(Some(&entry.path)) - { - abs_path.clear(); - abs_path.push(&snapshot.abs_path()); - abs_path.push(&entry.path); - if let Some(file) = - fs.open_sync(&abs_path).await.log_err() - { - query.detect(file).unwrap_or(false) - } else { - false - } - } else { - false - }; - - if matches { - let project_path = - (snapshot.id(), entry.path.clone()); - if matching_paths_tx - .send(project_path) - .await - .is_err() - { - break; - } - } - } - - snapshot_start_ix = snapshot_end_ix; - } - } - }); - } - }) - .await; - } - }) - .detach(); - - let (buffers_tx, buffers_rx) = smol::channel::bounded(1024); - let open_buffers = self - .opened_buffers - .values() - .filter_map(|b| b.upgrade(cx)) - .collect::>(); - cx.spawn(|this, cx| async move { - for buffer in &open_buffers { - let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); - buffers_tx.send((buffer.clone(), snapshot)).await?; - } - let open_buffers = Rc::new(RefCell::new(open_buffers)); - while let Some(project_path) = matching_paths_rx.next().await { - if buffers_tx.is_closed() { - break; - } - - let this = this.clone(); - let open_buffers = open_buffers.clone(); - let buffers_tx = buffers_tx.clone(); - cx.spawn(|mut cx| async move { - if let Some(buffer) = this - .update(&mut cx, |this, cx| this.open_buffer(project_path, cx)) - .await - .log_err() - { - if open_buffers.borrow_mut().insert(buffer.clone()) { - let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); - buffers_tx.send((buffer, snapshot)).await?; - } - } - - Ok::<_, anyhow::Error>(()) - }) - .detach(); - } - - Ok::<_, anyhow::Error>(()) + let buffer_snapshot = buffer.snapshot(); + cx.spawn(|_, mut cx| async move { + let resolve_task = lang_server.request::( + InlayHints::project_to_lsp_hint(hint, &buffer_snapshot), + ); + let resolved_hint = resolve_task + .await + .context("inlay hint resolve LSP request")?; + let resolved_hint = InlayHints::lsp_to_project_hint( + resolved_hint, + &buffer_handle, + server_id, + ResolveState::Resolved, + false, + &mut cx, + ) + .await?; + Ok(resolved_hint) }) - .detach_and_log_err(cx); - - let background = cx.background().clone(); - cx.background().spawn(async move { - let query = &query; - let mut matched_buffers = Vec::new(); - for _ in 0..workers { - matched_buffers.push(HashMap::default()); + } else if let Some(project_id) = self.remote_id() { + let client = self.client.clone(); + let request = proto::ResolveInlayHint { + project_id, + buffer_id: buffer_handle.read(cx).remote_id(), + language_server_id: server_id.0 as u64, + hint: Some(InlayHints::project_to_proto_hint(hint.clone())), + }; + cx.spawn(|_, _| async move { + let response = client + .request(request) + .await + .context("inlay hints proto request")?; + match response.hint { + Some(resolved_hint) => InlayHints::proto_to_project_hint(resolved_hint) + .context("inlay hints proto resolve response conversion"), + None => Ok(hint), } - background - .scoped(|scope| { - for worker_matched_buffers in matched_buffers.iter_mut() { - let mut buffers_rx = buffers_rx.clone(); - scope.spawn(async move { - while let Some((buffer, snapshot)) = buffers_rx.next().await { - let buffer_matches = if query.file_matches( - snapshot.file().map(|file| file.path().as_ref()), - ) { - query - .search(&snapshot, None) - .await - .iter() - .map(|range| { - snapshot.anchor_before(range.start) - ..snapshot.anchor_after(range.end) - }) - .collect() - } else { - Vec::new() - }; - if !buffer_matches.is_empty() { - worker_matched_buffers - .insert(buffer.clone(), buffer_matches); - } - } - }); - } - }) - .await; - Ok(matched_buffers.into_iter().flatten().collect()) }) + } else { + Task::ready(Err(anyhow!("project does not have a remote id"))) + } + } + + #[allow(clippy::type_complexity)] + pub fn search( + &self, + query: SearchQuery, + cx: &mut ModelContext, + ) -> Receiver<(ModelHandle, Vec>)> { + if self.is_local() { + self.search_local(query, cx) } else if let Some(project_id) = self.remote_id() { + let (tx, rx) = smol::channel::unbounded(); let request = self.client.request(query.to_proto(project_id)); cx.spawn(|this, mut cx| async move { let response = request.await?; @@ -5226,11 +5144,301 @@ impl Project { .or_insert(Vec::new()) .push(start..end) } - Ok(result) + for (buffer, ranges) in result { + let _ = tx.send((buffer, ranges)).await; + } + Result::<(), anyhow::Error>::Ok(()) }) + .detach_and_log_err(cx); + rx } else { - Task::ready(Ok(Default::default())) + unimplemented!(); + } + } + + pub fn search_local( + &self, + query: SearchQuery, + cx: &mut ModelContext, + ) -> Receiver<(ModelHandle, Vec>)> { + // Local search is split into several phases. + // TL;DR is that we do 2 passes; initial pass to pick files which contain at least one match + // and the second phase that finds positions of all the matches found in the candidate files. + // The Receiver obtained from this function returns matches sorted by buffer path. Files without a buffer path are reported first. + // + // It gets a bit hairy though, because we must account for files that do not have a persistent representation + // on FS. Namely, if you have an untitled buffer or unsaved changes in a buffer, we want to scan that too. + // + // 1. We initialize a queue of match candidates and feed all opened buffers into it (== unsaved files / untitled buffers). + // Then, we go through a worktree and check for files that do match a predicate. If the file had an opened version, we skip the scan + // of FS version for that file altogether - after all, what we have in memory is more up-to-date than what's in FS. + // 2. At this point, we have a list of all potentially matching buffers/files. + // We sort that list by buffer path - this list is retained for later use. + // We ensure that all buffers are now opened and available in project. + // 3. We run a scan over all the candidate buffers on multiple background threads. + // We cannot assume that there will even be a match - while at least one match + // is guaranteed for files obtained from FS, the buffers we got from memory (unsaved files/unnamed buffers) might not have a match at all. + // There is also an auxilliary background thread responsible for result gathering. + // This is where the sorted list of buffers comes into play to maintain sorted order; Whenever this background thread receives a notification (buffer has/doesn't have matches), + // it keeps it around. It reports matches in sorted order, though it accepts them in unsorted order as well. + // As soon as the match info on next position in sorted order becomes available, it reports it (if it's a match) or skips to the next + // entry - which might already be available thanks to out-of-order processing. + // + // We could also report matches fully out-of-order, without maintaining a sorted list of matching paths. + // This however would mean that project search (that is the main user of this function) would have to do the sorting itself, on the go. + // This isn't as straightforward as running an insertion sort sadly, and would also mean that it would have to care about maintaining match index + // in face of constantly updating list of sorted matches. + // Meanwhile, this implementation offers index stability, since the matches are already reported in a sorted order. + let snapshots = self + .visible_worktrees(cx) + .filter_map(|tree| { + let tree = tree.read(cx).as_local()?; + Some(tree.snapshot()) + }) + .collect::>(); + + let background = cx.background().clone(); + let path_count: usize = snapshots.iter().map(|s| s.visible_file_count()).sum(); + if path_count == 0 { + let (_, rx) = smol::channel::bounded(1024); + return rx; + } + let workers = background.num_cpus().min(path_count); + let (matching_paths_tx, matching_paths_rx) = smol::channel::bounded(1024); + let mut unnamed_files = vec![]; + let opened_buffers = self + .opened_buffers + .iter() + .filter_map(|(_, b)| { + let buffer = b.upgrade(cx)?; + let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); + if let Some(path) = snapshot.file().map(|file| file.path()) { + Some((path.clone(), (buffer, snapshot))) + } else { + unnamed_files.push(buffer); + None + } + }) + .collect(); + cx.background() + .spawn(Self::background_search( + unnamed_files, + opened_buffers, + cx.background().clone(), + self.fs.clone(), + workers, + query.clone(), + path_count, + snapshots, + matching_paths_tx, + )) + .detach(); + + let (buffers, buffers_rx) = Self::sort_candidates_and_open_buffers(matching_paths_rx, cx); + let background = cx.background().clone(); + let (result_tx, result_rx) = smol::channel::bounded(1024); + cx.background() + .spawn(async move { + let Ok(buffers) = buffers.await else { + return; + }; + + let buffers_len = buffers.len(); + if buffers_len == 0 { + return; + } + let query = &query; + let (finished_tx, mut finished_rx) = smol::channel::unbounded(); + background + .scoped(|scope| { + #[derive(Clone)] + struct FinishedStatus { + entry: Option<(ModelHandle, Vec>)>, + buffer_index: SearchMatchCandidateIndex, + } + + for _ in 0..workers { + let finished_tx = finished_tx.clone(); + let mut buffers_rx = buffers_rx.clone(); + scope.spawn(async move { + while let Some((entry, buffer_index)) = buffers_rx.next().await { + let buffer_matches = if let Some((_, snapshot)) = entry.as_ref() + { + if query.file_matches( + snapshot.file().map(|file| file.path().as_ref()), + ) { + query + .search(&snapshot, None) + .await + .iter() + .map(|range| { + snapshot.anchor_before(range.start) + ..snapshot.anchor_after(range.end) + }) + .collect() + } else { + Vec::new() + } + } else { + Vec::new() + }; + + let status = if !buffer_matches.is_empty() { + let entry = if let Some((buffer, _)) = entry.as_ref() { + Some((buffer.clone(), buffer_matches)) + } else { + None + }; + FinishedStatus { + entry, + buffer_index, + } + } else { + FinishedStatus { + entry: None, + buffer_index, + } + }; + if finished_tx.send(status).await.is_err() { + break; + } + } + }); + } + // Report sorted matches + scope.spawn(async move { + let mut current_index = 0; + let mut scratch = vec![None; buffers_len]; + while let Some(status) = finished_rx.next().await { + debug_assert!( + scratch[status.buffer_index].is_none(), + "Got match status of position {} twice", + status.buffer_index + ); + let index = status.buffer_index; + scratch[index] = Some(status); + while current_index < buffers_len { + let Some(current_entry) = scratch[current_index].take() else { + // We intentionally **do not** increment `current_index` here. When next element arrives + // from `finished_rx`, we will inspect the same position again, hoping for it to be Some(_) + // this time. + break; + }; + if let Some(entry) = current_entry.entry { + result_tx.send(entry).await.log_err(); + } + current_index += 1; + } + if current_index == buffers_len { + break; + } + } + }); + }) + .await; + }) + .detach(); + result_rx + } + /// Pick paths that might potentially contain a match of a given search query. + async fn background_search( + unnamed_buffers: Vec>, + opened_buffers: HashMap, (ModelHandle, BufferSnapshot)>, + background: Arc, + fs: Arc, + workers: usize, + query: SearchQuery, + path_count: usize, + snapshots: Vec, + matching_paths_tx: Sender, + ) { + let fs = &fs; + let query = &query; + let matching_paths_tx = &matching_paths_tx; + let snapshots = &snapshots; + let paths_per_worker = (path_count + workers - 1) / workers; + for buffer in unnamed_buffers { + matching_paths_tx + .send(SearchMatchCandidate::OpenBuffer { + buffer: buffer.clone(), + path: None, + }) + .await + .log_err(); } + for (path, (buffer, _)) in opened_buffers.iter() { + matching_paths_tx + .send(SearchMatchCandidate::OpenBuffer { + buffer: buffer.clone(), + path: Some(path.clone()), + }) + .await + .log_err(); + } + background + .scoped(|scope| { + for worker_ix in 0..workers { + let worker_start_ix = worker_ix * paths_per_worker; + let worker_end_ix = worker_start_ix + paths_per_worker; + let unnamed_buffers = opened_buffers.clone(); + scope.spawn(async move { + let mut snapshot_start_ix = 0; + let mut abs_path = PathBuf::new(); + for snapshot in snapshots { + let snapshot_end_ix = snapshot_start_ix + snapshot.visible_file_count(); + if worker_end_ix <= snapshot_start_ix { + break; + } else if worker_start_ix > snapshot_end_ix { + snapshot_start_ix = snapshot_end_ix; + continue; + } else { + let start_in_snapshot = + worker_start_ix.saturating_sub(snapshot_start_ix); + let end_in_snapshot = + cmp::min(worker_end_ix, snapshot_end_ix) - snapshot_start_ix; + + for entry in snapshot + .files(false, start_in_snapshot) + .take(end_in_snapshot - start_in_snapshot) + { + if matching_paths_tx.is_closed() { + break; + } + if unnamed_buffers.contains_key(&entry.path) { + continue; + } + let matches = if query.file_matches(Some(&entry.path)) { + abs_path.clear(); + abs_path.push(&snapshot.abs_path()); + abs_path.push(&entry.path); + if let Some(file) = fs.open_sync(&abs_path).await.log_err() + { + query.detect(file).unwrap_or(false) + } else { + false + } + } else { + false + }; + + if matches { + let project_path = SearchMatchCandidate::Path { + worktree_id: snapshot.id(), + path: entry.path.clone(), + }; + if matching_paths_tx.send(project_path).await.is_err() { + break; + } + } + } + + snapshot_start_ix = snapshot_end_ix; + } + } + }); + } + }) + .await; } // TODO: Wire this up to allow selecting a server? @@ -5307,6 +5515,61 @@ impl Project { Task::ready(Ok(Default::default())) } + fn sort_candidates_and_open_buffers( + mut matching_paths_rx: Receiver, + cx: &mut ModelContext, + ) -> ( + futures::channel::oneshot::Receiver>, + Receiver<( + Option<(ModelHandle, BufferSnapshot)>, + SearchMatchCandidateIndex, + )>, + ) { + let (buffers_tx, buffers_rx) = smol::channel::bounded(1024); + let (sorted_buffers_tx, sorted_buffers_rx) = futures::channel::oneshot::channel(); + cx.spawn(|this, cx| async move { + let mut buffers = vec![]; + while let Some(entry) = matching_paths_rx.next().await { + buffers.push(entry); + } + buffers.sort_by_key(|candidate| candidate.path()); + let matching_paths = buffers.clone(); + let _ = sorted_buffers_tx.send(buffers); + for (index, candidate) in matching_paths.into_iter().enumerate() { + if buffers_tx.is_closed() { + break; + } + let this = this.clone(); + let buffers_tx = buffers_tx.clone(); + cx.spawn(|mut cx| async move { + let buffer = match candidate { + SearchMatchCandidate::OpenBuffer { buffer, .. } => Some(buffer), + SearchMatchCandidate::Path { worktree_id, path } => this + .update(&mut cx, |this, cx| { + this.open_buffer((worktree_id, path), cx) + }) + .await + .log_err(), + }; + if let Some(buffer) = buffer { + let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); + buffers_tx + .send((Some((buffer, snapshot)), index)) + .await + .log_err(); + } else { + buffers_tx.send((None, index)).await.log_err(); + } + + Ok::<_, anyhow::Error>(()) + }) + .detach(); + } + }) + .detach(); + (sorted_buffers_rx, buffers_rx) + } + pub fn find_or_create_local_worktree( &mut self, abs_path: impl AsRef, @@ -5930,6 +6193,7 @@ impl Project { let collaborator = Collaborator::from_proto(collaborator)?; this.update(&mut cx, |this, cx| { this.shared_buffers.remove(&collaborator.peer_id); + cx.emit(Event::CollaboratorJoined(collaborator.peer_id)); this.collaborators .insert(collaborator.peer_id, collaborator); cx.notify(); @@ -6813,6 +7077,40 @@ impl Project { })) } + async fn handle_resolve_inlay_hint( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let proto_hint = envelope + .payload + .hint + .expect("incorrect protobuf resolve inlay hint message: missing the inlay hint"); + let hint = InlayHints::proto_to_project_hint(proto_hint) + .context("resolved proto inlay hint conversion")?; + let buffer = this.update(&mut cx, |this, cx| { + this.opened_buffers + .get(&envelope.payload.buffer_id) + .and_then(|buffer| buffer.upgrade(cx)) + .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id)) + })?; + let response_hint = this + .update(&mut cx, |project, cx| { + project.resolve_inlay_hint( + hint, + buffer, + LanguageServerId(envelope.payload.language_server_id as usize), + cx, + ) + }) + .await + .context("inlay hints fetch")?; + Ok(proto::ResolveInlayHintResponse { + hint: Some(InlayHints::project_to_proto_hint(response_hint)), + }) + } + async fn handle_refresh_inlay_hints( this: ModelHandle, _: TypedEnvelope, @@ -6891,17 +7189,17 @@ impl Project { ) -> Result { let peer_id = envelope.original_sender_id()?; let query = SearchQuery::from_proto(envelope.payload)?; - let result = this - .update(&mut cx, |this, cx| this.search(query, cx)) - .await?; + let mut result = this.update(&mut cx, |this, cx| this.search(query, cx)); - this.update(&mut cx, |this, cx| { + cx.spawn(|mut cx| async move { let mut locations = Vec::new(); - for (buffer, ranges) in result { + while let Some((buffer, ranges)) = result.next().await { for range in ranges { let start = serialize_anchor(&range.start); let end = serialize_anchor(&range.end); - let buffer_id = this.create_buffer_for_peer(&buffer, peer_id, cx); + let buffer_id = this.update(&mut cx, |this, cx| { + this.create_buffer_for_peer(&buffer, peer_id, cx) + }); locations.push(proto::Location { buffer_id, start: Some(start), @@ -6911,6 +7209,7 @@ impl Project { } Ok(proto::SearchProjectResponse { locations }) }) + .await } async fn handle_open_buffer_for_symbol( @@ -7576,7 +7875,7 @@ impl Project { self.language_servers_for_buffer(buffer, cx).next() } - fn language_server_for_buffer( + pub fn language_server_for_buffer( &self, buffer: &Buffer, server_id: LanguageServerId, @@ -7756,6 +8055,7 @@ impl Collaborator { Ok(Self { peer_id: message.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?, replica_id: message.replica_id as ReplicaId, + user_id: message.user_id as UserId, }) } } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 259c10ca057c8bb29ad5b2d805107eb982239441..7c5983a0a90de924384c37871720246dce5f6983 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1,4 +1,4 @@ -use crate::{search::PathMatcher, worktree::WorktreeHandle, Event, *}; +use crate::{search::PathMatcher, worktree::WorktreeModelHandle, Event, *}; use fs::{FakeFs, LineEnding, RealFs}; use futures::{future, StreamExt}; use gpui::{executor::Deterministic, test::subscribe, AppContext}; @@ -3953,11 +3953,12 @@ async fn search( query: SearchQuery, cx: &mut gpui::TestAppContext, ) -> Result>>> { - let results = project - .update(cx, |project, cx| project.search(query, cx)) - .await?; - - Ok(results + let mut search_rx = project.update(cx, |project, cx| project.search(query, cx)); + let mut result = HashMap::default(); + while let Some((buffer, range)) = search_rx.next().await { + result.entry(buffer).or_insert(range); + } + Ok(result .into_iter() .map(|(buffer, ranges)| { buffer.read_with(cx, |buffer, _| { diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index db5996829fa278db04e793d751d02ace086594e3..68a043131684619d0a2cb12e2d18f52fd4e3ebaa 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -1,7 +1,13 @@ use crate::Project; use gpui::{AnyWindowHandle, ModelContext, ModelHandle, WeakModelHandle}; -use std::path::PathBuf; -use terminal::{Terminal, TerminalBuilder, TerminalSettings}; +use std::path::{Path, PathBuf}; +use terminal::{ + terminal_settings::{self, TerminalSettings, VenvSettingsContent}, + Terminal, TerminalBuilder, +}; + +#[cfg(target_os = "macos")] +use std::os::unix::ffi::OsStrExt; pub struct Terminals { pub(crate) local_handles: Vec>, @@ -20,10 +26,12 @@ impl Project { )); } else { let settings = settings::get::(cx); + let python_settings = settings.detect_venv.clone(); + let shell = settings.shell.clone(); let terminal = TerminalBuilder::new( working_directory.clone(), - settings.shell.clone(), + shell.clone(), settings.env.clone(), Some(settings.blinking.clone()), settings.alternate_scroll, @@ -47,6 +55,15 @@ impl Project { }) .detach(); + if let Some(python_settings) = &python_settings.as_option() { + let activate_script_path = + self.find_activate_script_path(&python_settings, working_directory); + self.activate_python_virtual_environment( + activate_script_path, + &terminal_handle, + cx, + ); + } terminal_handle }); @@ -54,6 +71,50 @@ impl Project { } } + pub fn find_activate_script_path( + &mut self, + settings: &VenvSettingsContent, + working_directory: Option, + ) -> Option { + // When we are unable to resolve the working directory, the terminal builder + // defaults to '/'. We should probably encode this directly somewhere, but for + // now, let's just hard code it here. + let working_directory = working_directory.unwrap_or_else(|| Path::new("/").to_path_buf()); + let activate_script_name = match settings.activate_script { + terminal_settings::ActivateScript::Default => "activate", + terminal_settings::ActivateScript::Csh => "activate.csh", + terminal_settings::ActivateScript::Fish => "activate.fish", + }; + + for virtual_environment_name in settings.directories { + let mut path = working_directory.join(virtual_environment_name); + path.push("bin/"); + path.push(activate_script_name); + + if path.exists() { + return Some(path); + } + } + + None + } + + fn activate_python_virtual_environment( + &mut self, + activate_script: Option, + terminal_handle: &ModelHandle, + cx: &mut ModelContext, + ) { + if let Some(activate_script) = activate_script { + // Paths are not strings so we need to jump through some hoops to format the command without `format!` + let mut command = Vec::from("source ".as_bytes()); + command.extend_from_slice(activate_script.as_os_str().as_bytes()); + command.push(b'\n'); + + terminal_handle.update(cx, |this, _| this.input_bytes(command)); + } + } + pub fn local_terminal_handles(&self) -> &Vec> { &self.terminals.local_handles } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 9e30796bbc58e3176ef73e8acb95358565fc6b34..e6e0f37cc74b317c5d9de9adda90f9820c230777 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -2317,9 +2317,10 @@ impl BackgroundScannerState { for changed_path in changed_paths { let Some(dot_git_dir) = changed_path .ancestors() - .find(|ancestor| ancestor.file_name() == Some(&*DOT_GIT)) else { - continue; - }; + .find(|ancestor| ancestor.file_name() == Some(&*DOT_GIT)) + else { + continue; + }; // Avoid processing the same repository multiple times, if multiple paths // within it have changed. @@ -2348,7 +2349,10 @@ impl BackgroundScannerState { let Some(work_dir) = self .snapshot .entry_for_id(entry_id) - .map(|entry| RepositoryWorkDirectory(entry.path.clone())) else { continue }; + .map(|entry| RepositoryWorkDirectory(entry.path.clone())) + else { + continue; + }; log::info!("reload git repository {:?}", dot_git_dir); let repository = repository.repo_ptr.lock(); @@ -4026,7 +4030,7 @@ struct UpdateIgnoreStatusJob { scan_queue: Sender, } -pub trait WorktreeHandle { +pub trait WorktreeModelHandle { #[cfg(any(test, feature = "test-support"))] fn flush_fs_events<'a>( &self, @@ -4034,7 +4038,7 @@ pub trait WorktreeHandle { ) -> futures::future::LocalBoxFuture<'a, ()>; } -impl WorktreeHandle for ModelHandle { +impl WorktreeModelHandle for ModelHandle { // When the worktree's FS event stream sometimes delivers "redundant" events for FS changes that // occurred before the worktree was constructed. These events can cause the worktree to perform // extra directory scans, and emit extra scan-state notifications. diff --git a/crates/project/src/worktree_tests.rs b/crates/project/src/worktree_tests.rs index 6f5b3635096e334b57357f633370782f6a2a965a..4253f45b0ce912412b0f9716474f92d0f875f026 100644 --- a/crates/project/src/worktree_tests.rs +++ b/crates/project/src/worktree_tests.rs @@ -1,5 +1,5 @@ use crate::{ - worktree::{Event, Snapshot, WorktreeHandle}, + worktree::{Event, Snapshot, WorktreeModelHandle}, Entry, EntryKind, PathChange, Worktree, }; use anyhow::Result; diff --git a/crates/quick_action_bar/src/quick_action_bar.rs b/crates/quick_action_bar/src/quick_action_bar.rs index 3055399c139ddff1c469d58c4be979943c77b345..8595645e59b873f9cf3b5d7d4c596475eab4fb0e 100644 --- a/crates/quick_action_bar/src/quick_action_bar.rs +++ b/crates/quick_action_bar/src/quick_action_bar.rs @@ -40,7 +40,9 @@ impl View for QuickActionBar { } fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement { - let Some(editor) = self.active_editor() else { return Empty::new().into_any(); }; + let Some(editor) = self.active_editor() else { + return Empty::new().into_any(); + }; let inlay_hints_enabled = editor.read(cx).inlay_hints_enabled(); let mut bar = Flex::row().with_child(render_quick_action_bar_button( @@ -150,9 +152,10 @@ impl ToolbarItemView for QuickActionBar { cx.notify(); } })); + ToolbarItemLocation::PrimaryRight { flex: None } + } else { + ToolbarItemLocation::Hidden } - - ToolbarItemLocation::PrimaryRight { flex: None } } None => { self.active_item = None; diff --git a/crates/rpc/Cargo.toml b/crates/rpc/Cargo.toml index 008fa9c316e78c0e79e6cc7170fe5a7d837c5de3..3c307be4fbd299bb0a5fa65813da56f69a77641b 100644 --- a/crates/rpc/Cargo.toml +++ b/crates/rpc/Cargo.toml @@ -23,7 +23,7 @@ async-tungstenite = "0.16" base64 = "0.13" futures.workspace = true parking_lot.workspace = true -prost = "0.8" +prost.workspace = true rand.workspace = true rsa = "0.4" serde.workspace = true diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index caa5efd2cb10271ab3320f76cbc0e38fce257764..94d6075ecfc9c714168d3ef8a1d2462576eafbbb 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -128,6 +128,8 @@ message Envelope { InlayHints inlay_hints = 116; InlayHintsResponse inlay_hints_response = 117; + ResolveInlayHint resolve_inlay_hint = 137; + ResolveInlayHintResponse resolve_inlay_hint_response = 138; RefreshInlayHints refresh_inlay_hints = 118; CreateChannel create_channel = 119; @@ -142,6 +144,13 @@ message Envelope { GetChannelMembersResponse get_channel_members_response = 128; SetChannelMemberAdmin set_channel_member_admin = 129; RenameChannel rename_channel = 130; + + JoinChannelBuffer join_channel_buffer = 131; + JoinChannelBufferResponse join_channel_buffer_response = 132; + UpdateChannelBuffer update_channel_buffer = 133; + LeaveChannelBuffer leave_channel_buffer = 134; + AddChannelBufferCollaborator add_channel_buffer_collaborator = 135; + RemoveChannelBufferCollaborator remove_channel_buffer_collaborator = 136; } } @@ -411,6 +420,16 @@ message RemoveProjectCollaborator { PeerId peer_id = 2; } +message AddChannelBufferCollaborator { + uint64 channel_id = 1; + Collaborator collaborator = 2; +} + +message RemoveChannelBufferCollaborator { + uint64 channel_id = 1; + PeerId peer_id = 2; +} + message GetDefinition { uint64 project_id = 1; uint64 buffer_id = 2; @@ -540,6 +559,11 @@ message UpdateBuffer { repeated Operation operations = 3; } +message UpdateChannelBuffer { + uint64 channel_id = 1; + repeated Operation operations = 2; +} + message UpdateBufferFile { uint64 project_id = 1; uint64 buffer_id = 2; @@ -732,6 +756,7 @@ message InlayHint { bool padding_left = 4; bool padding_right = 5; InlayHintTooltip tooltip = 6; + ResolveState resolve_state = 7; } message InlayHintLabel { @@ -748,7 +773,10 @@ message InlayHintLabelParts { message InlayHintLabelPart { string value = 1; InlayHintLabelPartTooltip tooltip = 2; - Location location = 3; + optional string location_url = 3; + PointUtf16 location_range_start = 4; + PointUtf16 location_range_end = 5; + optional uint64 language_server_id = 6; } message InlayHintTooltip { @@ -765,12 +793,39 @@ message InlayHintLabelPartTooltip { } } +message ResolveState { + State state = 1; + LspResolveState lsp_resolve_state = 2; + + enum State { + Resolved = 0; + CanResolve = 1; + Resolving = 2; + } + + message LspResolveState { + string value = 1; + uint64 server_id = 2; + } +} + +message ResolveInlayHint { + uint64 project_id = 1; + uint64 buffer_id = 2; + uint64 language_server_id = 3; + InlayHint hint = 4; +} + +message ResolveInlayHintResponse { + InlayHint hint = 1; +} + message RefreshInlayHints { uint64 project_id = 1; } message MarkupContent { - string kind = 1; + bool is_markdown = 1; string value = 2; } @@ -948,6 +1003,22 @@ message RenameChannel { string name = 2; } +message JoinChannelBuffer { + uint64 channel_id = 1; +} + +message JoinChannelBufferResponse { + uint64 buffer_id = 1; + uint32 replica_id = 2; + string base_text = 3; + repeated Operation operations = 4; + repeated Collaborator collaborators = 5; +} + +message LeaveChannelBuffer { + uint64 channel_id = 1; +} + message RespondToChannelInvite { uint64 channel_id = 1; bool accept = 2; @@ -1043,6 +1114,7 @@ message GetPrivateUserInfo {} message GetPrivateUserInfoResponse { string metrics_id = 1; bool staff = 2; + repeated string flags = 3; } // Entities @@ -1082,6 +1154,7 @@ message View { oneof variant { Editor editor = 3; + ChannelView channel_view = 4; } message Editor { @@ -1094,6 +1167,11 @@ message View { float scroll_x = 7; float scroll_y = 8; } + + message ChannelView { + uint64 channel_id = 1; + Editor editor = 2; + } } message Collaborator { @@ -1144,7 +1222,6 @@ enum GitStatus { Conflict = 2; } - message BufferState { uint64 id = 1; optional File file = 2; diff --git a/crates/rpc/src/peer.rs b/crates/rpc/src/peer.rs index 72ddfa567b5822540632bce4bf70d26fe192281c..91b914f169a86b974232b5e8f3e988d36cb64d07 100644 --- a/crates/rpc/src/peer.rs +++ b/crates/rpc/src/peer.rs @@ -171,12 +171,12 @@ impl Peer { let this = self.clone(); let response_channels = connection_state.response_channels.clone(); let handle_io = async move { - tracing::debug!(%connection_id, "handle io future: start"); + tracing::trace!(%connection_id, "handle io future: start"); let _end_connection = util::defer(|| { response_channels.lock().take(); this.connections.write().remove(&connection_id); - tracing::debug!(%connection_id, "handle io future: end"); + tracing::trace!(%connection_id, "handle io future: end"); }); // Send messages on this frequency so the connection isn't closed. @@ -188,68 +188,68 @@ impl Peer { futures::pin_mut!(receive_timeout); loop { - tracing::debug!(%connection_id, "outer loop iteration start"); + tracing::trace!(%connection_id, "outer loop iteration start"); let read_message = reader.read().fuse(); futures::pin_mut!(read_message); loop { - tracing::debug!(%connection_id, "inner loop iteration start"); + tracing::trace!(%connection_id, "inner loop iteration start"); futures::select_biased! { outgoing = outgoing_rx.next().fuse() => match outgoing { Some(outgoing) => { - tracing::debug!(%connection_id, "outgoing rpc message: writing"); + tracing::trace!(%connection_id, "outgoing rpc message: writing"); futures::select_biased! { result = writer.write(outgoing).fuse() => { - tracing::debug!(%connection_id, "outgoing rpc message: done writing"); + tracing::trace!(%connection_id, "outgoing rpc message: done writing"); result.context("failed to write RPC message")?; - tracing::debug!(%connection_id, "keepalive interval: resetting after sending message"); + tracing::trace!(%connection_id, "keepalive interval: resetting after sending message"); keepalive_timer.set(create_timer(KEEPALIVE_INTERVAL).fuse()); } _ = create_timer(WRITE_TIMEOUT).fuse() => { - tracing::debug!(%connection_id, "outgoing rpc message: writing timed out"); + tracing::trace!(%connection_id, "outgoing rpc message: writing timed out"); Err(anyhow!("timed out writing message"))?; } } } None => { - tracing::debug!(%connection_id, "outgoing rpc message: channel closed"); + tracing::trace!(%connection_id, "outgoing rpc message: channel closed"); return Ok(()) }, }, _ = keepalive_timer => { - tracing::debug!(%connection_id, "keepalive interval: pinging"); + tracing::trace!(%connection_id, "keepalive interval: pinging"); futures::select_biased! { result = writer.write(proto::Message::Ping).fuse() => { - tracing::debug!(%connection_id, "keepalive interval: done pinging"); + tracing::trace!(%connection_id, "keepalive interval: done pinging"); result.context("failed to send keepalive")?; - tracing::debug!(%connection_id, "keepalive interval: resetting after pinging"); + tracing::trace!(%connection_id, "keepalive interval: resetting after pinging"); keepalive_timer.set(create_timer(KEEPALIVE_INTERVAL).fuse()); } _ = create_timer(WRITE_TIMEOUT).fuse() => { - tracing::debug!(%connection_id, "keepalive interval: pinging timed out"); + tracing::trace!(%connection_id, "keepalive interval: pinging timed out"); Err(anyhow!("timed out sending keepalive"))?; } } } incoming = read_message => { let incoming = incoming.context("error reading rpc message from socket")?; - tracing::debug!(%connection_id, "incoming rpc message: received"); - tracing::debug!(%connection_id, "receive timeout: resetting"); + tracing::trace!(%connection_id, "incoming rpc message: received"); + tracing::trace!(%connection_id, "receive timeout: resetting"); receive_timeout.set(create_timer(RECEIVE_TIMEOUT).fuse()); if let proto::Message::Envelope(incoming) = incoming { - tracing::debug!(%connection_id, "incoming rpc message: processing"); + tracing::trace!(%connection_id, "incoming rpc message: processing"); futures::select_biased! { result = incoming_tx.send(incoming).fuse() => match result { Ok(_) => { - tracing::debug!(%connection_id, "incoming rpc message: processed"); + tracing::trace!(%connection_id, "incoming rpc message: processed"); } Err(_) => { - tracing::debug!(%connection_id, "incoming rpc message: channel closed"); + tracing::trace!(%connection_id, "incoming rpc message: channel closed"); return Ok(()) } }, _ = create_timer(WRITE_TIMEOUT).fuse() => { - tracing::debug!(%connection_id, "incoming rpc message: processing timed out"); + tracing::trace!(%connection_id, "incoming rpc message: processing timed out"); Err(anyhow!("timed out processing incoming message"))? } } @@ -257,7 +257,7 @@ impl Peer { break; }, _ = receive_timeout => { - tracing::debug!(%connection_id, "receive timeout: delay between messages too long"); + tracing::trace!(%connection_id, "receive timeout: delay between messages too long"); Err(anyhow!("delay between messages too long"))? } } @@ -274,13 +274,13 @@ impl Peer { let response_channels = response_channels.clone(); async move { let message_id = incoming.id; - tracing::debug!(?incoming, "incoming message future: start"); + tracing::trace!(?incoming, "incoming message future: start"); let _end = util::defer(move || { - tracing::debug!(%connection_id, message_id, "incoming message future: end"); + tracing::trace!(%connection_id, message_id, "incoming message future: end"); }); if let Some(responding_to) = incoming.responding_to { - tracing::debug!( + tracing::trace!( %connection_id, message_id, responding_to, @@ -290,7 +290,7 @@ impl Peer { if let Some(tx) = channel { let requester_resumed = oneshot::channel(); if let Err(error) = tx.send((incoming, requester_resumed.0)) { - tracing::debug!( + tracing::trace!( %connection_id, message_id, responding_to = responding_to, @@ -299,14 +299,14 @@ impl Peer { ); } - tracing::debug!( + tracing::trace!( %connection_id, message_id, responding_to, "incoming response: waiting to resume requester" ); let _ = requester_resumed.1.await; - tracing::debug!( + tracing::trace!( %connection_id, message_id, responding_to, @@ -323,7 +323,7 @@ impl Peer { None } else { - tracing::debug!(%connection_id, message_id, "incoming message: received"); + tracing::trace!(%connection_id, message_id, "incoming message: received"); proto::build_typed_envelope(connection_id, incoming).or_else(|| { tracing::error!( %connection_id, diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 92732b00b5dc19ab16136c1ac9511a54f6d2e932..2e4dce01e1a3bf5789206c80b3a4574f6e198c0d 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -197,6 +197,8 @@ messages!( (OnTypeFormattingResponse, Background), (InlayHints, Background), (InlayHintsResponse, Background), + (ResolveInlayHint, Background), + (ResolveInlayHintResponse, Background), (RefreshInlayHints, Foreground), (Ping, Foreground), (PrepareRename, Background), @@ -248,7 +250,13 @@ messages!( (GetPrivateUserInfo, Foreground), (GetPrivateUserInfoResponse, Foreground), (GetChannelMembers, Foreground), - (GetChannelMembersResponse, Foreground) + (GetChannelMembersResponse, Foreground), + (JoinChannelBuffer, Foreground), + (JoinChannelBufferResponse, Foreground), + (LeaveChannelBuffer, Background), + (UpdateChannelBuffer, Foreground), + (RemoveChannelBufferCollaborator, Foreground), + (AddChannelBufferCollaborator, Foreground), ); request_messages!( @@ -293,6 +301,7 @@ request_messages!( (PrepareRename, PrepareRenameResponse), (OnTypeFormatting, OnTypeFormattingResponse), (InlayHints, InlayHintsResponse), + (ResolveInlayHint, ResolveInlayHintResponse), (RefreshInlayHints, Ack), (ReloadBuffers, ReloadBuffersResponse), (RequestContact, Ack), @@ -315,6 +324,8 @@ request_messages!( (UpdateParticipantLocation, Ack), (UpdateProject, Ack), (UpdateWorktree, Ack), + (JoinChannelBuffer, JoinChannelBufferResponse), + (LeaveChannelBuffer, Ack) ); entity_messages!( @@ -347,6 +358,7 @@ entity_messages!( PerformRename, OnTypeFormatting, InlayHints, + ResolveInlayHint, RefreshInlayHints, PrepareRename, ReloadBuffers, @@ -370,6 +382,13 @@ entity_messages!( UpdateDiffBase ); +entity_messages!( + channel_id, + UpdateChannelBuffer, + RemoveChannelBufferCollaborator, + AddChannelBufferCollaborator +); + const KIB: usize = 1024; const MIB: usize = KIB * 1024; const MAX_BUFFER_LEN: usize = MIB; diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index 3cb8b6bffa2ca1549ca854db39e46ef8fc8634a7..bc9dd6f80ba039bb705e3d1518c737ba56c969b9 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 60; +pub const PROTOCOL_VERSION: u32 = 61; diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 4078cb572d6dcc57320f04d1bf7e13504dcbd521..78729df936c15140d034ce29a6c4ccb108c46deb 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1,6 +1,6 @@ use crate::{ history::SearchHistory, - mode::{next_mode, SearchMode}, + mode::{next_mode, SearchMode, Side}, search_bar::{render_nav_button, render_search_mode_button}, CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord, @@ -156,11 +156,12 @@ impl View for BufferSearchBar { self.query_editor.update(cx, |editor, cx| { editor.set_placeholder_text(new_placeholder_text, cx); }); - let search_button_for_mode = |mode, cx: &mut ViewContext| { + let search_button_for_mode = |mode, side, cx: &mut ViewContext| { let is_active = self.current_mode == mode; render_search_mode_button( mode, + side, is_active, move |_, this, cx| { this.activate_search_mode(mode, cx); @@ -212,20 +213,11 @@ impl View for BufferSearchBar { ) }; - let icon_style = theme.search.editor_icon.clone(); - let nav_column = Flex::row() - .with_child(self.render_action_button("Select All", cx)) - .with_child(nav_button_for_direction("<", Direction::Prev, cx)) - .with_child(nav_button_for_direction(">", Direction::Next, cx)) - .with_child(Flex::row().with_children(match_count)) - .constrained() - .with_height(theme.search.search_bar_row_height); - - let query = Flex::row() + let query_column = Flex::row() .with_child( - Svg::for_style(icon_style.icon) + Svg::for_style(theme.search.editor_icon.clone().icon) .contained() - .with_style(icon_style.container), + .with_style(theme.search.editor_icon.clone().container), ) .with_child(ChildView::new(&self.query_editor, cx).flex(1., true)) .with_child( @@ -244,49 +236,45 @@ impl View for BufferSearchBar { .contained(), ) .align_children_center() - .flex(1., true); - let editor_column = Flex::row() - .with_child( - query - .contained() - .with_style(query_container_style) - .constrained() - .with_min_width(theme.search.editor.min_width) - .with_max_width(theme.search.editor.max_width) - .with_height(theme.search.search_bar_row_height) - .flex(1., false), - ) .contained() + .with_style(query_container_style) .constrained() + .with_min_width(theme.search.editor.min_width) + .with_max_width(theme.search.editor.max_width) .with_height(theme.search.search_bar_row_height) .flex(1., false); + let mode_column = Flex::row() - .with_child( - Flex::row() - .with_child(search_button_for_mode(SearchMode::Text, cx)) - .with_child(search_button_for_mode(SearchMode::Regex, cx)) - .contained() - .with_style(theme.search.modes_container), - ) - .with_child(super::search_bar::render_close_button( - "Dismiss Buffer Search", - &theme.search, + .with_child(search_button_for_mode( + SearchMode::Text, + Some(Side::Left), cx, - |_, this, cx| this.dismiss(&Default::default(), cx), - Some(Box::new(Dismiss)), )) + .with_child(search_button_for_mode( + SearchMode::Regex, + Some(Side::Right), + cx, + )) + .contained() + .with_style(theme.search.modes_container) + .constrained() + .with_height(theme.search.search_bar_row_height); + + let nav_column = Flex::row() + .with_child(self.render_action_button("all", cx)) + .with_child(Flex::row().with_children(match_count)) + .with_child(nav_button_for_direction("<", Direction::Prev, cx)) + .with_child(nav_button_for_direction(">", Direction::Next, cx)) .constrained() .with_height(theme.search.search_bar_row_height) - .aligned() - .right() .flex_float(); + Flex::row() - .with_child(editor_column) - .with_child(nav_column) + .with_child(query_column) .with_child(mode_column) + .with_child(nav_column) .contained() .with_style(theme.search.container) - .aligned() .into_any_named("search bar") } } @@ -340,8 +328,9 @@ impl ToolbarItemView for BufferSearchBar { ToolbarItemLocation::Hidden } } + fn row_count(&self, _: &ViewContext) -> usize { - 2 + 1 } } @@ -837,6 +826,7 @@ mod tests { let buffer = cx.add_model(|cx| { Buffer::new( 0, + cx.model_id() as u64, r#" A regular expression (shortened as regex or regexp;[1] also referred to as rational expression[2][3]) is a sequence of characters that specifies a search @@ -844,7 +834,6 @@ mod tests { for "find" or "find and replace" operations on strings, or for input validation. "# .unindent(), - cx, ) }); let window = cx.add_window(|_| EmptyView); @@ -1225,7 +1214,7 @@ mod tests { expected_query_matches_count > 1, "Should pick a query with multiple results" ); - let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, buffer_text)); let window = cx.add_window(|_| EmptyView); let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx)); @@ -1412,7 +1401,7 @@ mod tests { for "find" or "find and replace" operations on strings, or for input validation. "# .unindent(); - let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, buffer_text)); let window = cx.add_window(|_| EmptyView); let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx)); diff --git a/crates/search/src/mode.rs b/crates/search/src/mode.rs index 2c180be761ef9e4132a35d6cb47e7962e431e90c..8afc2bd3f496cc502f5ffd53fec5b05973108501 100644 --- a/crates/search/src/mode.rs +++ b/crates/search/src/mode.rs @@ -48,41 +48,18 @@ impl SearchMode { SearchMode::Regex => Box::new(ActivateRegexMode), } } - - pub(crate) fn border_right(&self) -> bool { - match self { - SearchMode::Regex => true, - SearchMode::Text => true, - SearchMode::Semantic => true, - } - } - - pub(crate) fn border_left(&self) -> bool { - match self { - SearchMode::Text => true, - _ => false, - } - } - - pub(crate) fn button_side(&self) -> Option { - match self { - SearchMode::Text => Some(Side::Left), - SearchMode::Semantic => None, - SearchMode::Regex => Some(Side::Right), - } - } } pub(crate) fn next_mode(mode: &SearchMode, semantic_enabled: bool) -> SearchMode { - let next_text_state = if semantic_enabled { - SearchMode::Semantic - } else { - SearchMode::Regex - }; - match mode { - SearchMode::Text => next_text_state, - SearchMode::Semantic => SearchMode::Regex, - SearchMode::Regex => SearchMode::Text, + SearchMode::Text => SearchMode::Regex, + SearchMode::Regex => { + if semantic_enabled { + SearchMode::Semantic + } else { + SearchMode::Text + } + } + SearchMode::Semantic => SearchMode::Text, } } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 52247b9bc0d1cc6b55fb876ecc0549a29eb4d1a6..c2ecde4ce585081cf0f1d76a3c5be1c61d10b8cc 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1,6 +1,6 @@ use crate::{ history::SearchHistory, - mode::SearchMode, + mode::{SearchMode, Side}, search_bar::{render_nav_button, render_option_button_icon, render_search_mode_button}, ActivateRegexMode, CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord, @@ -185,28 +185,26 @@ impl ProjectSearch { self.active_query = Some(query); self.match_ranges.clear(); self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move { - let matches = search.await.log_err()?; + let mut matches = search; let this = this.upgrade(&cx)?; - let mut matches = matches.into_iter().collect::>(); - let (_task, mut match_ranges) = this.update(&mut cx, |this, cx| { + this.update(&mut cx, |this, cx| { this.match_ranges.clear(); + this.excerpts.update(cx, |this, cx| this.clear(cx)); this.no_results = Some(true); - matches.sort_by_key(|(buffer, _)| buffer.read(cx).file().map(|file| file.path())); - this.excerpts.update(cx, |excerpts, cx| { - excerpts.clear(cx); - excerpts.stream_excerpts_with_context_lines(matches, 1, cx) - }) }); - while let Some(match_range) = match_ranges.next().await { - this.update(&mut cx, |this, cx| { - this.match_ranges.push(match_range); - while let Ok(Some(match_range)) = match_ranges.try_next() { - this.match_ranges.push(match_range); - } + while let Some((buffer, anchors)) = matches.next().await { + let mut ranges = this.update(&mut cx, |this, cx| { this.no_results = Some(false); - cx.notify(); + this.excerpts.update(cx, |excerpts, cx| { + excerpts.stream_excerpts_with_context_lines(buffer, anchors, 1, cx) + }) }); + + while let Some(range) = ranges.next().await { + this.update(&mut cx, |this, _| this.match_ranges.push(range)); + } + this.update(&mut cx, |_, cx| cx.notify()); } this.update(&mut cx, |this, cx| { @@ -238,29 +236,31 @@ impl ProjectSearch { self.no_results = Some(true); self.pending_search = Some(cx.spawn(|this, mut cx| async move { let results = search?.await.log_err()?; + let matches = results + .into_iter() + .map(|result| (result.buffer, vec![result.range.start..result.range.start])); - let (_task, mut match_ranges) = this.update(&mut cx, |this, cx| { + this.update(&mut cx, |this, cx| { this.excerpts.update(cx, |excerpts, cx| { excerpts.clear(cx); - - let matches = results - .into_iter() - .map(|result| (result.buffer, vec![result.range.start..result.range.start])) - .collect(); - - excerpts.stream_excerpts_with_context_lines(matches, 3, cx) }) }); - - while let Some(match_range) = match_ranges.next().await { - this.update(&mut cx, |this, cx| { - this.match_ranges.push(match_range); - while let Ok(Some(match_range)) = match_ranges.try_next() { - this.match_ranges.push(match_range); - } + for (buffer, ranges) in matches { + let mut match_ranges = this.update(&mut cx, |this, cx| { this.no_results = Some(false); - cx.notify(); + this.excerpts.update(cx, |excerpts, cx| { + excerpts.stream_excerpts_with_context_lines(buffer, ranges, 3, cx) + }) }); + while let Some(match_range) = match_ranges.next().await { + this.update(&mut cx, |this, cx| { + this.match_ranges.push(match_range); + while let Ok(Some(match_range)) = match_ranges.try_next() { + this.match_ranges.push(match_range); + } + cx.notify(); + }); + } } this.update(&mut cx, |this, cx| { @@ -640,6 +640,7 @@ impl ProjectSearchView { self.search_options = SearchOptions::none(); let project = self.model.read(cx).project.clone(); + let index_task = semantic_index.update(cx, |semantic_index, cx| { semantic_index.index_project(project, cx) }); @@ -884,7 +885,9 @@ impl ProjectSearchView { if !dir_entry.is_dir() { return; } - let Some(filter_str) = dir_entry.path.to_str() else { return; }; + let Some(filter_str) = dir_entry.path.to_str() else { + return; + }; let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx)); let search = cx.add_view(|cx| ProjectSearchView::new(model, cx)); @@ -893,6 +896,7 @@ impl ProjectSearchView { search .included_files_editor .update(cx, |editor, cx| editor.set_text(filter_str, cx)); + search.filters_enabled = true; search.focus_query_editor(cx) }); } @@ -1420,8 +1424,13 @@ impl View for ProjectSearchBar { }, cx, ); + let search = _search.read(cx); + let is_semantic_available = SemanticIndex::enabled(cx); let is_semantic_disabled = search.semantic_state.is_none(); + let icon_style = theme.search.editor_icon.clone(); + let is_active = search.active_match_index.is_some(); + let render_option_button_icon = |path, option, cx: &mut ViewContext| { crate::search_bar::render_option_button_icon( self.is_option_enabled(option, cx), @@ -1447,28 +1456,23 @@ impl View for ProjectSearchBar { render_option_button_icon("icons/word_search_12.svg", SearchOptions::WHOLE_WORD, cx) }); - let search = _search.read(cx); - let icon_style = theme.search.editor_icon.clone(); - - // Editor Functionality - let query = Flex::row() - .with_child( - Svg::for_style(icon_style.icon) - .contained() - .with_style(icon_style.container), - ) - .with_child(ChildView::new(&search.query_editor, cx).flex(1., true)) - .with_child( - Flex::row() - .with_child(filter_button) - .with_children(case_sensitive) - .with_children(whole_word) - .flex(1., false) - .constrained() - .contained(), + let search_button_for_mode = |mode, side, cx: &mut ViewContext| { + let is_active = if let Some(search) = self.active_project_search.as_ref() { + let search = search.read(cx); + search.current_mode == mode + } else { + false + }; + render_search_mode_button( + mode, + side, + is_active, + move |_, this, cx| { + this.activate_search_mode(mode, cx); + }, + cx, ) - .align_children_center() - .flex(1., true); + }; let search = _search.read(cx); @@ -1486,50 +1490,6 @@ impl View for ProjectSearchBar { theme.search.include_exclude_editor.input.container }; - let included_files_view = ChildView::new(&search.included_files_editor, cx) - .contained() - .flex(1., true); - let excluded_files_view = ChildView::new(&search.excluded_files_editor, cx) - .contained() - .flex(1., true); - let filters = search.filters_enabled.then(|| { - Flex::row() - .with_child( - included_files_view - .contained() - .with_style(include_container_style) - .constrained() - .with_height(theme.search.search_bar_row_height) - .with_min_width(theme.search.include_exclude_editor.min_width) - .with_max_width(theme.search.include_exclude_editor.max_width), - ) - .with_child( - excluded_files_view - .contained() - .with_style(exclude_container_style) - .constrained() - .with_height(theme.search.search_bar_row_height) - .with_min_width(theme.search.include_exclude_editor.min_width) - .with_max_width(theme.search.include_exclude_editor.max_width), - ) - .contained() - .with_padding_top(theme.workspace.toolbar.container.padding.bottom) - }); - - let editor_column = Flex::column() - .with_child( - query - .contained() - .with_style(query_container_style) - .constrained() - .with_min_width(theme.search.editor.min_width) - .with_max_width(theme.search.editor.max_width) - .with_height(theme.search.search_bar_row_height) - .flex(1., false), - ) - .with_children(filters) - .flex(1., false); - let matches = search.active_match_index.map(|match_ix| { Label::new( format!( @@ -1544,25 +1504,81 @@ impl View for ProjectSearchBar { .aligned() }); - let search_button_for_mode = |mode, cx: &mut ViewContext| { - let is_active = if let Some(search) = self.active_project_search.as_ref() { - let search = search.read(cx); - search.current_mode == mode - } else { - false - }; - render_search_mode_button( - mode, - is_active, - move |_, this, cx| { - this.activate_search_mode(mode, cx); - }, - cx, + let query_column = Flex::column() + .with_spacing(theme.search.search_row_spacing) + .with_child( + Flex::row() + .with_child( + Svg::for_style(icon_style.icon) + .contained() + .with_style(icon_style.container), + ) + .with_child(ChildView::new(&search.query_editor, cx).flex(1., true)) + .with_child( + Flex::row() + .with_child(filter_button) + .with_children(case_sensitive) + .with_children(whole_word) + .flex(1., false) + .constrained() + .contained(), + ) + .align_children_center() + .contained() + .with_style(query_container_style) + .constrained() + .with_min_width(theme.search.editor.min_width) + .with_max_width(theme.search.editor.max_width) + .with_height(theme.search.search_bar_row_height) + .flex(1., false), ) - }; - let is_active = search.active_match_index.is_some(); - let semantic_index = SemanticIndex::enabled(cx) - .then(|| search_button_for_mode(SearchMode::Semantic, cx)); + .with_children(search.filters_enabled.then(|| { + Flex::row() + .with_child( + ChildView::new(&search.included_files_editor, cx) + .contained() + .with_style(include_container_style) + .constrained() + .with_height(theme.search.search_bar_row_height) + .flex(1., true), + ) + .with_child( + ChildView::new(&search.excluded_files_editor, cx) + .contained() + .with_style(exclude_container_style) + .constrained() + .with_height(theme.search.search_bar_row_height) + .flex(1., true), + ) + .constrained() + .with_min_width(theme.search.editor.min_width) + .with_max_width(theme.search.editor.max_width) + .flex(1., false) + })) + .flex(1., false); + + let mode_column = + Flex::row() + .with_child(search_button_for_mode( + SearchMode::Text, + Some(Side::Left), + cx, + )) + .with_child(search_button_for_mode( + SearchMode::Regex, + if is_semantic_available { + None + } else { + Some(Side::Right) + }, + cx, + )) + .with_children(is_semantic_available.then(|| { + search_button_for_mode(SearchMode::Semantic, Some(Side::Right), cx) + })) + .contained() + .with_style(theme.search.modes_container); + let nav_button_for_direction = |label, direction, cx: &mut ViewContext| { render_nav_button( label, @@ -1578,43 +1594,17 @@ impl View for ProjectSearchBar { }; let nav_column = Flex::row() + .with_child(Flex::row().with_children(matches)) .with_child(nav_button_for_direction("<", Direction::Prev, cx)) .with_child(nav_button_for_direction(">", Direction::Next, cx)) - .with_child(Flex::row().with_children(matches)) - .constrained() - .with_height(theme.search.search_bar_row_height); - - let mode_column = Flex::row() - .with_child( - Flex::row() - .with_child(search_button_for_mode(SearchMode::Text, cx)) - .with_children(semantic_index) - .with_child(search_button_for_mode(SearchMode::Regex, cx)) - .contained() - .with_style(theme.search.modes_container), - ) - .with_child(super::search_bar::render_close_button( - "Dismiss Project Search", - &theme.search, - cx, - |_, this, cx| { - if let Some(search) = this.active_project_search.as_mut() { - search.update(cx, |_, cx| cx.emit(ViewEvent::Dismiss)) - } - }, - None, - )) .constrained() .with_height(theme.search.search_bar_row_height) - .aligned() - .right() - .top() .flex_float(); Flex::row() - .with_child(editor_column) - .with_child(nav_column) + .with_child(query_column) .with_child(mode_column) + .with_child(nav_column) .contained() .with_style(theme.search.container) .into_any_named("project search") @@ -1634,10 +1624,16 @@ impl ToolbarItemView for ProjectSearchBar { self.subscription = None; self.active_project_search = None; if let Some(search) = active_pane_item.and_then(|i| i.downcast::()) { + search.update(cx, |search, cx| { + if search.current_mode == SearchMode::Semantic { + search.index_project(cx); + } + }); + self.subscription = Some(cx.observe(&search, |_, _, cx| cx.notify())); self.active_project_search = Some(search); ToolbarItemLocation::PrimaryLeft { - flex: Some((1., false)), + flex: Some((1., true)), } } else { ToolbarItemLocation::Hidden @@ -1645,13 +1641,12 @@ impl ToolbarItemView for ProjectSearchBar { } fn row_count(&self, cx: &ViewContext) -> usize { - self.active_project_search - .as_ref() - .map(|search| { - let offset = search.read(cx).filters_enabled as usize; - 2 + offset - }) - .unwrap_or_else(|| 2) + if let Some(search) = self.active_project_search.as_ref() { + if search.read(cx).filters_enabled { + return 2; + } + } + 1 } } diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 8d8c02c8d7e0fbff84a509ba8a9fbf6233965adb..47f7f485c486a86d3ff3725b5e05aedb52305b36 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -2,13 +2,13 @@ use bitflags::bitflags; pub use buffer_search::BufferSearchBar; use gpui::{ actions, - elements::{Component, StyleableComponent, TooltipStyle}, + elements::{Component, SafeStylable, TooltipStyle}, Action, AnyElement, AppContext, Element, View, }; pub use mode::SearchMode; use project::search::SearchQuery; pub use project_search::{ProjectSearchBar, ProjectSearchView}; -use theme::components::{action_button::ActionButton, ComponentExt, ToggleIconButtonStyle}; +use theme::components::{action_button::Button, svg::Svg, ComponentExt, ToggleIconButtonStyle}; pub mod buffer_search; mod history; @@ -89,15 +89,12 @@ impl SearchOptions { tooltip_style: TooltipStyle, button_style: ToggleIconButtonStyle, ) -> AnyElement { - ActionButton::new_dynamic( - self.to_toggle_action(), - format!("Toggle {}", self.label()), - tooltip_style, - ) - .with_contents(theme::components::svg::Svg::new(self.icon())) - .toggleable(active) - .with_style(button_style) - .element() - .into_any() + Button::dynamic_action(self.to_toggle_action()) + .with_tooltip(format!("Toggle {}", self.label()), tooltip_style) + .with_contents(Svg::new(self.icon())) + .toggleable(active) + .with_style(button_style) + .element() + .into_any() } } diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index 7d3c5261ea759c9a58d5442de4630621c87044f9..d1a5a0380a3b3f210706d2b2fb488c6e6f156834 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -13,34 +13,6 @@ use crate::{ SelectNextMatch, SelectPrevMatch, }; -pub(super) fn render_close_button( - tooltip: &'static str, - theme: &theme::Search, - cx: &mut ViewContext, - on_click: impl Fn(MouseClick, &mut V, &mut EventContext) + 'static, - dismiss_action: Option>, -) -> AnyElement { - let tooltip_style = theme::current(cx).tooltip.clone(); - - enum CloseButton {} - MouseEventHandler::new::(0, cx, |state, _| { - let style = theme.dismiss_button.style_for(state); - Svg::new("icons/x_mark_8.svg") - .with_color(style.color) - .constrained() - .with_width(style.icon_width) - .aligned() - .contained() - .with_style(style.container) - .constrained() - .with_height(theme.search_bar_row_height) - }) - .on_click(MouseButton::Left, on_click) - .with_cursor_style(CursorStyle::PointingHand) - .with_tooltip::(0, tooltip.to_string(), dismiss_action, tooltip_style, cx) - .into_any() -} - pub(super) fn render_nav_button( icon: &'static str, direction: Direction, @@ -111,6 +83,7 @@ pub(super) fn render_nav_button( pub(crate) fn render_search_mode_button( mode: SearchMode, + side: Option, is_active: bool, on_click: impl Fn(MouseClick, &mut V, &mut EventContext) + 'static, cx: &mut ViewContext, @@ -119,41 +92,41 @@ pub(crate) fn render_search_mode_button( enum SearchModeButton {} MouseEventHandler::new::(mode.region_id(), cx, |state, cx| { let theme = theme::current(cx); - let mut style = theme + let style = theme .search .mode_button .in_state(is_active) .style_for(state) .clone(); - style.container.border.left = mode.border_left(); - style.container.border.right = mode.border_right(); - let label = Label::new(mode.label(), style.text.clone()) - .aligned() - .contained(); - let mut container_style = style.container.clone(); - if let Some(button_side) = mode.button_side() { + let mut container_style = style.container; + if let Some(button_side) = side { if button_side == Side::Left { + container_style.border.left = true; container_style.corner_radii = CornerRadii { bottom_right: 0., top_right: 0., ..container_style.corner_radii }; - label.with_style(container_style) } else { + container_style.border.left = false; container_style.corner_radii = CornerRadii { bottom_left: 0., top_left: 0., ..container_style.corner_radii }; - label.with_style(container_style) } } else { + container_style.border.left = false; container_style.corner_radii = CornerRadii::default(); - label.with_style(container_style) } - .constrained() - .with_height(theme.search.search_bar_row_height) + + Label::new(mode.label(), style.text) + .aligned() + .contained() + .with_style(container_style) + .constrained() + .with_height(theme.search.search_bar_row_height) }) .on_click(MouseButton::Left, on_click) .with_cursor_style(CursorStyle::PointingHand) diff --git a/crates/semantic_index/Cargo.toml b/crates/semantic_index/Cargo.toml index 3c7a6ff5df6df4b0ae7de9e0e7754a0e0d5850cc..4e817fcbe2c6dc8a6edac00fc51dd9e5be437b8f 100644 --- a/crates/semantic_index/Cargo.toml +++ b/crates/semantic_index/Cargo.toml @@ -38,6 +38,7 @@ parking_lot.workspace = true rand.workspace = true schemars.workspace = true globset.workspace = true +sha1 = "0.10.5" [dev-dependencies] gpui = { path = "../gpui", features = ["test-support"] } diff --git a/crates/semantic_index/src/db.rs b/crates/semantic_index/src/db.rs index e57a5d733fb2a0b2c68dc6e874b8ac050de5e78b..60ecf3b45fef383e73172c11b0b5ee3d7d48d93d 100644 --- a/crates/semantic_index/src/db.rs +++ b/crates/semantic_index/src/db.rs @@ -26,6 +26,9 @@ pub struct FileRecord { #[derive(Debug)] struct Embedding(pub Vec); +#[derive(Debug)] +struct Sha1(pub Vec); + impl FromSql for Embedding { fn column_result(value: ValueRef) -> FromSqlResult { let bytes = value.as_blob()?; @@ -37,6 +40,17 @@ impl FromSql for Embedding { } } +impl FromSql for Sha1 { + fn column_result(value: ValueRef) -> FromSqlResult { + let bytes = value.as_blob()?; + let sha1: Result, Box> = bincode::deserialize(bytes); + if sha1.is_err() { + return Err(rusqlite::types::FromSqlError::Other(sha1.unwrap_err())); + } + return Ok(Sha1(sha1.unwrap())); + } +} + pub struct VectorDatabase { db: rusqlite::Connection, } @@ -132,6 +146,7 @@ impl VectorDatabase { end_byte INTEGER NOT NULL, name VARCHAR NOT NULL, embedding BLOB NOT NULL, + sha1 BLOB NOT NULL, FOREIGN KEY(file_id) REFERENCES files(id) ON DELETE CASCADE )", [], @@ -182,15 +197,17 @@ impl VectorDatabase { // I imagine we can speed this up with a bulk insert of some kind. for document in documents { let embedding_blob = bincode::serialize(&document.embedding)?; + let sha_blob = bincode::serialize(&document.sha1)?; self.db.execute( - "INSERT INTO documents (file_id, start_byte, end_byte, name, embedding) VALUES (?1, ?2, ?3, ?4, $5)", + "INSERT INTO documents (file_id, start_byte, end_byte, name, embedding, sha1) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", params![ file_id, document.range.start.to_string(), document.range.end.to_string(), document.name, - embedding_blob + embedding_blob, + sha_blob ], )?; } diff --git a/crates/semantic_index/src/embedding.rs b/crates/semantic_index/src/embedding.rs index 77457ec7f6e34961ab2a784ef6f0d8068c4c1dbb..f2269a786a66af3896e0410c5558f3f18d618bd0 100644 --- a/crates/semantic_index/src/embedding.rs +++ b/crates/semantic_index/src/embedding.rs @@ -106,8 +106,8 @@ impl OpenAIEmbeddings { #[async_trait] impl EmbeddingProvider for OpenAIEmbeddings { async fn embed_batch(&self, spans: Vec<&str>) -> Result>> { - const BACKOFF_SECONDS: [usize; 3] = [45, 75, 125]; - const MAX_RETRIES: usize = 3; + const BACKOFF_SECONDS: [usize; 4] = [3, 5, 15, 45]; + const MAX_RETRIES: usize = 4; let api_key = OPENAI_API_KEY .as_ref() diff --git a/crates/semantic_index/src/parsing.rs b/crates/semantic_index/src/parsing.rs index cef23862c563f470000306fde5ac32f95a50a458..4aefb0b00d86df9ce6b0a927647687c20e050c83 100644 --- a/crates/semantic_index/src/parsing.rs +++ b/crates/semantic_index/src/parsing.rs @@ -1,5 +1,6 @@ use anyhow::{anyhow, Ok, Result}; use language::{Grammar, Language}; +use sha1::{Digest, Sha1}; use std::{ cmp::{self, Reverse}, collections::HashSet, @@ -15,6 +16,7 @@ pub struct Document { pub range: Range, pub content: String, pub embedding: Vec, + pub sha1: [u8; 20], } const CODE_CONTEXT_TEMPLATE: &str = @@ -63,11 +65,15 @@ impl CodeContextRetriever { .replace("", language_name.as_ref()) .replace("", &content); + let mut sha1 = Sha1::new(); + sha1.update(&document_span); + Ok(vec![Document { range: 0..content.len(), content: document_span, embedding: Vec::new(), name: language_name.to_string(), + sha1: sha1.finalize().into(), }]) } @@ -76,11 +82,15 @@ impl CodeContextRetriever { .replace("", relative_path.to_string_lossy().as_ref()) .replace("", &content); + let mut sha1 = Sha1::new(); + sha1.update(&document_span); + Ok(vec![Document { range: 0..content.len(), content: document_span, embedding: Vec::new(), name: "Markdown".to_string(), + sha1: sha1.finalize().into(), }]) } @@ -253,11 +263,15 @@ impl CodeContextRetriever { ); } + let mut sha1 = Sha1::new(); + sha1.update(&document_content); + documents.push(Document { name, content: document_content, range: item_range.clone(), embedding: vec![], + sha1: sha1.finalize().into(), }) } diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 5aaecac733a3d171d36704bda7f0051b6f4db79b..736f2c98a8b6ac92f8b857b961e5e5a796135f65 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -16,7 +16,7 @@ use language::{Anchor, Buffer, Language, LanguageRegistry}; use parking_lot::Mutex; use parsing::{CodeContextRetriever, Document, PARSEABLE_ENTIRE_FILE_TYPES}; use postage::watch; -use project::{search::PathMatcher, Fs, Project, WorktreeId}; +use project::{search::PathMatcher, Fs, PathChange, Project, ProjectEntryId, WorktreeId}; use smol::channel; use std::{ cmp::Ordering, @@ -33,8 +33,9 @@ use util::{ paths::EMBEDDINGS_DIR, ResultExt, }; +use workspace::WorkspaceCreated; -const SEMANTIC_INDEX_VERSION: usize = 6; +const SEMANTIC_INDEX_VERSION: usize = 7; const EMBEDDINGS_BATCH_SIZE: usize = 80; pub fn init( @@ -54,6 +55,24 @@ pub fn init( return; } + cx.subscribe_global::({ + move |event, cx| { + let Some(semantic_index) = SemanticIndex::global(cx) else { + return; + }; + let workspace = &event.0; + if let Some(workspace) = workspace.upgrade(cx) { + let project = workspace.read(cx).project().clone(); + if project.read(cx).is_local() { + semantic_index.update(cx, |index, cx| { + index.initialize_project(project, cx).detach_and_log_err(cx) + }); + } + } + } + }) + .detach(); + cx.spawn(move |mut cx| async move { let semantic_index = SemanticIndex::new( fs, @@ -92,8 +111,11 @@ pub struct SemanticIndex { struct ProjectState { worktree_db_ids: Vec<(WorktreeId, i64)>, + _subscription: gpui::Subscription, outstanding_job_count_rx: watch::Receiver, _outstanding_job_count_tx: Arc>>, + job_queue_tx: channel::Sender, + _queue_update_task: Task<()>, } #[derive(Clone)] @@ -112,6 +134,72 @@ impl JobHandle { } } impl ProjectState { + fn new( + cx: &mut AppContext, + subscription: gpui::Subscription, + worktree_db_ids: Vec<(WorktreeId, i64)>, + outstanding_job_count_rx: watch::Receiver, + _outstanding_job_count_tx: Arc>>, + ) -> Self { + let (job_queue_tx, job_queue_rx) = channel::unbounded(); + let _queue_update_task = cx.background().spawn({ + let mut worktree_queue = HashMap::new(); + async move { + while let Ok(operation) = job_queue_rx.recv().await { + Self::update_queue(&mut worktree_queue, operation); + } + } + }); + + Self { + worktree_db_ids, + outstanding_job_count_rx, + _outstanding_job_count_tx, + _subscription: subscription, + _queue_update_task, + job_queue_tx, + } + } + + pub fn get_outstanding_count(&self) -> usize { + self.outstanding_job_count_rx.borrow().clone() + } + + fn update_queue(queue: &mut HashMap, operation: IndexOperation) { + match operation { + IndexOperation::FlushQueue => { + let queue = std::mem::take(queue); + for (_, op) in queue { + match op { + IndexOperation::IndexFile { + absolute_path: _, + payload, + tx, + } => { + let _ = tx.try_send(payload); + } + IndexOperation::DeleteFile { + absolute_path: _, + payload, + tx, + } => { + let _ = tx.try_send(payload); + } + _ => {} + } + } + } + IndexOperation::IndexFile { + ref absolute_path, .. + } + | IndexOperation::DeleteFile { + ref absolute_path, .. + } => { + queue.insert(absolute_path.clone(), operation); + } + } + } + fn db_id_for_worktree_id(&self, id: WorktreeId) -> Option { self.worktree_db_ids .iter() @@ -137,6 +225,7 @@ impl ProjectState { } } +#[derive(Clone)] pub struct PendingFile { worktree_db_id: i64, relative_path: PathBuf, @@ -145,6 +234,19 @@ pub struct PendingFile { modified_time: SystemTime, job_handle: JobHandle, } +enum IndexOperation { + IndexFile { + absolute_path: PathBuf, + payload: PendingFile, + tx: channel::Sender, + }, + DeleteFile { + absolute_path: PathBuf, + payload: DbOperation, + tx: channel::Sender, + }, + FlushQueue, +} pub struct SearchResult { pub buffer: ModelHandle, @@ -576,12 +678,112 @@ impl SemanticIndex { }) } - pub fn index_project( + fn project_entries_changed( + &self, + project: ModelHandle, + changes: Arc<[(Arc, ProjectEntryId, PathChange)]>, + cx: &mut ModelContext<'_, SemanticIndex>, + worktree_id: &WorktreeId, + ) -> Result<()> { + let parsing_files_tx = self.parsing_files_tx.clone(); + let db_update_tx = self.db_update_tx.clone(); + let (job_queue_tx, outstanding_job_tx, worktree_db_id) = { + let state = self + .projects + .get(&project.downgrade()) + .ok_or(anyhow!("Project not yet initialized"))?; + let worktree_db_id = state + .db_id_for_worktree_id(*worktree_id) + .ok_or(anyhow!("Worktree ID in Database Not Available"))?; + ( + state.job_queue_tx.clone(), + state._outstanding_job_count_tx.clone(), + worktree_db_id, + ) + }; + + let language_registry = self.language_registry.clone(); + let parsing_files_tx = parsing_files_tx.clone(); + let db_update_tx = db_update_tx.clone(); + + let worktree = project + .read(cx) + .worktree_for_id(worktree_id.clone(), cx) + .ok_or(anyhow!("Worktree not available"))? + .read(cx) + .snapshot(); + cx.spawn(|_, _| async move { + let worktree = worktree.clone(); + for (path, entry_id, path_change) in changes.iter() { + let relative_path = path.to_path_buf(); + let absolute_path = worktree.absolutize(path); + + let Some(entry) = worktree.entry_for_id(*entry_id) else { + continue; + }; + if entry.is_ignored || entry.is_symlink || entry.is_external { + continue; + } + + log::trace!("File Event: {:?}, Path: {:?}", &path_change, &path); + match path_change { + PathChange::AddedOrUpdated | PathChange::Updated | PathChange::Added => { + if let Ok(language) = language_registry + .language_for_file(&relative_path, None) + .await + { + if !PARSEABLE_ENTIRE_FILE_TYPES.contains(&language.name().as_ref()) + && &language.name().as_ref() != &"Markdown" + && language + .grammar() + .and_then(|grammar| grammar.embedding_config.as_ref()) + .is_none() + { + continue; + } + + let job_handle = JobHandle::new(&outstanding_job_tx); + let new_operation = IndexOperation::IndexFile { + absolute_path: absolute_path.clone(), + payload: PendingFile { + worktree_db_id, + relative_path, + absolute_path, + language, + modified_time: entry.mtime, + job_handle, + }, + tx: parsing_files_tx.clone(), + }; + let _ = job_queue_tx.try_send(new_operation); + } + } + PathChange::Removed => { + let new_operation = IndexOperation::DeleteFile { + absolute_path, + payload: DbOperation::Delete { + worktree_id: worktree_db_id, + path: relative_path, + }, + tx: db_update_tx.clone(), + }; + let _ = job_queue_tx.try_send(new_operation); + } + _ => {} + } + } + }) + .detach(); + + Ok(()) + } + + pub fn initialize_project( &mut self, project: ModelHandle, cx: &mut ModelContext, - ) -> Task)>> { - let t0 = Instant::now(); + ) -> Task> { + log::trace!("Initializing Project for Semantic Index"); let worktree_scans_complete = project .read(cx) .worktrees(cx) @@ -592,6 +794,7 @@ impl SemanticIndex { } }) .collect::>(); + let worktree_db_ids = project .read(cx) .worktrees(cx) @@ -600,15 +803,21 @@ impl SemanticIndex { }) .collect::>(); + let _subscription = cx.subscribe(&project, |this, project, event, cx| { + if let project::Event::WorktreeUpdatedEntries(worktree_id, changes) = event { + let _ = + this.project_entries_changed(project.clone(), changes.clone(), cx, worktree_id); + }; + }); + let language_registry = self.language_registry.clone(); - let db_update_tx = self.db_update_tx.clone(); let parsing_files_tx = self.parsing_files_tx.clone(); + let db_update_tx = self.db_update_tx.clone(); cx.spawn(|this, mut cx| async move { futures::future::join_all(worktree_scans_complete).await; let worktree_db_ids = futures::future::join_all(worktree_db_ids).await; - let worktrees = project.read_with(&cx, |project, cx| { project .worktrees(cx) @@ -618,6 +827,7 @@ impl SemanticIndex { let mut worktree_file_mtimes = HashMap::new(); let mut db_ids_by_worktree_id = HashMap::new(); + for (worktree, db_id) in worktrees.iter().zip(worktree_db_ids) { let db_id = db_id?; db_ids_by_worktree_id.insert(worktree.id(), db_id); @@ -628,34 +838,34 @@ impl SemanticIndex { ); } + let worktree_db_ids = db_ids_by_worktree_id + .iter() + .map(|(a, b)| (*a, *b)) + .collect(); + let (job_count_tx, job_count_rx) = watch::channel_with(0); let job_count_tx = Arc::new(Mutex::new(job_count_tx)); - this.update(&mut cx, |this, _| { - this.projects.insert( - project.downgrade(), - ProjectState { - worktree_db_ids: db_ids_by_worktree_id - .iter() - .map(|(a, b)| (*a, *b)) - .collect(), - outstanding_job_count_rx: job_count_rx.clone(), - _outstanding_job_count_tx: job_count_tx.clone(), - }, - ); - }); + let job_count_tx_longlived = job_count_tx.clone(); - cx.background() + let worktree_files = cx + .background() .spawn(async move { - let mut count = 0; + let mut worktree_files = Vec::new(); for worktree in worktrees.into_iter() { let mut file_mtimes = worktree_file_mtimes.remove(&worktree.id()).unwrap(); + let worktree_db_id = db_ids_by_worktree_id[&worktree.id()]; for file in worktree.files(false, 0) { let absolute_path = worktree.absolutize(&file.path); + if file.is_external || file.is_ignored || file.is_symlink { + continue; + } + if let Ok(language) = language_registry .language_for_file(&absolute_path, None) .await { + // Test if file is valid parseable file if !PARSEABLE_ENTIRE_FILE_TYPES.contains(&language.name().as_ref()) && &language.name().as_ref() != &"Markdown" && language @@ -672,39 +882,84 @@ impl SemanticIndex { .map_or(false, |existing_mtime| existing_mtime == file.mtime); if !already_stored { - count += 1; - let job_handle = JobHandle::new(&job_count_tx); - parsing_files_tx - .try_send(PendingFile { - worktree_db_id: db_ids_by_worktree_id[&worktree.id()], + worktree_files.push(IndexOperation::IndexFile { + absolute_path: absolute_path.clone(), + payload: PendingFile { + worktree_db_id, relative_path: path_buf, absolute_path, language, job_handle, modified_time: file.mtime, - }) - .unwrap(); + }, + tx: parsing_files_tx.clone(), + }); } } } - for file in file_mtimes.keys() { - db_update_tx - .try_send(DbOperation::Delete { - worktree_id: db_ids_by_worktree_id[&worktree.id()], - path: file.to_owned(), - }) - .unwrap(); + // Clean up entries from database that are no longer in the worktree. + for (path, _) in file_mtimes { + worktree_files.push(IndexOperation::DeleteFile { + absolute_path: worktree.absolutize(path.as_path()), + payload: DbOperation::Delete { + worktree_id: worktree_db_id, + path, + }, + tx: db_update_tx.clone(), + }); } } - log::trace!( - "walking worktree took {:?} milliseconds", - t0.elapsed().as_millis() - ); - anyhow::Ok((count, job_count_rx)) + anyhow::Ok(worktree_files) }) - .await + .await?; + + this.update(&mut cx, |this, cx| { + let project_state = ProjectState::new( + cx, + _subscription, + worktree_db_ids, + job_count_rx, + job_count_tx_longlived, + ); + + for op in worktree_files { + let _ = project_state.job_queue_tx.try_send(op); + } + + this.projects.insert(project.downgrade(), project_state); + }); + Result::<(), _>::Ok(()) + }) + } + + pub fn index_project( + &mut self, + project: ModelHandle, + cx: &mut ModelContext, + ) -> Task)>> { + let state = self.projects.get_mut(&project.downgrade()); + let state = if state.is_none() { + return Task::Ready(Some(Err(anyhow!("Project not yet initialized")))); + } else { + state.unwrap() + }; + + // let parsing_files_tx = self.parsing_files_tx.clone(); + // let db_update_tx = self.db_update_tx.clone(); + let job_count_rx = state.outstanding_job_count_rx.clone(); + let count = state.get_outstanding_count(); + + cx.spawn(|this, mut cx| async move { + this.update(&mut cx, |this, _| { + let Some(state) = this.projects.get_mut(&project.downgrade()) else { + return; + }; + let _ = state.job_queue_tx.try_send(IndexOperation::FlushQueue); + }); + + Ok((count, job_count_rx)) }) } diff --git a/crates/semantic_index/src/semantic_index_tests.rs b/crates/semantic_index/src/semantic_index_tests.rs index 07ddce4d37c641e45b46599db73c6686e6421949..32d8bb0fb879fe9e1dcf69713d73dbcdc722ffcb 100644 --- a/crates/semantic_index/src/semantic_index_tests.rs +++ b/crates/semantic_index/src/semantic_index_tests.rs @@ -86,6 +86,13 @@ async fn test_semantic_index(cx: &mut TestAppContext) { .unwrap(); let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await; + + let _ = store + .update(cx, |store, cx| { + store.initialize_project(project.clone(), cx) + }) + .await; + let (file_count, outstanding_file_count) = store .update(cx, |store, cx| store.index_project(project.clone(), cx)) .await diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index 06b81a0c61139ce0bd0a0c58a6101b8a043393bb..f89b80902d0f8e12aade715e7903e8191a8445dc 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -16,7 +16,7 @@ collections = { path = "../collections" } gpui = { path = "../gpui" } sqlez = { path = "../sqlez" } fs = { path = "../fs" } -staff_mode = { path = "../staff_mode" } +feature_flags = { path = "../feature_flags" } util = { path = "../util" } anyhow.workspace = true diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index 93cb2ab3d74bd873f55c75d4b4415e7fbf782b51..28cc2db784d5d4a9f4cfc8f2049a18171f1ce551 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -63,20 +63,23 @@ impl KeymapFile { // string. But `RawValue` currently does not work inside of an untagged enum. match action { Value::Array(items) => { - let Ok([name, data]): Result<[serde_json::Value; 2], _> = items.try_into() else { + 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."))) + return Some(Err(anyhow!( + "Expected first item in array to be a string." + ))); }; - cx.deserialize_action( - &name, - Some(data), - ) - }, + 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:?}"))), + _ => { + return Some(Err(anyhow!("Expected two-element array, got {action:?}"))) + } } .with_context(|| { format!( diff --git a/crates/staff_mode/src/staff_mode.rs b/crates/staff_mode/src/staff_mode.rs deleted file mode 100644 index 49fadc0b2cccdd64fdf22e8fed1a887de009749e..0000000000000000000000000000000000000000 --- a/crates/staff_mode/src/staff_mode.rs +++ /dev/null @@ -1,36 +0,0 @@ -use gpui::AppContext; - -#[derive(Debug, Default)] -pub struct StaffMode(pub bool); - -impl std::ops::Deref for StaffMode { - type Target = bool; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -/// Despite what the type system requires me to tell you, the init function will only be called a once -/// as soon as we know that the staff mode is enabled. -pub fn staff_mode(cx: &mut AppContext, mut init: F) { - if **cx.default_global::() { - init(cx) - } else { - let mut once = Some(()); - cx.observe_global::(move |cx| { - if **cx.global::() && once.take().is_some() { - init(cx); - } - }) - .detach(); - } -} - -/// Immediately checks and runs the init function if the staff mode is not enabled. -/// This is only included for symettry with staff_mode() above -pub fn not_staff_mode(cx: &mut AppContext, init: F) { - if !**cx.default_global::() { - init(cx) - } -} diff --git a/crates/sum_tree/src/tree_map.rs b/crates/sum_tree/src/tree_map.rs index 4bb98d2ac8668cb96afbcf31c717f4bb87dbe16f..edb9010e50eb172f379071ad4b2139991c5650d4 100644 --- a/crates/sum_tree/src/tree_map.rs +++ b/crates/sum_tree/src/tree_map.rs @@ -2,7 +2,7 @@ use std::{cmp::Ordering, fmt::Debug}; use crate::{Bias, Dimension, Edit, Item, KeyedItem, SeekTarget, SumTree, Summary}; -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, PartialEq, Eq)] pub struct TreeMap(SumTree>) where K: Clone + Debug + Default + Ord, @@ -162,6 +162,16 @@ impl TreeMap { } } +impl Debug for TreeMap +where + K: Clone + Debug + Default + Ord, + V: Clone + Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_map().entries(self.iter()).finish() + } +} + #[derive(Debug)] struct MapSeekTargetAdaptor<'a, T>(&'a T); diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 3bae06a86dc754126effb1c3c3302a31315d246c..ea919c8f84c9f87e08558309cb2c5adca811c5cf 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -1,5 +1,6 @@ pub mod mappings; pub use alacritty_terminal; +pub mod terminal_settings; use alacritty_terminal::{ ansi::{ClearMode, Handler}, @@ -7,7 +8,7 @@ use alacritty_terminal::{ event::{Event as AlacTermEvent, EventListener, Notify, WindowSize}, event_loop::{EventLoop, Msg, Notifier}, grid::{Dimensions, Scroll as AlacScroll}, - index::{Column, Direction as AlacDirection, Line, Point}, + index::{Boundary, Column, Direction as AlacDirection, Line, Point}, selection::{Selection, SelectionRange, SelectionType}, sync::FairMutex, term::{ @@ -31,8 +32,8 @@ use mappings::mouse::{ }; use procinfo::LocalProcessInfo; -use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use terminal_settings::{AlternateScroll, Shell, TerminalBlink, TerminalSettings}; use util::truncate_and_trailoff; use std::{ @@ -48,7 +49,6 @@ use std::{ use thiserror::Error; use gpui::{ - fonts, geometry::vector::{vec2f, Vector2F}, keymap_matcher::Keystroke, platform::{Modifiers, MouseButton, MouseMovedEvent, TouchPhase}, @@ -78,7 +78,7 @@ lazy_static! { // * use more strict regex for `file://` protocol matching: original regex has `file:` inside, but we want to avoid matching `some::file::module` strings. static ref URL_REGEX: RegexSearch = RegexSearch::new(r#"(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file://|git://|ssh:|ftp://)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>"\s{-}\^⟨⟩`]+"#).unwrap(); - static ref WORD_REGEX: RegexSearch = RegexSearch::new(r#"[\w.:/@\-~]+"#).unwrap(); + static ref WORD_REGEX: RegexSearch = RegexSearch::new(r#"[\w.\[\]:/@\-~]+"#).unwrap(); } ///Upward flowing events, for changing the title and such @@ -134,122 +134,6 @@ pub fn init(cx: &mut AppContext) { settings::register::(cx); } -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum TerminalDockPosition { - Left, - Bottom, - Right, -} - -#[derive(Deserialize)] -pub struct TerminalSettings { - pub shell: Shell, - pub working_directory: WorkingDirectory, - font_size: Option, - pub font_family: Option, - pub line_height: TerminalLineHeight, - pub font_features: Option, - pub env: HashMap, - pub blinking: TerminalBlink, - pub alternate_scroll: AlternateScroll, - pub option_as_meta: bool, - pub copy_on_select: bool, - pub dock: TerminalDockPosition, - pub default_width: f32, - pub default_height: f32, -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] -pub struct TerminalSettingsContent { - pub shell: Option, - pub working_directory: Option, - pub font_size: Option, - pub font_family: Option, - pub line_height: Option, - pub font_features: Option, - pub env: Option>, - pub blinking: Option, - pub alternate_scroll: Option, - pub option_as_meta: Option, - pub copy_on_select: Option, - pub dock: Option, - pub default_width: Option, - pub default_height: Option, -} - -impl TerminalSettings { - pub fn font_size(&self, cx: &AppContext) -> Option { - self.font_size - .map(|size| theme::adjusted_font_size(size, cx)) - } -} - -impl settings::Setting for TerminalSettings { - const KEY: Option<&'static str> = Some("terminal"); - - type FileContent = TerminalSettingsContent; - - fn load( - default_value: &Self::FileContent, - user_values: &[&Self::FileContent], - _: &AppContext, - ) -> Result { - Self::load_via_json_merge(default_value, user_values) - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)] -#[serde(rename_all = "snake_case")] -pub enum TerminalLineHeight { - #[default] - Comfortable, - Standard, - Custom(f32), -} - -impl TerminalLineHeight { - pub fn value(&self) -> f32 { - match self { - TerminalLineHeight::Comfortable => 1.618, - TerminalLineHeight::Standard => 1.3, - TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.), - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum TerminalBlink { - Off, - TerminalControlled, - On, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum Shell { - System, - Program(String), - WithArguments { program: String, args: Vec }, -} - -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum AlternateScroll { - On, - Off, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum WorkingDirectory { - CurrentProjectDirectory, - FirstProjectDirectory, - AlwaysHome, - Always { directory: String }, -} - #[derive(Clone, Copy, Debug, Serialize, Deserialize)] pub struct TerminalSize { pub cell_width: f32, @@ -840,14 +724,13 @@ impl Terminal { self.last_content.size, term.grid().display_offset(), ) - .grid_clamp(term, alacritty_terminal::index::Boundary::Grid); + .grid_clamp(term, Boundary::Grid); let link = term.grid().index(point).hyperlink(); let found_word = if link.is_some() { let mut min_index = point; loop { - let new_min_index = - min_index.sub(term, alacritty_terminal::index::Boundary::Cursor, 1); + let new_min_index = min_index.sub(term, Boundary::Cursor, 1); if new_min_index == min_index { break; } else if term.grid().index(new_min_index).hyperlink() != link { @@ -859,8 +742,7 @@ impl Terminal { let mut max_index = point; loop { - let new_max_index = - max_index.add(term, alacritty_terminal::index::Boundary::Cursor, 1); + let new_max_index = max_index.add(term, Boundary::Cursor, 1); if new_max_index == max_index { break; } else if term.grid().index(new_max_index).hyperlink() != link { @@ -877,11 +759,34 @@ impl Terminal { } else if let Some(word_match) = regex_match_at(term, point, &WORD_REGEX) { let maybe_url_or_path = term.bounds_to_string(*word_match.start(), *word_match.end()); + let original_match = word_match.clone(); + let (sanitized_match, sanitized_word) = + if maybe_url_or_path.starts_with('[') && maybe_url_or_path.ends_with(']') { + ( + Match::new( + word_match.start().add(term, Boundary::Cursor, 1), + word_match.end().sub(term, Boundary::Cursor, 1), + ), + maybe_url_or_path[1..maybe_url_or_path.len() - 1].to_owned(), + ) + } else { + (word_match, maybe_url_or_path) + }; + let is_url = match regex_match_at(term, point, &URL_REGEX) { - Some(url_match) => url_match == word_match, + Some(url_match) => { + // `]` is a valid symbol in the `file://` URL, so the regex match will include it + // consider that when ensuring that the URL match is the same as the original word + if sanitized_match != original_match { + url_match.start() == sanitized_match.start() + && url_match.end() == original_match.end() + } else { + url_match == sanitized_match + } + } None => false, }; - Some((maybe_url_or_path, is_url, word_match)) + Some((sanitized_word, is_url, sanitized_match)) } else { None }; @@ -1018,6 +923,10 @@ impl Terminal { self.pty_tx.notify(input.into_bytes()); } + fn write_bytes_to_pty(&self, input: Vec) { + self.pty_tx.notify(input); + } + pub fn input(&mut self, input: String) { self.events .push_back(InternalEvent::Scroll(AlacScroll::Bottom)); @@ -1026,6 +935,14 @@ impl Terminal { self.write_to_pty(input); } + pub fn input_bytes(&mut self, input: Vec) { + self.events + .push_back(InternalEvent::Scroll(AlacScroll::Bottom)); + self.events.push_back(InternalEvent::SetSelection(None)); + + self.write_bytes_to_pty(input); + } + pub fn try_keystroke(&mut self, keystroke: &Keystroke, alt_is_meta: bool) -> bool { let esc = to_esc_str(keystroke, &self.last_content.mode, alt_is_meta); if let Some(esc) = esc { diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs new file mode 100644 index 0000000000000000000000000000000000000000..e0649ebf65cbecb84da761d3a295de08334c6176 --- /dev/null +++ b/crates/terminal/src/terminal_settings.rs @@ -0,0 +1,163 @@ +use std::{collections::HashMap, path::PathBuf}; + +use gpui::{fonts, AppContext}; +use schemars::JsonSchema; +use serde_derive::{Deserialize, Serialize}; + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum TerminalDockPosition { + Left, + Bottom, + Right, +} + +#[derive(Deserialize)] +pub struct TerminalSettings { + pub shell: Shell, + pub working_directory: WorkingDirectory, + font_size: Option, + pub font_family: Option, + pub line_height: TerminalLineHeight, + pub font_features: Option, + pub env: HashMap, + pub blinking: TerminalBlink, + pub alternate_scroll: AlternateScroll, + pub option_as_meta: bool, + pub copy_on_select: bool, + pub dock: TerminalDockPosition, + pub default_width: f32, + pub default_height: f32, + pub detect_venv: VenvSettings, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum VenvSettings { + #[default] + Off, + On { + activate_script: Option, + directories: Option>, + }, +} + +pub struct VenvSettingsContent<'a> { + pub activate_script: ActivateScript, + pub directories: &'a [PathBuf], +} + +impl VenvSettings { + pub fn as_option(&self) -> Option { + match self { + VenvSettings::Off => None, + VenvSettings::On { + activate_script, + directories, + } => Some(VenvSettingsContent { + activate_script: activate_script.unwrap_or(ActivateScript::Default), + directories: directories.as_deref().unwrap_or(&[]), + }), + } + } +} + +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ActivateScript { + #[default] + Default, + Csh, + Fish, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +pub struct TerminalSettingsContent { + pub shell: Option, + pub working_directory: Option, + pub font_size: Option, + pub font_family: Option, + pub line_height: Option, + pub font_features: Option, + pub env: Option>, + pub blinking: Option, + pub alternate_scroll: Option, + pub option_as_meta: Option, + pub copy_on_select: Option, + pub dock: Option, + pub default_width: Option, + pub default_height: Option, + pub detect_venv: Option, +} + +impl TerminalSettings { + pub fn font_size(&self, cx: &AppContext) -> Option { + self.font_size + .map(|size| theme::adjusted_font_size(size, cx)) + } +} + +impl settings::Setting for TerminalSettings { + const KEY: Option<&'static str> = Some("terminal"); + + type FileContent = TerminalSettingsContent; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &AppContext, + ) -> anyhow::Result { + Self::load_via_json_merge(default_value, user_values) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)] +#[serde(rename_all = "snake_case")] +pub enum TerminalLineHeight { + #[default] + Comfortable, + Standard, + Custom(f32), +} + +impl TerminalLineHeight { + pub fn value(&self) -> f32 { + match self { + TerminalLineHeight::Comfortable => 1.618, + TerminalLineHeight::Standard => 1.3, + TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum TerminalBlink { + Off, + TerminalControlled, + On, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum Shell { + System, + Program(String), + WithArguments { program: String, args: Vec }, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum AlternateScroll { + On, + Off, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum WorkingDirectory { + CurrentProjectDirectory, + FirstProjectDirectory, + AlwaysHome, + Always { directory: String }, +} diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 1d12b83c5c31162d39125322eef282a9d45b7f59..b3d87f531ad5794b86f4b56dbe307e3078a5ffd3 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -25,7 +25,8 @@ use terminal::{ term::{cell::Flags, TermMode}, }, mappings::colors::convert_color, - IndexedCell, Terminal, TerminalContent, TerminalSettings, TerminalSize, + terminal_settings::TerminalSettings, + IndexedCell, Terminal, TerminalContent, TerminalSize, }; use theme::{TerminalStyle, ThemeSettings}; use util::ResultExt; diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 472e748359ea7399a5bcf680c57ffa5a17ad1e8d..9fb3939e1f17a9adfe842130c43684ee4b2cddac 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -9,7 +9,7 @@ use gpui::{ use project::Fs; use serde::{Deserialize, Serialize}; use settings::SettingsStore; -use terminal::{TerminalDockPosition, TerminalSettings}; +use terminal::terminal_settings::{TerminalDockPosition, TerminalSettings}; use util::{ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel}, diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 92465d6b32cff774480ca33d9adc8b5665a49141..104d181a7b9de60460bafa0f12abac51f4ac22e2 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -33,7 +33,8 @@ use terminal::{ index::Point, term::{search::RegexSearch, TermMode}, }, - Event, MaybeNavigationTarget, Terminal, TerminalBlink, WorkingDirectory, + terminal_settings::{TerminalBlink, TerminalSettings, WorkingDirectory}, + Event, MaybeNavigationTarget, Terminal, }; use util::{paths::PathLikeWithPosition, ResultExt}; use workspace::{ @@ -44,8 +45,6 @@ use workspace::{ NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId, }; -pub use terminal::TerminalSettings; - const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); ///Event to transmit the scroll from the element to the view diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 7c94f25e1eb73c125e61effef56565cb9597bd85..4a97faf01515317855bfb8fdea1ac33fb7814b41 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -12,7 +12,7 @@ mod undo_map; pub use anchor::*; use anyhow::{anyhow, Result}; -use clock::ReplicaId; +pub use clock::ReplicaId; use collections::{HashMap, HashSet}; use fs::LineEnding; use locator::Locator; diff --git a/crates/theme/src/components.rs b/crates/theme/src/components.rs index 8a0d8527a4d9384c822c6ba0b1b51e52221a588b..9011821b7727fcbe1cc7f4d7f397d00db42de3b4 100644 --- a/crates/theme/src/components.rs +++ b/crates/theme/src/components.rs @@ -1,23 +1,143 @@ -use gpui::elements::StyleableComponent; +use gpui::{elements::SafeStylable, Action}; use crate::{Interactive, Toggleable}; -use self::{action_button::ButtonStyle, svg::SvgStyle, toggle::Toggle}; +use self::{action_button::ButtonStyle, disclosure::Disclosable, svg::SvgStyle, toggle::Toggle}; -pub type ToggleIconButtonStyle = Toggleable>>; +pub type IconButtonStyle = Interactive>; +pub type ToggleIconButtonStyle = Toggleable; -pub trait ComponentExt { +pub trait ComponentExt { fn toggleable(self, active: bool) -> Toggle; + fn disclosable(self, disclosed: Option, action: Box) -> Disclosable; } -impl ComponentExt for C { +impl ComponentExt for C { fn toggleable(self, active: bool) -> Toggle { Toggle::new(self, active) } + + /// Some(True) => disclosed => content is visible + /// Some(false) => closed => content is hidden + /// None => No disclosure button, but reserve disclosure spacing + fn disclosable(self, disclosed: Option, action: Box) -> Disclosable { + Disclosable::new(disclosed, self, action) + } +} + +pub mod disclosure { + + use gpui::{ + elements::{Component, ContainerStyle, Empty, Flex, ParentElement, SafeStylable}, + Action, Element, + }; + use schemars::JsonSchema; + use serde_derive::Deserialize; + + use super::{action_button::Button, svg::Svg, IconButtonStyle}; + + #[derive(Clone, Default, Deserialize, JsonSchema)] + pub struct DisclosureStyle { + pub button: IconButtonStyle, + #[serde(flatten)] + pub container: ContainerStyle, + pub spacing: f32, + #[serde(flatten)] + content: S, + } + + impl DisclosureStyle { + pub fn button_space(&self) -> f32 { + self.spacing + self.button.button_width.unwrap() + } + } + + pub struct Disclosable { + disclosed: Option, + action: Box, + id: usize, + content: C, + style: S, + } + + impl Disclosable<(), ()> { + pub fn new( + disclosed: Option, + content: C, + action: Box, + ) -> Disclosable { + Disclosable { + disclosed, + content, + action, + id: 0, + style: (), + } + } + } + + impl Disclosable { + pub fn with_id(mut self, id: usize) -> Disclosable { + self.id = id; + self + } + } + + impl SafeStylable for Disclosable { + type Style = DisclosureStyle; + + type Output = Disclosable; + + fn with_style(self, style: Self::Style) -> Self::Output { + Disclosable { + disclosed: self.disclosed, + action: self.action, + content: self.content, + id: self.id, + style, + } + } + } + + impl Component for Disclosable> { + fn render(self, cx: &mut gpui::ViewContext) -> gpui::AnyElement { + Flex::row() + .with_spacing(self.style.spacing) + .with_child(if let Some(disclosed) = self.disclosed { + Button::dynamic_action(self.action) + .with_id(self.id) + .with_contents(Svg::new(if disclosed { + "icons/file_icons/chevron_down.svg" + } else { + "icons/file_icons/chevron_right.svg" + })) + .with_style(self.style.button) + .element() + .into_any() + } else { + Empty::new() + .into_any() + .constrained() + // TODO: Why is this optional at all? + .with_width(self.style.button.button_width.unwrap()) + .into_any() + }) + .with_child( + self.content + .with_style(self.style.content) + .render(cx) + .flex(1., true), + ) + .align_children_center() + .contained() + .with_style(self.style.container) + .into_any() + } + } } pub mod toggle { - use gpui::elements::{GeneralComponent, StyleableComponent}; + use gpui::elements::{Component, SafeStylable}; use crate::Toggleable; @@ -27,7 +147,7 @@ pub mod toggle { component: C, } - impl Toggle { + impl Toggle { pub fn new(component: C, active: bool) -> Self { Toggle { active, @@ -37,7 +157,7 @@ pub mod toggle { } } - impl StyleableComponent for Toggle { + impl SafeStylable for Toggle { type Style = Toggleable; type Output = Toggle; @@ -51,15 +171,11 @@ pub mod toggle { } } - impl GeneralComponent for Toggle> { - fn render( - self, - v: &mut V, - cx: &mut gpui::ViewContext, - ) -> gpui::AnyElement { + impl Component for Toggle> { + fn render(self, cx: &mut gpui::ViewContext) -> gpui::AnyElement { self.component .with_style(self.style.in_state(self.active).clone()) - .render(v, cx) + .render(cx) } } } @@ -68,96 +184,103 @@ pub mod action_button { use std::borrow::Cow; use gpui::{ - elements::{ - ContainerStyle, GeneralComponent, MouseEventHandler, StyleableComponent, TooltipStyle, - }, + elements::{Component, ContainerStyle, MouseEventHandler, SafeStylable, TooltipStyle}, platform::{CursorStyle, MouseButton}, - Action, Element, TypeTag, View, + Action, Element, TypeTag, }; use schemars::JsonSchema; use serde_derive::Deserialize; use crate::Interactive; - pub struct ActionButton { - action: Box, - tooltip: Cow<'static, str>, - tooltip_style: TooltipStyle, - tag: TypeTag, - contents: C, - style: Interactive, - } - #[derive(Clone, Deserialize, Default, JsonSchema)] pub struct ButtonStyle { #[serde(flatten)] - container: ContainerStyle, - button_width: Option, - button_height: Option, + pub container: ContainerStyle, + // TODO: These are incorrect for the intended usage of the buttons. + // The size should be constant, but putting them here duplicates them + // across the states the buttons can be in + pub button_width: Option, + pub button_height: Option, #[serde(flatten)] contents: C, } - impl ActionButton<(), ()> { - pub fn new_dynamic( - action: Box, - tooltip: impl Into>, - tooltip_style: TooltipStyle, - ) -> Self { + pub struct Button { + action: Box, + tooltip: Option<(Cow<'static, str>, TooltipStyle)>, + tag: TypeTag, + id: usize, + contents: C, + style: Interactive, + } + + impl Button<(), ()> { + pub fn dynamic_action(action: Box) -> Button<(), ()> { Self { contents: (), tag: action.type_tag(), - style: Interactive::new_blank(), - tooltip: tooltip.into(), - tooltip_style, action, + style: Interactive::new_blank(), + tooltip: None, + id: 0, } } - pub fn new( - action: A, + pub fn action(action: A) -> Self { + Self::dynamic_action(Box::new(action)) + } + + pub fn with_tooltip( + mut self, tooltip: impl Into>, tooltip_style: TooltipStyle, ) -> Self { - Self::new_dynamic(Box::new(action), tooltip, tooltip_style) + self.tooltip = Some((tooltip.into(), tooltip_style)); + self + } + + pub fn with_id(mut self, id: usize) -> Self { + self.id = id; + self } - pub fn with_contents(self, contents: C) -> ActionButton { - ActionButton { + pub fn with_contents(self, contents: C) -> Button { + Button { action: self.action, tag: self.tag, style: self.style, tooltip: self.tooltip, - tooltip_style: self.tooltip_style, + id: self.id, contents, } } } - impl StyleableComponent for ActionButton { + impl SafeStylable for Button { type Style = Interactive>; - type Output = ActionButton>; + type Output = Button>; fn with_style(self, style: Self::Style) -> Self::Output { - ActionButton { + Button { action: self.action, tag: self.tag, contents: self.contents, tooltip: self.tooltip, - tooltip_style: self.tooltip_style, + id: self.id, style, } } } - impl GeneralComponent for ActionButton> { - fn render(self, v: &mut V, cx: &mut gpui::ViewContext) -> gpui::AnyElement { - MouseEventHandler::new_dynamic(self.tag, 0, cx, |state, cx| { + impl Component for Button> { + fn render(self, cx: &mut gpui::ViewContext) -> gpui::AnyElement { + let mut button = MouseEventHandler::new_dynamic(self.tag, self.id, cx, |state, cx| { let style = self.style.style_for(state); let mut contents = self .contents .with_style(style.contents.to_owned()) - .render(v, cx) + .render(cx) .contained() .with_style(style.container) .constrained(); @@ -185,15 +308,15 @@ pub mod action_button { } }) .with_cursor_style(CursorStyle::PointingHand) - .with_dynamic_tooltip( - self.tag, - 0, - self.tooltip, - Some(self.action), - self.tooltip_style, - cx, - ) - .into_any() + .into_any(); + + if let Some((tooltip, style)) = self.tooltip { + button = button + .with_dynamic_tooltip(self.tag, 0, tooltip, Some(self.action), style, cx) + .into_any() + } + + button } } } @@ -202,7 +325,7 @@ pub mod svg { use std::borrow::Cow; use gpui::{ - elements::{GeneralComponent, StyleableComponent}, + elements::{Component, Empty, SafeStylable}, Element, }; use schemars::JsonSchema; @@ -225,6 +348,7 @@ pub mod svg { pub enum IconSize { IconSize { icon_size: f32 }, Dimensions { width: f32, height: f32 }, + IconDimensions { icon_width: f32, icon_height: f32 }, } #[derive(Deserialize)] @@ -248,6 +372,14 @@ pub mod svg { icon_height: height, color, }, + IconSize::IconDimensions { + icon_width, + icon_height, + } => SvgStyle { + icon_width, + icon_height, + color, + }, }; Ok(result) @@ -255,20 +387,27 @@ pub mod svg { } pub struct Svg { - path: Cow<'static, str>, + path: Option>, style: S, } impl Svg<()> { pub fn new(path: impl Into>) -> Self { Self { - path: path.into(), + path: Some(path.into()), + style: (), + } + } + + pub fn optional(path: Option>>) -> Self { + Self { + path: path.map(Into::into), style: (), } } } - impl StyleableComponent for Svg<()> { + impl SafeStylable for Svg<()> { type Style = SvgStyle; type Output = Svg; @@ -281,18 +420,19 @@ pub mod svg { } } - impl GeneralComponent for Svg { - fn render( - self, - _: &mut V, - _: &mut gpui::ViewContext, - ) -> gpui::AnyElement { - gpui::elements::Svg::new(self.path) - .with_color(self.style.color) - .constrained() - .with_width(self.style.icon_width) - .with_height(self.style.icon_height) - .into_any() + impl Component for Svg { + fn render(self, _: &mut gpui::ViewContext) -> gpui::AnyElement { + if let Some(path) = self.path { + gpui::elements::Svg::new(path) + .with_color(self.style.color) + .constrained() + } else { + Empty::new().constrained() + } + .constrained() + .with_width(self.style.icon_width) + .with_height(self.style.icon_height) + .into_any() } } } @@ -301,7 +441,8 @@ pub mod label { use std::borrow::Cow; use gpui::{ - elements::{GeneralComponent, LabelStyle, StyleableComponent}, + elements::{Component, LabelStyle, SafeStylable}, + fonts::TextStyle, Element, }; @@ -319,25 +460,21 @@ pub mod label { } } - impl StyleableComponent for Label<()> { - type Style = LabelStyle; + impl SafeStylable for Label<()> { + type Style = TextStyle; type Output = Label; fn with_style(self, style: Self::Style) -> Self::Output { Label { text: self.text, - style, + style: style.into(), } } } - impl GeneralComponent for Label { - fn render( - self, - _: &mut V, - _: &mut gpui::ViewContext, - ) -> gpui::AnyElement { + impl Component for Label { + fn render(self, _: &mut gpui::ViewContext) -> gpui::AnyElement { gpui::elements::Label::new(self.text, self.style).into_any() } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 80e823632a9c10613abbf1f3f5effee615c1cd15..a5faba8eaf2016af7875665b679aa96de2518674 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -3,7 +3,7 @@ mod theme_registry; mod theme_settings; pub mod ui; -use components::ToggleIconButtonStyle; +use components::{action_button::ButtonStyle, disclosure::DisclosureStyle, ToggleIconButtonStyle}; use gpui::{ color::Color, elements::{ContainerStyle, ImageStyle, LabelStyle, Shadow, SvgStyle, TooltipStyle}, @@ -14,7 +14,7 @@ use schemars::JsonSchema; use serde::{de::DeserializeOwned, Deserialize}; use serde_json::Value; use settings::SettingsStore; -use std::{collections::HashMap, sync::Arc}; +use std::{collections::HashMap, ops::Deref, sync::Arc}; use ui::{CheckboxStyle, CopilotCTAButton, IconStyle, ModalStyle}; pub use theme_registry::*; @@ -66,6 +66,7 @@ pub struct Theme { pub feedback: FeedbackStyle, pub welcome: WelcomeStyle, pub titlebar: Titlebar, + pub component_test: ComponentTest, } #[derive(Deserialize, Default, Clone, JsonSchema)] @@ -221,6 +222,7 @@ pub struct CopilotAuthAuthorized { pub struct CollabPanel { #[serde(flatten)] pub container: ContainerStyle, + pub disclosure: DisclosureStyle<()>, pub list_empty_state: Toggleable>, pub list_empty_icon: Icon, pub list_empty_label_container: ContainerStyle, @@ -259,6 +261,13 @@ pub struct CollabPanel { pub face_overlap: f32, } +#[derive(Deserialize, Default, JsonSchema)] +pub struct ComponentTest { + pub button: Interactive>, + pub toggle: Toggleable>>, + pub disclosure: DisclosureStyle, +} + #[derive(Deserialize, Default, JsonSchema)] pub struct TabbedModal { pub tab_button: Toggleable>, @@ -428,11 +437,11 @@ pub struct Search { pub match_index: ContainedText, pub major_results_status: TextStyle, pub minor_results_status: TextStyle, - pub dismiss_button: Interactive, pub editor_icon: IconStyle, pub mode_button: Toggleable>, pub nav_button: Toggleable>, pub search_bar_row_height: f32, + pub search_row_spacing: f32, pub option_button_height: f32, pub modes_container: ContainerStyle, } @@ -747,6 +756,7 @@ pub struct Editor { pub line_number: Color, pub line_number_active: Color, pub guest_selections: Vec, + pub absent_selection: SelectionStyle, pub syntax: Arc, pub hint: HighlightStyle, pub suggestion: HighlightStyle, @@ -890,6 +900,14 @@ pub struct Interactive { pub disabled: Option, } +impl Deref for Interactive { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.default + } +} + impl Interactive<()> { pub fn new_blank() -> Self { Self { @@ -907,6 +925,14 @@ pub struct Toggleable { inactive: T, } +impl Deref for Toggleable { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.inactive + } +} + impl Toggleable<()> { pub fn new_blank() -> Self { Self { diff --git a/crates/theme_selector/Cargo.toml b/crates/theme_selector/Cargo.toml index 377f64aad6f1579dfe9ebb50fb0e8b9c683e0f01..7e97d3918606e42cbadd62e354bea5ded0f44e76 100644 --- a/crates/theme_selector/Cargo.toml +++ b/crates/theme_selector/Cargo.toml @@ -16,7 +16,7 @@ gpui = { path = "../gpui" } picker = { path = "../picker" } theme = { path = "../theme" } settings = { path = "../settings" } -staff_mode = { path = "../staff_mode" } +feature_flags = { path = "../feature_flags" } workspace = { path = "../workspace" } util = { path = "../util" } log.workspace = true diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index 551000573300a16334a6a44035c91e8777af14d2..1969b0256a3aa5ee9c203ee02b765695bb748bf9 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -1,9 +1,9 @@ +use feature_flags::FeatureFlagAppExt; use fs::Fs; use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; use gpui::{actions, elements::*, AnyElement, AppContext, Element, MouseState, ViewContext}; use picker::{Picker, PickerDelegate, PickerEvent}; use settings::{update_settings_file, SettingsStore}; -use staff_mode::StaffMode; use std::sync::Arc; use theme::{Theme, ThemeMeta, ThemeRegistry, ThemeSettings}; use util::ResultExt; @@ -54,7 +54,7 @@ impl ThemeSelectorDelegate { fn new(fs: Arc, cx: &mut ViewContext) -> Self { let original_theme = theme::current(cx).clone(); - let staff_mode = **cx.default_global::(); + let staff_mode = cx.is_staff(); let registry = cx.global::>(); let mut theme_names = registry.list(staff_mode).collect::>(); theme_names.sort_unstable_by(|a, b| a.is_light.cmp(&b.is_light).then(a.name.cmp(&b.name))); diff --git a/crates/vcs_menu/src/lib.rs b/crates/vcs_menu/src/lib.rs index 5d2055051792c8967d2ca7d2d4ffa46940fc5c29..73ed4b059ea37ba6770280a2e84318d1c4517aec 100644 --- a/crates/vcs_menu/src/lib.rs +++ b/crates/vcs_menu/src/lib.rs @@ -107,20 +107,15 @@ impl PickerDelegate for BranchListDelegate { let delegate = view.delegate(); let project = delegate.workspace.read(cx).project().read(&cx); - let Some(worktree) = project - .visible_worktrees(cx) - .next() - else { + let Some(worktree) = project.visible_worktrees(cx).next() else { bail!("Cannot update branch list as there are no visible worktrees") }; - let mut cwd = worktree .read(cx) - .abs_path() - .to_path_buf(); + let mut cwd = worktree.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()?; + 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 @@ -142,8 +137,13 @@ impl PickerDelegate for BranchListDelegate { }) .collect::>()) }) - .log_err() else { return; }; - let Some(candidates) = candidates.log_err() else {return;}; + .log_err() + else { + return; + }; + let Some(candidates) = candidates.log_err() else { + return; + }; let matches = if query.is_empty() { candidates .into_iter() @@ -184,7 +184,11 @@ impl PickerDelegate for BranchListDelegate { fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { let current_pick = self.selected_index(); - let Some(current_pick) = self.matches.get(current_pick).map(|pick| pick.string.clone()) else { + let Some(current_pick) = self + .matches + .get(current_pick) + .map(|pick| pick.string.clone()) + else { return; }; cx.spawn(|picker, mut cx| async move { diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 8cd29e5e9f16ffb982e3a76a37038acb3405222b..0d3fb700efff6e651ff23769070a37e9cc4d9b20 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -1,8 +1,8 @@ -use std::sync::Arc; +use std::{cmp, sync::Arc}; use editor::{ char_kind, - display_map::{DisplaySnapshot, ToDisplayPoint}, + display_map::{DisplaySnapshot, FoldPoint, ToDisplayPoint}, movement, Bias, CharKind, DisplayPoint, ToOffset, }; use gpui::{actions, impl_actions, AppContext, WindowContext}; @@ -21,16 +21,16 @@ use crate::{ pub enum Motion { Left, Backspace, - Down, - Up, + Down { display_lines: bool }, + Up { display_lines: bool }, Right, NextWordStart { ignore_punctuation: bool }, NextWordEnd { ignore_punctuation: bool }, PreviousWordStart { ignore_punctuation: bool }, - FirstNonWhitespace, + FirstNonWhitespace { display_lines: bool }, CurrentLine, - StartOfLine, - EndOfLine, + StartOfLine { display_lines: bool }, + EndOfLine { display_lines: bool }, StartOfParagraph, EndOfParagraph, StartOfDocument, @@ -62,6 +62,41 @@ struct PreviousWordStart { ignore_punctuation: bool, } +#[derive(Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +struct Up { + #[serde(default)] + display_lines: bool, +} + +#[derive(Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +struct Down { + #[serde(default)] + display_lines: bool, +} + +#[derive(Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +struct FirstNonWhitespace { + #[serde(default)] + display_lines: bool, +} + +#[derive(Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +struct EndOfLine { + #[serde(default)] + display_lines: bool, +} + +#[derive(Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +struct StartOfLine { + #[serde(default)] + display_lines: bool, +} + #[derive(Clone, Deserialize, PartialEq)] struct RepeatFind { #[serde(default)] @@ -73,12 +108,7 @@ actions!( [ Left, Backspace, - Down, - Up, Right, - FirstNonWhitespace, - StartOfLine, - EndOfLine, CurrentLine, StartOfParagraph, EndOfParagraph, @@ -90,20 +120,63 @@ actions!( ); impl_actions!( vim, - [NextWordStart, NextWordEnd, PreviousWordStart, RepeatFind] + [ + NextWordStart, + NextWordEnd, + PreviousWordStart, + RepeatFind, + Up, + Down, + FirstNonWhitespace, + EndOfLine, + StartOfLine, + ] ); pub fn init(cx: &mut AppContext) { cx.add_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx)); cx.add_action(|_: &mut Workspace, _: &Backspace, cx: _| motion(Motion::Backspace, cx)); - cx.add_action(|_: &mut Workspace, _: &Down, cx: _| motion(Motion::Down, cx)); - cx.add_action(|_: &mut Workspace, _: &Up, cx: _| motion(Motion::Up, cx)); + cx.add_action(|_: &mut Workspace, action: &Down, cx: _| { + motion( + Motion::Down { + display_lines: action.display_lines, + }, + cx, + ) + }); + cx.add_action(|_: &mut Workspace, action: &Up, cx: _| { + motion( + Motion::Up { + display_lines: action.display_lines, + }, + cx, + ) + }); cx.add_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx)); - cx.add_action(|_: &mut Workspace, _: &FirstNonWhitespace, cx: _| { - motion(Motion::FirstNonWhitespace, cx) + cx.add_action(|_: &mut Workspace, action: &FirstNonWhitespace, cx: _| { + motion( + Motion::FirstNonWhitespace { + display_lines: action.display_lines, + }, + cx, + ) + }); + cx.add_action(|_: &mut Workspace, action: &StartOfLine, cx: _| { + motion( + Motion::StartOfLine { + display_lines: action.display_lines, + }, + cx, + ) + }); + cx.add_action(|_: &mut Workspace, action: &EndOfLine, cx: _| { + motion( + Motion::EndOfLine { + display_lines: action.display_lines, + }, + cx, + ) }); - 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) @@ -192,19 +265,25 @@ impl Motion { pub fn linewise(&self) -> bool { use Motion::*; match self { - Down | Up | StartOfDocument | EndOfDocument | CurrentLine | NextLineStart - | StartOfParagraph | EndOfParagraph => true, - EndOfLine + Down { .. } + | Up { .. } + | StartOfDocument + | EndOfDocument + | CurrentLine + | NextLineStart + | StartOfParagraph + | EndOfParagraph => true, + EndOfLine { .. } | NextWordEnd { .. } | Matching | FindForward { .. } | Left | Backspace | Right - | StartOfLine + | StartOfLine { .. } | NextWordStart { .. } | PreviousWordStart { .. } - | FirstNonWhitespace + | FirstNonWhitespace { .. } | FindBackward { .. } => false, } } @@ -213,21 +292,21 @@ impl Motion { use Motion::*; match self { StartOfDocument | EndOfDocument | CurrentLine => true, - Down - | Up - | EndOfLine + Down { .. } + | Up { .. } + | EndOfLine { .. } | NextWordEnd { .. } | Matching | FindForward { .. } | Left | Backspace | Right - | StartOfLine + | StartOfLine { .. } | StartOfParagraph | EndOfParagraph | NextWordStart { .. } | PreviousWordStart { .. } - | FirstNonWhitespace + | FirstNonWhitespace { .. } | FindBackward { .. } | NextLineStart => false, } @@ -236,12 +315,12 @@ impl Motion { pub fn inclusive(&self) -> bool { use Motion::*; match self { - Down - | Up + Down { .. } + | Up { .. } | StartOfDocument | EndOfDocument | CurrentLine - | EndOfLine + | EndOfLine { .. } | NextWordEnd { .. } | Matching | FindForward { .. } @@ -249,12 +328,12 @@ impl Motion { Left | Backspace | Right - | StartOfLine + | StartOfLine { .. } | StartOfParagraph | EndOfParagraph | NextWordStart { .. } | PreviousWordStart { .. } - | FirstNonWhitespace + | FirstNonWhitespace { .. } | FindBackward { .. } => false, } } @@ -272,8 +351,18 @@ impl Motion { let (new_point, goal) = match self { Left => (left(map, point, times), SelectionGoal::None), Backspace => (backspace(map, point, times), SelectionGoal::None), - Down => down(map, point, goal, times), - Up => up(map, point, goal, times), + Down { + display_lines: false, + } => down(map, point, goal, times), + Down { + display_lines: true, + } => down_display(map, point, goal, times), + Up { + display_lines: false, + } => up(map, point, goal, times), + Up { + display_lines: true, + } => up_display(map, point, goal, times), Right => (right(map, point, times), SelectionGoal::None), NextWordStart { ignore_punctuation } => ( next_word_start(map, point, *ignore_punctuation, times), @@ -287,9 +376,17 @@ impl Motion { previous_word_start(map, point, *ignore_punctuation, times), SelectionGoal::None, ), - FirstNonWhitespace => (first_non_whitespace(map, point), SelectionGoal::None), - StartOfLine => (start_of_line(map, point), SelectionGoal::None), - EndOfLine => (end_of_line(map, point), SelectionGoal::None), + FirstNonWhitespace { display_lines } => ( + first_non_whitespace(map, *display_lines, point), + SelectionGoal::None, + ), + StartOfLine { display_lines } => ( + start_of_line(map, *display_lines, point), + SelectionGoal::None, + ), + EndOfLine { display_lines } => { + (end_of_line(map, *display_lines, point), SelectionGoal::None) + } StartOfParagraph => ( movement::start_of_paragraph(map, point, times), SelectionGoal::None, @@ -298,7 +395,7 @@ impl Motion { map.clip_at_line_end(movement::end_of_paragraph(map, point, times)), SelectionGoal::None, ), - CurrentLine => (end_of_line(map, point), SelectionGoal::None), + CurrentLine => (end_of_line(map, false, point), SelectionGoal::None), StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None), EndOfDocument => ( end_of_document(map, point, maybe_times), @@ -399,6 +496,33 @@ fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> Di } fn down( + map: &DisplaySnapshot, + point: DisplayPoint, + mut goal: SelectionGoal, + times: usize, +) -> (DisplayPoint, SelectionGoal) { + let start = map.display_point_to_fold_point(point, Bias::Left); + + let goal_column = match goal { + SelectionGoal::Column(column) => column, + SelectionGoal::ColumnRange { end, .. } => end, + _ => { + goal = SelectionGoal::Column(start.column()); + start.column() + } + }; + + let new_row = cmp::min( + start.row() + times as u32, + map.buffer_snapshot.max_point().row, + ); + let new_col = cmp::min(goal_column, map.fold_snapshot.line_len(new_row)); + let point = map.fold_point_to_display_point(FoldPoint::new(new_row, new_col)); + + (map.clip_point(point, Bias::Left), goal) +} + +fn down_display( map: &DisplaySnapshot, mut point: DisplayPoint, mut goal: SelectionGoal, @@ -407,10 +531,35 @@ fn down( for _ in 0..times { (point, goal) = movement::down(map, point, goal, true); } + (point, goal) } -fn up( +pub(crate) fn up( + map: &DisplaySnapshot, + point: DisplayPoint, + mut goal: SelectionGoal, + times: usize, +) -> (DisplayPoint, SelectionGoal) { + let start = map.display_point_to_fold_point(point, Bias::Left); + + let goal_column = match goal { + SelectionGoal::Column(column) => column, + SelectionGoal::ColumnRange { end, .. } => end, + _ => { + goal = SelectionGoal::Column(start.column()); + start.column() + } + }; + + let new_row = start.row().saturating_sub(times as u32); + let new_col = cmp::min(goal_column, map.fold_snapshot.line_len(new_row)); + let point = map.fold_point_to_display_point(FoldPoint::new(new_row, new_col)); + + (map.clip_point(point, Bias::Left), goal) +} + +fn up_display( map: &DisplaySnapshot, mut point: DisplayPoint, mut goal: SelectionGoal, @@ -419,6 +568,7 @@ fn up( for _ in 0..times { (point, goal) = movement::up(map, point, goal, true); } + (point, goal) } @@ -509,8 +659,12 @@ fn previous_word_start( point } -fn first_non_whitespace(map: &DisplaySnapshot, from: DisplayPoint) -> DisplayPoint { - let mut last_point = DisplayPoint::new(from.row(), 0); +fn first_non_whitespace( + map: &DisplaySnapshot, + display_lines: bool, + from: DisplayPoint, +) -> DisplayPoint { + let mut last_point = start_of_line(map, display_lines, from); let language = map.buffer_snapshot.language_at(from.to_point(map)); for (ch, point) in map.chars_at(last_point) { if ch == '\n' { @@ -527,12 +681,31 @@ fn first_non_whitespace(map: &DisplaySnapshot, from: DisplayPoint) -> DisplayPoi map.clip_point(last_point, Bias::Left) } -fn start_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { - map.prev_line_boundary(point.to_point(map)).1 +pub(crate) fn start_of_line( + map: &DisplaySnapshot, + display_lines: bool, + point: DisplayPoint, +) -> DisplayPoint { + if display_lines { + map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right) + } else { + map.prev_line_boundary(point.to_point(map)).1 + } } -fn end_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { - map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left) +pub(crate) fn end_of_line( + map: &DisplaySnapshot, + display_lines: bool, + point: DisplayPoint, +) -> DisplayPoint { + if display_lines { + map.clip_point( + DisplayPoint::new(point.row(), map.line_len(point.row())), + Bias::Left, + ) + } else { + map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left) + } } fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint { @@ -654,11 +827,8 @@ fn find_backward( } fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint { - let new_row = (point.row() + times as u32).min(map.max_buffer_row()); - first_non_whitespace( - map, - map.clip_point(DisplayPoint::new(new_row, 0), Bias::Left), - ) + let correct_line = down(map, point, SelectionGoal::None, times).0; + first_non_whitespace(map, false, correct_line) } #[cfg(test)] diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 3a2d15a878865418f260c5a14dc9b09e5734944b..a73c51880964c09e41698bbad242c5877aa16796 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -10,7 +10,7 @@ mod yank; use std::sync::Arc; use crate::{ - motion::Motion, + motion::{self, Motion}, object::Object, state::{Mode, Operator}, Vim, @@ -78,13 +78,27 @@ pub fn init(cx: &mut AppContext) { cx.add_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| { Vim::update(cx, |vim, cx| { let times = vim.pop_number_operator(cx); - change_motion(vim, Motion::EndOfLine, times, cx); + change_motion( + vim, + Motion::EndOfLine { + display_lines: false, + }, + times, + cx, + ); }) }); cx.add_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| { Vim::update(cx, |vim, cx| { let times = vim.pop_number_operator(cx); - delete_motion(vim, Motion::EndOfLine, times, cx); + delete_motion( + vim, + Motion::EndOfLine { + display_lines: false, + }, + times, + cx, + ); }) }); scroll::init(cx); @@ -165,7 +179,10 @@ fn insert_first_non_whitespace( vim.update_active_editor(cx, |editor, cx| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.maybe_move_cursors_with(|map, cursor, goal| { - Motion::FirstNonWhitespace.move_point(map, cursor, goal, None) + Motion::FirstNonWhitespace { + display_lines: false, + } + .move_point(map, cursor, goal, None) }); }); }); @@ -178,7 +195,7 @@ fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewConte vim.update_active_editor(cx, |editor, cx| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.maybe_move_cursors_with(|map, cursor, goal| { - Motion::EndOfLine.move_point(map, cursor, goal, None) + Motion::CurrentLine.move_point(map, cursor, goal, None) }); }); }); @@ -197,19 +214,19 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContex .collect(); let edits = selection_start_rows.into_iter().map(|row| { let (indent, _) = map.line_indent(row); - let start_of_line = map - .clip_point(DisplayPoint::new(row, 0), Bias::Left) - .to_point(&map); + let start_of_line = + motion::start_of_line(&map, false, DisplayPoint::new(row, 0)) + .to_point(&map); let mut new_text = " ".repeat(indent as usize); new_text.push('\n'); (start_of_line..start_of_line, new_text) }); editor.edit_with_autoindent(edits, cx); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.move_cursors_with(|map, mut cursor, _| { - *cursor.row_mut() -= 1; - *cursor.column_mut() = map.line_len(cursor.row()); - (map.clip_point(cursor, Bias::Left), SelectionGoal::None) + s.move_cursors_with(|map, cursor, _| { + let previous_line = motion::up(map, cursor, SelectionGoal::None, 1).0; + let insert_point = motion::end_of_line(map, false, previous_line); + (insert_point, SelectionGoal::None) }); }); }); @@ -223,22 +240,23 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex vim.update_active_editor(cx, |editor, cx| { editor.transact(cx, |editor, cx| { let (map, old_selections) = editor.selections.all_display(cx); + let selection_end_rows: HashSet = old_selections .into_iter() .map(|selection| selection.end.row()) .collect(); let edits = selection_end_rows.into_iter().map(|row| { let (indent, _) = map.line_indent(row); - let end_of_line = map - .clip_point(DisplayPoint::new(row, map.line_len(row)), Bias::Left) - .to_point(&map); + let end_of_line = + motion::end_of_line(&map, false, DisplayPoint::new(row, 0)).to_point(&map); + let mut new_text = "\n".to_string(); new_text.push_str(&" ".repeat(indent as usize)); (end_of_line..end_of_line, new_text) }); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.maybe_move_cursors_with(|map, cursor, goal| { - Motion::EndOfLine.move_point(map, cursor, goal, None) + Motion::CurrentLine.move_point(map, cursor, goal, None) }); }); editor.edit_with_autoindent(edits, cx); diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index 50bc049a3aa96d37ae9acce6a1505369333bf534..5591de89c668be823b10f47bf41d2710619ae42c 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -10,7 +10,11 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &m // Some motions ignore failure when switching to normal mode let mut motion_succeeded = matches!( motion, - Motion::Left | Motion::Right | Motion::EndOfLine | Motion::Backspace | Motion::StartOfLine + Motion::Left + | Motion::Right + | Motion::EndOfLine { .. } + | Motion::Backspace + | Motion::StartOfLine { .. } ); vim.update_active_editor(cx, |editor, cx| { editor.transact(cx, |editor, cx| { diff --git a/crates/vim/src/normal/paste.rs b/crates/vim/src/normal/paste.rs index 3d16bb355246688096f425a3ab5076a3864d9e25..3c437f91779ba27f2f2f36c555e6574b2158094b 100644 --- a/crates/vim/src/normal/paste.rs +++ b/crates/vim/src/normal/paste.rs @@ -33,7 +33,7 @@ fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext) { editor.set_clip_at_line_ends(false, cx); let Some(item) = cx.read_from_clipboard() else { - return + return; }; let clipboard_text = Cow::Borrowed(item.text()); if clipboard_text.is_empty() { diff --git a/crates/vim/src/normal/substitute.rs b/crates/vim/src/normal/substitute.rs index 1d53c6831cc0e92be9b021e1faa114928e03276b..b04596240a25d224e9785b4d169bee9743affe19 100644 --- a/crates/vim/src/normal/substitute.rs +++ b/crates/vim/src/normal/substitute.rs @@ -15,7 +15,10 @@ pub fn substitute(vim: &mut Vim, count: Option, cx: &mut WindowContext) { } if line_mode { Motion::CurrentLine.expand_selection(map, selection, None, false); - if let Some((point, _)) = Motion::FirstNonWhitespace.move_point( + if let Some((point, _)) = (Motion::FirstNonWhitespace { + display_lines: false, + }) + .move_point( map, selection.start, selection.goal, diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 3b1b8c24f2d301f5f843bfe56754a3d3d07233f9..88fa37585150d3e0749468094feab4bd98545f15 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -1,7 +1,6 @@ mod neovim_backed_binding_test_context; mod neovim_backed_test_context; mod neovim_connection; -mod vim_binding_test_context; mod vim_test_context; use std::sync::Arc; @@ -10,7 +9,6 @@ use command_palette::CommandPalette; use editor::DisplayPoint; pub use neovim_backed_binding_test_context::*; pub use neovim_backed_test_context::*; -pub use vim_binding_test_context::*; pub use vim_test_context::*; use indoc::indoc; @@ -287,3 +285,218 @@ async fn test_word_characters(cx: &mut gpui::TestAppContext) { Mode::Visual, ) } + +#[gpui::test] +async fn test_wrapped_lines(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_wrap(12).await; + // tests line wrap as follows: + // 1: twelve char + // twelve char + // 2: twelve char + cx.set_shared_state(indoc! { " + tˇwelve char twelve char + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["j"]).await; + cx.assert_shared_state(indoc! { " + twelve char twelve char + tˇwelve char + "}) + .await; + cx.simulate_shared_keystrokes(["k"]).await; + cx.assert_shared_state(indoc! { " + tˇwelve char twelve char + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["g", "j"]).await; + cx.assert_shared_state(indoc! { " + twelve char tˇwelve char + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["g", "j"]).await; + cx.assert_shared_state(indoc! { " + twelve char twelve char + tˇwelve char + "}) + .await; + + cx.simulate_shared_keystrokes(["g", "k"]).await; + cx.assert_shared_state(indoc! { " + twelve char tˇwelve char + twelve char + "}) + .await; + + cx.simulate_shared_keystrokes(["g", "^"]).await; + cx.assert_shared_state(indoc! { " + twelve char ˇtwelve char + twelve char + "}) + .await; + + cx.simulate_shared_keystrokes(["^"]).await; + cx.assert_shared_state(indoc! { " + ˇtwelve char twelve char + twelve char + "}) + .await; + + cx.simulate_shared_keystrokes(["g", "$"]).await; + cx.assert_shared_state(indoc! { " + twelve charˇ twelve char + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["$"]).await; + cx.assert_shared_state(indoc! { " + twelve char twelve chaˇr + twelve char + "}) + .await; + + cx.set_shared_state(indoc! { " + tˇwelve char twelve char + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["enter"]).await; + cx.assert_shared_state(indoc! { " + twelve char twelve char + ˇtwelve char + "}) + .await; + + cx.set_shared_state(indoc! { " + twelve char + tˇwelve char twelve char + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["o", "o", "escape"]).await; + cx.assert_shared_state(indoc! { " + twelve char + twelve char twelve char + ˇo + twelve char + "}) + .await; + + cx.set_shared_state(indoc! { " + twelve char + tˇwelve char twelve char + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["shift-a", "a", "escape"]) + .await; + cx.assert_shared_state(indoc! { " + twelve char + twelve char twelve charˇa + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["shift-i", "i", "escape"]) + .await; + cx.assert_shared_state(indoc! { " + twelve char + ˇitwelve char twelve chara + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["shift-d"]).await; + cx.assert_shared_state(indoc! { " + twelve char + ˇ + twelve char + "}) + .await; + + cx.set_shared_state(indoc! { " + twelve char + twelve char tˇwelve char + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["shift-o", "o", "escape"]) + .await; + cx.assert_shared_state(indoc! { " + twelve char + ˇo + twelve char twelve char + twelve char + "}) + .await; +} + +#[gpui::test] +async fn test_folds(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + cx.set_neovim_option("foldmethod=manual").await; + + cx.set_shared_state(indoc! { " + fn boop() { + ˇbarp() + bazp() + } + "}) + .await; + cx.simulate_shared_keystrokes(["shift-v", "j", "z", "f"]) + .await; + + // visual display is now: + // fn boop () { + // [FOLDED] + // } + + // TODO: this should not be needed but currently zf does not + // return to normal mode. + cx.simulate_shared_keystrokes(["escape"]).await; + + // skip over fold downward + cx.simulate_shared_keystrokes(["g", "g"]).await; + cx.assert_shared_state(indoc! { " + ˇfn boop() { + barp() + bazp() + } + "}) + .await; + + cx.simulate_shared_keystrokes(["j", "j"]).await; + cx.assert_shared_state(indoc! { " + fn boop() { + barp() + bazp() + ˇ} + "}) + .await; + + // skip over fold upward + cx.simulate_shared_keystrokes(["2", "k"]).await; + cx.assert_shared_state(indoc! { " + ˇfn boop() { + barp() + bazp() + } + "}) + .await; + + // yank the fold + cx.simulate_shared_keystrokes(["down", "y", "y"]).await; + cx.assert_shared_clipboard(" barp()\n bazp()\n").await; + + // re-open + cx.simulate_shared_keystrokes(["z", "o"]).await; + cx.assert_shared_state(indoc! { " + fn boop() { + ˇ barp() + bazp() + } + "}) + .await; +} diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index f4b0e961839d087b36acd52bd52ba28e54119af5..d04b1b776836b04addddf5e767023393dd5782f7 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -1,9 +1,13 @@ use indoc::indoc; +use settings::SettingsStore; use std::ops::{Deref, DerefMut, Range}; use collections::{HashMap, HashSet}; use gpui::ContextHandle; -use language::OffsetRangeExt; +use language::{ + language_settings::{AllLanguageSettings, SoftWrap}, + OffsetRangeExt, +}; use util::test::{generate_marked_text, marked_text_offsets}; use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext}; @@ -127,6 +131,27 @@ impl<'a> NeovimBackedTestContext<'a> { context_handle } + pub async fn set_shared_wrap(&mut self, columns: u32) { + if columns < 12 { + panic!("nvim doesn't support columns < 12") + } + self.neovim.set_option("wrap").await; + self.neovim.set_option("columns=12").await; + + self.update(|cx| { + cx.update_global(|settings: &mut SettingsStore, cx| { + settings.update_user_settings::(cx, |settings| { + settings.defaults.soft_wrap = Some(SoftWrap::PreferredLineLength); + settings.defaults.preferred_line_length = Some(columns); + }); + }) + }) + } + + pub async fn set_neovim_option(&mut self, option: &str) { + self.neovim.set_option(option).await; + } + pub async fn assert_shared_state(&mut self, marked_text: &str) { let neovim = self.neovim_state().await; let editor = self.editor_state(); diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index 68f3374772bd34bea247a7e3e814c2b83b394c7f..3e59080b13040c81c362528afda42f5e3fa94ff6 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -41,6 +41,7 @@ pub enum NeovimData { Key(String), Get { state: String, mode: Option }, ReadRegister { name: char, value: String }, + SetOption { value: String }, } pub struct NeovimConnection { @@ -222,6 +223,29 @@ impl NeovimConnection { ); } + #[cfg(feature = "neovim")] + pub async fn set_option(&mut self, value: &str) { + self.nvim + .command_output(format!("set {}", value).as_str()) + .await + .unwrap(); + + self.data.push_back(NeovimData::SetOption { + value: value.to_string(), + }) + } + + #[cfg(not(feature = "neovim"))] + pub async fn set_option(&mut self, value: &str) { + assert_eq!( + self.data.pop_front(), + Some(NeovimData::SetOption { + value: value.to_string(), + }), + "operation does not match recorded script. re-record with --features=neovim" + ); + } + #[cfg(not(feature = "neovim"))] pub async fn read_register(&mut self, register: char) -> String { if let Some(NeovimData::Get { .. }) = self.data.front() { diff --git a/crates/vim/src/test/vim_binding_test_context.rs b/crates/vim/src/test/vim_binding_test_context.rs deleted file mode 100644 index 04afe0c058d9983773add9dd4d5e853db60a7c5c..0000000000000000000000000000000000000000 --- a/crates/vim/src/test/vim_binding_test_context.rs +++ /dev/null @@ -1,64 +0,0 @@ -use std::ops::{Deref, DerefMut}; - -use crate::*; - -use super::VimTestContext; - -pub struct VimBindingTestContext<'a, const COUNT: usize> { - cx: VimTestContext<'a>, - keystrokes_under_test: [&'static str; COUNT], - mode_before: Mode, - mode_after: Mode, -} - -impl<'a, const COUNT: usize> VimBindingTestContext<'a, COUNT> { - pub fn new( - keystrokes_under_test: [&'static str; COUNT], - mode_before: Mode, - mode_after: Mode, - cx: VimTestContext<'a>, - ) -> Self { - Self { - cx, - keystrokes_under_test, - mode_before, - mode_after, - } - } - - pub fn binding( - self, - keystrokes_under_test: [&'static str; NEW_COUNT], - ) -> VimBindingTestContext<'a, NEW_COUNT> { - VimBindingTestContext { - keystrokes_under_test, - cx: self.cx, - mode_before: self.mode_before, - mode_after: self.mode_after, - } - } - - pub fn assert(&mut self, initial_state: &str, state_after: &str) { - self.cx.assert_binding( - self.keystrokes_under_test, - initial_state, - self.mode_before, - state_after, - self.mode_after, - ) - } -} - -impl<'a, const COUNT: usize> Deref for VimBindingTestContext<'a, COUNT> { - type Target = VimTestContext<'a>; - - fn deref(&self) -> &Self::Target { - &self.cx - } -} - -impl<'a, const COUNT: usize> DerefMut for VimBindingTestContext<'a, COUNT> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.cx - } -} diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index 24fb16fd3d736f4afec804ce8752c1c7e42f8232..9b0373957035213b6de019425f4484aaa647920a 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -8,8 +8,6 @@ use search::{BufferSearchBar, ProjectSearchBar}; use crate::{state::Operator, *}; -use super::VimBindingTestContext; - pub struct VimTestContext<'a> { cx: EditorLspTestContext<'a>, } @@ -126,14 +124,6 @@ impl<'a> VimTestContext<'a> { assert_eq!(self.mode(), mode_after, "{}", self.assertion_context()); assert_eq!(self.active_operator(), None, "{}", self.assertion_context()); } - - pub fn binding( - mut self, - keystrokes: [&'static str; COUNT], - ) -> VimBindingTestContext<'a, COUNT> { - let mode = self.mode(); - VimBindingTestContext::new(keystrokes, mode, mode, self) - } } impl<'a> Deref for VimTestContext<'a> { diff --git a/crates/vim/src/utils.rs b/crates/vim/src/utils.rs index c8ca4df72bed76aaeb232d0b9dfc5d3293e5546f..4a96b5bbea165db11bcd42b69baa25e055342e50 100644 --- a/crates/vim/src/utils.rs +++ b/crates/vim/src/utils.rs @@ -1,5 +1,6 @@ use editor::{ClipboardSelection, Editor}; use gpui::{AppContext, ClipboardItem}; +use language::Point; pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut AppContext) { let selections = editor.selections.all_adjusted(cx); @@ -9,7 +10,7 @@ pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut App { let mut is_first = true; for selection in selections.iter() { - let start = selection.start; + let mut start = selection.start; let end = selection.end; if is_first { is_first = false; @@ -17,9 +18,25 @@ pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut App text.push_str("\n"); } let initial_len = text.len(); + + // if the file does not end with \n, and our line-mode selection ends on + // that line, we will have expanded the start of the selection to ensure it + // contains a newline (so that delete works as expected). We undo that change + // here. + let is_last_line = linewise + && end.row == buffer.max_buffer_row() + && buffer.max_point().column > 0 + && start == Point::new(start.row, buffer.line_len(start.row)); + + if is_last_line { + start = Point::new(buffer.max_buffer_row(), 0); + } for chunk in buffer.text_for_range(start..end) { text.push_str(chunk); } + if is_last_line { + text.push_str("\n"); + } clipboard_selections.push(ClipboardSelection { len: text.len() - initial_len, is_entire_line: linewise, diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 1a11721a4e3c7cd9fb2456c00ac151387b55f57b..ee46a0d209f348f3fc25c2252ca9a8a06921fd68 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -51,8 +51,15 @@ pub fn init(cx: &mut AppContext) { pub fn visual_motion(motion: Motion, times: Option, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { vim.update_active_editor(cx, |editor, cx| { - if vim.state().mode == Mode::VisualBlock && !matches!(motion, Motion::EndOfLine) { - let is_up_or_down = matches!(motion, Motion::Up | Motion::Down); + if vim.state().mode == Mode::VisualBlock + && !matches!( + motion, + Motion::EndOfLine { + display_lines: false + } + ) + { + let is_up_or_down = matches!(motion, Motion::Up { .. } | Motion::Down { .. }); visual_block_motion(is_up_or_down, editor, cx, |map, point, goal| { motion.move_point(map, point, goal, times) }) @@ -77,7 +84,10 @@ pub fn visual_motion(motion: Motion, times: Option, cx: &mut WindowContex } let Some((new_head, goal)) = - motion.move_point(map, current_head, selection.goal, times) else { return }; + motion.move_point(map, current_head, selection.goal, times) + else { + return; + }; selection.set_head(new_head, goal); @@ -123,16 +133,21 @@ pub fn visual_block_motion( let map = &s.display_map(); let mut head = s.newest_anchor().head().to_display_point(map); let mut tail = s.oldest_anchor().tail().to_display_point(map); - let mut goal = s.newest_anchor().goal; - let was_reversed = tail.column() > head.column(); + let (start, end) = match s.newest_anchor().goal { + SelectionGoal::ColumnRange { start, end } if preserve_goal => (start, end), + SelectionGoal::Column(start) if preserve_goal => (start, start + 1), + _ => (tail.column(), head.column()), + }; + let goal = SelectionGoal::ColumnRange { start, end }; + let was_reversed = tail.column() > head.column(); if !was_reversed && !preserve_goal { head = movement::saturating_left(map, head); } let Some((new_head, _)) = move_selection(&map, head, goal) else { - return + return; }; head = new_head; @@ -146,13 +161,6 @@ pub fn visual_block_motion( head = movement::saturating_right(map, head) } - let (start, end) = match goal { - SelectionGoal::ColumnRange { start, end } if preserve_goal => (start, end), - SelectionGoal::Column(start) if preserve_goal => (start, start + 1), - _ => (tail.column(), head.column()), - }; - goal = SelectionGoal::ColumnRange { start, end }; - let columns = if is_reversed { head.column()..tail.column() } else if head.column() == tail.column() { @@ -391,7 +399,7 @@ mod test { the lazy dog" }) .await; - let cursor = cx.update_editor(|editor, _| editor.pixel_position_of_cursor()); + let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx)); // entering visual mode should select the character // under cursor @@ -400,7 +408,7 @@ mod test { fox jumps over the lazy dog"}) .await; - cx.update_editor(|editor, _| assert_eq!(cursor, editor.pixel_position_of_cursor())); + cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx))); // forwards motions should extend the selection cx.simulate_shared_keystrokes(["w", "j"]).await; @@ -430,7 +438,7 @@ mod test { b "}) .await; - let cursor = cx.update_editor(|editor, _| editor.pixel_position_of_cursor()); + let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx)); cx.simulate_shared_keystrokes(["v"]).await; cx.assert_shared_state(indoc! {" a @@ -438,7 +446,7 @@ mod test { ˇ»b "}) .await; - cx.update_editor(|editor, _| assert_eq!(cursor, editor.pixel_position_of_cursor())); + cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx))); // toggles off again cx.simulate_shared_keystrokes(["v"]).await; @@ -510,7 +518,7 @@ mod test { b ˇ"}) .await; - let cursor = cx.update_editor(|editor, _| editor.pixel_position_of_cursor()); + let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx)); cx.simulate_shared_keystrokes(["shift-v"]).await; cx.assert_shared_state(indoc! {" a @@ -518,7 +526,7 @@ mod test { ˇ"}) .await; assert_eq!(cx.mode(), cx.neovim_mode().await); - cx.update_editor(|editor, _| assert_eq!(cursor, editor.pixel_position_of_cursor())); + cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx))); cx.simulate_shared_keystrokes(["x"]).await; cx.assert_shared_state(indoc! {" a @@ -563,38 +571,41 @@ mod test { #[gpui::test] async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx) - .await - .binding(["shift-v", "x"]); - cx.assert(indoc! {" + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! {" The quˇick brown fox jumps over the lazy dog"}) .await; - // Test pasting code copied on delete - cx.simulate_shared_keystroke("p").await; + cx.simulate_shared_keystrokes(["shift-v", "x"]).await; cx.assert_state_matches().await; - cx.assert_all(indoc! {" - The quick brown - fox juˇmps over - the laˇzy dog"}) - .await; - let mut cx = cx.binding(["shift-v", "j", "x"]); - cx.assert(indoc! {" - The quˇick brown - fox jumps over - the lazy dog"}) - .await; // Test pasting code copied on delete cx.simulate_shared_keystroke("p").await; cx.assert_state_matches().await; - cx.assert_all(indoc! {" + cx.set_shared_state(indoc! {" The quick brown - fox juˇmps over + fox jumps over the laˇzy dog"}) .await; + cx.simulate_shared_keystrokes(["shift-v", "x"]).await; + cx.assert_state_matches().await; + cx.assert_shared_clipboard("the lazy dog\n").await; + + for marked_text in cx.each_marked_position(indoc! {" + The quˇick brown + fox jumps over + the lazy dog"}) + { + cx.set_shared_state(&marked_text).await; + cx.simulate_shared_keystrokes(["shift-v", "j", "x"]).await; + cx.assert_state_matches().await; + // Test pasting code copied on delete + cx.simulate_shared_keystroke("p").await; + cx.assert_state_matches().await; + } cx.set_shared_state(indoc! {" The ˇlong line @@ -608,86 +619,57 @@ mod test { #[gpui::test] async fn test_visual_yank(cx: &mut gpui::TestAppContext) { - let cx = VimTestContext::new(cx, true).await; - let mut cx = cx.binding(["v", "w", "y"]); - cx.assert("The quick ˇbrown", "The quick ˇbrown"); - cx.assert_clipboard_content(Some("brown")); - let mut cx = cx.binding(["v", "w", "j", "y"]); - cx.assert( - indoc! {" - The ˇquick brown - fox jumps over - the lazy dog"}, - indoc! {" + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("The quick ˇbrown").await; + cx.simulate_shared_keystrokes(["v", "w", "y"]).await; + cx.assert_shared_state("The quick ˇbrown").await; + cx.assert_shared_clipboard("brown").await; + + cx.set_shared_state(indoc! {" The ˇquick brown fox jumps over - the lazy dog"}, - ); - cx.assert_clipboard_content(Some(indoc! {" - quick brown - fox jumps o"})); - cx.assert( - indoc! {" - The quick brown - fox jumps over - the ˇlazy dog"}, - indoc! {" - The quick brown - fox jumps over - the ˇlazy dog"}, - ); - cx.assert_clipboard_content(Some("lazy d")); - cx.assert( - indoc! {" - The quick brown - fox jumps ˇover - the lazy dog"}, - indoc! {" - The quick brown - fox jumps ˇover - the lazy dog"}, - ); - cx.assert_clipboard_content(Some(indoc! {" - over - t"})); + the lazy dog"}) + .await; + cx.simulate_shared_keystrokes(["v", "w", "j", "y"]).await; + cx.assert_shared_state(indoc! {" + The ˇquick brown + fox jumps over + the lazy dog"}) + .await; + cx.assert_shared_clipboard(indoc! {" + quick brown + fox jumps o"}) + .await; + + cx.set_shared_state(indoc! {" + The quick brown + fox jumps over + the ˇlazy dog"}) + .await; + cx.simulate_shared_keystrokes(["v", "w", "j", "y"]).await; + cx.assert_shared_state(indoc! {" + The quick brown + fox jumps over + the ˇlazy dog"}) + .await; + cx.assert_shared_clipboard("lazy d").await; + cx.simulate_shared_keystrokes(["shift-v", "y"]).await; + cx.assert_shared_clipboard("the lazy dog\n").await; + let mut cx = cx.binding(["v", "b", "k", "y"]); - cx.assert( - indoc! {" - The ˇquick brown - fox jumps over - the lazy dog"}, - indoc! {" - ˇThe quick brown - fox jumps over - the lazy dog"}, - ); + cx.set_shared_state(indoc! {" + The ˇquick brown + fox jumps over + the lazy dog"}) + .await; + cx.simulate_shared_keystrokes(["v", "b", "k", "y"]).await; + cx.assert_shared_state(indoc! {" + ˇThe quick brown + fox jumps over + the lazy dog"}) + .await; cx.assert_clipboard_content(Some("The q")); - cx.assert( - indoc! {" - The quick brown - fox jumps over - the ˇlazy dog"}, - indoc! {" - The quick brown - ˇfox jumps over - the lazy dog"}, - ); - cx.assert_clipboard_content(Some(indoc! {" - fox jumps over - the l"})); - cx.assert( - indoc! {" - The quick brown - fox jumps ˇover - the lazy dog"}, - indoc! {" - The ˇquick brown - fox jumps over - the lazy dog"}, - ); - cx.assert_clipboard_content(Some(indoc! {" - quick brown - fox jumps o"})); } #[gpui::test] @@ -814,6 +796,26 @@ mod test { " }) .await; + + //https://github.com/zed-industries/community/issues/1950 + cx.set_shared_state(indoc! { + "Theˇ quick brown + + fox jumps over + the lazy dog + " + }) + .await; + cx.simulate_shared_keystrokes(["l", "ctrl-v", "j", "j"]) + .await; + cx.assert_shared_state(indoc! { + "The «qˇ»uick brown + + fox «jˇ»umps over + the lazy dog + " + }) + .await; } #[gpui::test] diff --git a/crates/vim/test_data/test_folds.json b/crates/vim/test_data/test_folds.json new file mode 100644 index 0000000000000000000000000000000000000000..668df5ce269307a6c8d0b89a1ee67019b6746917 --- /dev/null +++ b/crates/vim/test_data/test_folds.json @@ -0,0 +1,23 @@ +{"SetOption":{"value":"foldmethod=manual"}} +{"Put":{"state":"fn boop() {\n ˇbarp()\n bazp()\n}\n"}} +{"Key":"shift-v"} +{"Key":"j"} +{"Key":"z"} +{"Key":"f"} +{"Key":"escape"} +{"Key":"g"} +{"Key":"g"} +{"Get":{"state":"ˇfn boop() {\n barp()\n bazp()\n}\n","mode":"Normal"}} +{"Key":"j"} +{"Key":"j"} +{"Get":{"state":"fn boop() {\n barp()\n bazp()\nˇ}\n","mode":"Normal"}} +{"Key":"2"} +{"Key":"k"} +{"Get":{"state":"ˇfn boop() {\n barp()\n bazp()\n}\n","mode":"Normal"}} +{"Key":"down"} +{"Key":"y"} +{"Key":"y"} +{"ReadRegister":{"name":"\"","value":" barp()\n bazp()\n"}} +{"Key":"z"} +{"Key":"o"} +{"Get":{"state":"fn boop() {\nˇ barp()\n bazp()\n}\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_visual_block_mode.json b/crates/vim/test_data/test_visual_block_mode.json index ac306de4ab783715fce34305fd3551a6a3d57131..2239ef43a8037d06d91be33afb45488fbda204fd 100644 --- a/crates/vim/test_data/test_visual_block_mode.json +++ b/crates/vim/test_data/test_visual_block_mode.json @@ -30,3 +30,9 @@ {"Key":"o"} {"Key":"escape"} {"Get":{"state":"Theˇouick\nbroo\nfoxo\njumo over the\n\nlazy dog\n","mode":"Normal"}} +{"Put":{"state":"Theˇ quick brown\n\nfox jumps over\nthe lazy dog\n"}} +{"Key":"l"} +{"Key":"ctrl-v"} +{"Key":"j"} +{"Key":"j"} +{"Get":{"state":"The «qˇ»uick brown\n\nfox «jˇ»umps over\nthe lazy dog\n","mode":"VisualBlock"}} diff --git a/crates/vim/test_data/test_visual_line_delete.json b/crates/vim/test_data/test_visual_line_delete.json index 51406266f655984c98abfd70be6edc489c715930..e221a4ad5f0d63ac8f50f48ff62aa4a3807bb400 100644 --- a/crates/vim/test_data/test_visual_line_delete.json +++ b/crates/vim/test_data/test_visual_line_delete.json @@ -4,14 +4,11 @@ {"Get":{"state":"fox juˇmps over\nthe lazy dog","mode":"Normal"}} {"Key":"p"} {"Get":{"state":"fox jumps over\nˇThe quick brown\nthe lazy dog","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Key":"x"} -{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}} {"Put":{"state":"The quick brown\nfox jumps over\nthe laˇzy dog"}} {"Key":"shift-v"} {"Key":"x"} {"Get":{"state":"The quick brown\nfox juˇmps over","mode":"Normal"}} +{"ReadRegister":{"name":"\"","value":"the lazy dog\n"}} {"Put":{"state":"The quˇick brown\nfox jumps over\nthe lazy dog"}} {"Key":"shift-v"} {"Key":"j"} @@ -19,16 +16,6 @@ {"Get":{"state":"the laˇzy dog","mode":"Normal"}} {"Key":"p"} {"Get":{"state":"the lazy dog\nˇThe quick brown\nfox jumps over","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Key":"j"} -{"Key":"x"} -{"Get":{"state":"The quˇick brown","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe laˇzy dog"}} -{"Key":"shift-v"} -{"Key":"j"} -{"Key":"x"} -{"Get":{"state":"The quick brown\nfox juˇmps over","mode":"Normal"}} {"Put":{"state":"The ˇlong line\nshould not\ncrash\n"}} {"Key":"shift-v"} {"Key":"$"} diff --git a/crates/vim/test_data/test_visual_yank.json b/crates/vim/test_data/test_visual_yank.json new file mode 100644 index 0000000000000000000000000000000000000000..edc3b4f83d341052ae847ade2a75b9e2ec18d7a7 --- /dev/null +++ b/crates/vim/test_data/test_visual_yank.json @@ -0,0 +1,29 @@ +{"Put":{"state":"The quick ˇbrown"}} +{"Key":"v"} +{"Key":"w"} +{"Key":"y"} +{"Get":{"state":"The quick ˇbrown","mode":"Normal"}} +{"ReadRegister":{"name":"\"","value":"brown"}} +{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} +{"Key":"v"} +{"Key":"w"} +{"Key":"j"} +{"Key":"y"} +{"Get":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}} +{"ReadRegister":{"name":"\"","value":"quick brown\nfox jumps o"}} +{"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}} +{"Key":"v"} +{"Key":"w"} +{"Key":"j"} +{"Key":"y"} +{"Get":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog","mode":"Normal"}} +{"ReadRegister":{"name":"\"","value":"lazy d"}} +{"Key":"shift-v"} +{"Key":"y"} +{"ReadRegister":{"name":"\"","value":"the lazy dog\n"}} +{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} +{"Key":"v"} +{"Key":"b"} +{"Key":"k"} +{"Key":"y"} +{"Get":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_wrapped_lines.json b/crates/vim/test_data/test_wrapped_lines.json new file mode 100644 index 0000000000000000000000000000000000000000..1ebbd4f20507a86747a50a39d9ee70cd2266f74b --- /dev/null +++ b/crates/vim/test_data/test_wrapped_lines.json @@ -0,0 +1,50 @@ +{"SetOption":{"value":"wrap"}} +{"SetOption":{"value":"columns=12"}} +{"Put":{"state":"tˇwelve char twelve char\ntwelve char\n"}} +{"Key":"j"} +{"Get":{"state":"twelve char twelve char\ntˇwelve char\n","mode":"Normal"}} +{"Key":"k"} +{"Get":{"state":"tˇwelve char twelve char\ntwelve char\n","mode":"Normal"}} +{"Key":"g"} +{"Key":"j"} +{"Get":{"state":"twelve char tˇwelve char\ntwelve char\n","mode":"Normal"}} +{"Key":"g"} +{"Key":"j"} +{"Get":{"state":"twelve char twelve char\ntˇwelve char\n","mode":"Normal"}} +{"Key":"g"} +{"Key":"k"} +{"Get":{"state":"twelve char tˇwelve char\ntwelve char\n","mode":"Normal"}} +{"Key":"g"} +{"Key":"^"} +{"Get":{"state":"twelve char ˇtwelve char\ntwelve char\n","mode":"Normal"}} +{"Key":"^"} +{"Get":{"state":"ˇtwelve char twelve char\ntwelve char\n","mode":"Normal"}} +{"Key":"g"} +{"Key":"$"} +{"Get":{"state":"twelve charˇ twelve char\ntwelve char\n","mode":"Normal"}} +{"Key":"$"} +{"Get":{"state":"twelve char twelve chaˇr\ntwelve char\n","mode":"Normal"}} +{"Put":{"state":"tˇwelve char twelve char\ntwelve char\n"}} +{"Key":"enter"} +{"Get":{"state":"twelve char twelve char\nˇtwelve char\n","mode":"Normal"}} +{"Put":{"state":"twelve char\ntˇwelve char twelve char\ntwelve char\n"}} +{"Key":"o"} +{"Key":"o"} +{"Key":"escape"} +{"Get":{"state":"twelve char\ntwelve char twelve char\nˇo\ntwelve char\n","mode":"Normal"}} +{"Put":{"state":"twelve char\ntˇwelve char twelve char\ntwelve char\n"}} +{"Key":"shift-a"} +{"Key":"a"} +{"Key":"escape"} +{"Get":{"state":"twelve char\ntwelve char twelve charˇa\ntwelve char\n","mode":"Normal"}} +{"Key":"shift-i"} +{"Key":"i"} +{"Key":"escape"} +{"Get":{"state":"twelve char\nˇitwelve char twelve chara\ntwelve char\n","mode":"Normal"}} +{"Key":"shift-d"} +{"Get":{"state":"twelve char\nˇ\ntwelve char\n","mode":"Normal"}} +{"Put":{"state":"twelve char\ntwelve char tˇwelve char\ntwelve char\n"}} +{"Key":"shift-o"} +{"Key":"o"} +{"Key":"escape"} +{"Get":{"state":"twelve char\nˇo\ntwelve char twelve char\ntwelve char\n","mode":"Normal"}} diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 8606be4944830f9859863f1510ffe3413631b31a..e2dae07b8c9dc9c75ac60c32499a7884834b75f4 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -22,6 +22,7 @@ test-support = [ db = { path = "../db" } call = { path = "../call" } client = { path = "../client" } +channel = { path = "../channel" } collections = { path = "../collections" } context_menu = { path = "../context_menu" } drag_and_drop = { path = "../drag_and_drop" } diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index a115e0f4735f5581cce108fa2fa63d498d8b11f5..c218a85234918d424526373b5fc5494316533283 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -158,9 +158,7 @@ pub trait Item: View { fn should_update_tab_on_event(_: &Self::Event) -> bool { false } - fn is_edit_event(_: &Self::Event) -> bool { - false - } + fn act_as_type<'a>( &'a self, type_id: TypeId, @@ -205,7 +203,7 @@ pub trait Item: View { fn show_toolbar(&self) -> bool { true } - fn pixel_position_of_cursor(&self) -> Option { + fn pixel_position_of_cursor(&self, _: &AppContext) -> Option { None } } @@ -623,7 +621,7 @@ impl ItemHandle for ViewHandle { } fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option { - self.read(cx).pixel_position_of_cursor() + self.read(cx).pixel_position_of_cursor(cx) } } @@ -674,7 +672,7 @@ pub trait FollowableItem: Item { fn to_state_proto(&self, cx: &AppContext) -> Option; fn from_state_proto( pane: ViewHandle, - project: ModelHandle, + project: ViewHandle, id: ViewId, state: &mut Option, cx: &mut AppContext, diff --git a/crates/workspace/src/pane/dragged_item_receiver.rs b/crates/workspace/src/pane/dragged_item_receiver.rs index 5e487b49b4ba3423d7dca085497f7f33ae3eee60..bbe391b5b543dbc698385c10d34f4f6f8e946505 100644 --- a/crates/workspace/src/pane/dragged_item_receiver.rs +++ b/crates/workspace/src/pane/dragged_item_receiver.rs @@ -42,7 +42,11 @@ where let mut handler = MouseEventHandler::above::(region_id, cx, |state, cx| { // Observing hovered will cause a render when the mouse enters regardless // of if mouse position was accessed before - let drag_position = if state.hovered() { drag_position } else { None }; + let drag_position = if state.dragging() { + drag_position + } else { + None + }; Stack::new() .with_child(render_child(state, cx)) .with_children(drag_position.map(|drag_position| { diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 7528fb746864e2c66f013cf88014e855fa52a20a..93fb484214fc181d4636845cf1e00a19760b2fb3 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -742,8 +742,8 @@ mod element { while proposed_current_pixel_change.abs() > 0. { let Some(current_ix) = successors.next() else { - break; - }; + break; + }; let next_target_size = f32::max( size(current_ix + 1, flexes.as_slice()) - proposed_current_pixel_change, diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs index 72c879d6d401f171f0f3fc442f74d8053d1b4ce2..c3f4bb9723bdb7208e31f75a704babd400819bb1 100644 --- a/crates/workspace/src/toolbar.rs +++ b/crates/workspace/src/toolbar.rs @@ -81,10 +81,7 @@ impl View for Toolbar { ToolbarItemLocation::PrimaryLeft { flex } => { primary_items_row_count = primary_items_row_count.max(item.row_count(cx)); - let left_item = ChildView::new(item.as_any(), cx) - .aligned() - .contained() - .with_margin_right(spacing); + let left_item = ChildView::new(item.as_any(), cx).aligned(); if let Some((flex, expanded)) = flex { primary_left_items.push(left_item.flex(flex, expanded).into_any()); } else { @@ -94,11 +91,7 @@ impl View for Toolbar { ToolbarItemLocation::PrimaryRight { flex } => { primary_items_row_count = primary_items_row_count.max(item.row_count(cx)); - let right_item = ChildView::new(item.as_any(), cx) - .aligned() - .contained() - .with_margin_left(spacing) - .flex_float(); + let right_item = ChildView::new(item.as_any(), cx).aligned().flex_float(); if let Some((flex, expanded)) = flex { primary_right_items.push(right_item.flex(flex, expanded).into_any()); } else { @@ -120,7 +113,7 @@ impl View for Toolbar { let container_style = theme.container; let height = theme.height * primary_items_row_count as f32; - let mut primary_items = Flex::row(); + let mut primary_items = Flex::row().with_spacing(spacing); primary_items.extend(primary_left_items); primary_items.extend(primary_right_items); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 79b701e015e0ed5a1a10d34962916fce50d3795f..be8148256d0b0f294bbeeafbcfb59e47ec4862d7 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -12,9 +12,10 @@ mod workspace_settings; use anyhow::{anyhow, Context, Result}; use call::ActiveCall; +use channel::ChannelStore; use client::{ proto::{self, PeerId}, - ChannelStore, Client, TypedEnvelope, UserStore, + Client, TypedEnvelope, UserStore, }; use collections::{hash_map, HashMap, HashSet}; use drag_and_drop::DragAndDrop; @@ -344,7 +345,7 @@ pub fn register_project_item(cx: &mut AppContext) { type FollowableItemBuilder = fn( ViewHandle, - ModelHandle, + ViewHandle, ViewId, &mut Option, &mut AppContext, @@ -361,8 +362,8 @@ pub fn register_followable_item(cx: &mut AppContext) { builders.insert( TypeId::of::(), ( - |pane, project, id, state, cx| { - I::from_state_proto(pane, project, id, state, cx).map(|task| { + |pane, workspace, id, state, cx| { + I::from_state_proto(pane, workspace, id, state, cx).map(|task| { cx.foreground() .spawn(async move { Ok(Box::new(task.await?) as Box<_>) }) }) @@ -2313,8 +2314,12 @@ impl Workspace { item_id_to_move: usize, cx: &mut ViewContext, ) { - let Some(pane_to_split) = pane_to_split.upgrade(cx) else { return; }; - let Some(from) = from.upgrade(cx) else { return; }; + let Some(pane_to_split) = pane_to_split.upgrade(cx) else { + return; + }; + let Some(from) = from.upgrade(cx) else { + return; + }; let new_pane = self.add_pane(cx); self.move_item(from.clone(), new_pane.clone(), item_id_to_move, 0, cx); @@ -2847,7 +2852,13 @@ impl Workspace { views: Vec, cx: &mut AsyncAppContext, ) -> Result<()> { - let project = this.read_with(cx, |this, _| this.project.clone())?; + let this = this + .upgrade(cx) + .ok_or_else(|| anyhow!("workspace dropped"))?; + let project = this + .read_with(cx, |this, _| this.project.clone()) + .ok_or_else(|| anyhow!("window dropped"))?; + let replica_id = project .read_with(cx, |project, _| { project @@ -2873,12 +2884,11 @@ impl Workspace { let id = ViewId::from_proto(id.clone())?; let mut variant = view.variant.clone(); if variant.is_none() { - Err(anyhow!("missing variant"))?; + Err(anyhow!("missing view variant"))?; } for build_item in &item_builders { - let task = cx.update(|cx| { - build_item(pane.clone(), project.clone(), id, &mut variant, cx) - }); + let task = cx + .update(|cx| build_item(pane.clone(), this.clone(), id, &mut variant, cx)); if let Some(task) = task { item_tasks.push(task); leader_view_ids.push(id); @@ -2906,7 +2916,7 @@ impl Workspace { } Some(()) - })?; + }); } Ok(()) } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index fa1c67a6d051db61e639fef8beec8b48acf6ae70..2a977646470507565dbea2b5ac847d2546f16845 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.101.0" +version = "0.102.0" publish = false [lib] @@ -21,10 +21,12 @@ activity_indicator = { path = "../activity_indicator" } auto_update = { path = "../auto_update" } breadcrumbs = { path = "../breadcrumbs" } call = { path = "../call" } +channel = { path = "../channel" } cli = { path = "../cli" } collab_ui = { path = "../collab_ui" } collections = { path = "../collections" } command_palette = { path = "../command_palette" } +component_test = { path = "../component_test" } context_menu = { path = "../context_menu" } client = { path = "../client" } clock = { path = "../clock" } @@ -58,7 +60,7 @@ quick_action_bar = { path = "../quick_action_bar" } recent_projects = { path = "../recent_projects" } rpc = { path = "../rpc" } settings = { path = "../settings" } -staff_mode = { path = "../staff_mode" } +feature_flags = { path = "../feature_flags" } sum_tree = { path = "../sum_tree" } text = { path = "../text" } terminal_view = { path = "../terminal_view" } @@ -93,7 +95,7 @@ postage.workspace = true rand.workspace = true regex.workspace = true rsa = "0.4" -rust-embed = { version = "6.8.1" } +rust-embed.workspace = true serde.workspace = true serde_derive.workspace = true serde_json.workspace = true diff --git a/crates/zed/src/languages/c.rs b/crates/zed/src/languages/c.rs index 47aa2b739c3773fe701552a0ac17477c15ee963b..c5041136c9eda608593373a08a57d46d27c0cafd 100644 --- a/crates/zed/src/languages/c.rs +++ b/crates/zed/src/languages/c.rs @@ -289,7 +289,7 @@ mod tests { let language = crate::languages::language("c", tree_sitter_c::language(), None).await; cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "", cx).with_language(language, cx); + let mut buffer = Buffer::new(0, cx.model_id() as u64, "").with_language(language, cx); // empty function buffer.edit([(0..0, "int main() {}")], None, cx); diff --git a/crates/zed/src/languages/json.rs b/crates/zed/src/languages/json.rs index b7e4ab4ba7b32491bbbb8aa025cab543dde113af..61d19ce5b6546ce1de8f040fc97c70b322730f3b 100644 --- a/crates/zed/src/languages/json.rs +++ b/crates/zed/src/languages/json.rs @@ -1,6 +1,7 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use collections::HashMap; +use feature_flags::FeatureFlagAppExt; use futures::{future::BoxFuture, FutureExt, StreamExt}; use gpui::AppContext; use language::{LanguageRegistry, LanguageServerName, LspAdapter, LspAdapterDelegate}; @@ -9,7 +10,6 @@ use node_runtime::NodeRuntime; use serde_json::json; use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore}; use smol::fs; -use staff_mode::StaffMode; use std::{ any::Any, ffi::OsString, @@ -104,7 +104,7 @@ impl LspAdapter for JsonLspAdapter { cx: &mut AppContext, ) -> Option> { let action_names = cx.all_action_names().collect::>(); - let staff_mode = cx.default_global::().0; + let staff_mode = cx.is_staff(); let language_names = &self.languages.language_names(); let settings_schema = cx.global::().json_schema( &SettingsJsonSchemaParams { diff --git a/crates/zed/src/languages/python.rs b/crates/zed/src/languages/python.rs index 41ad28ba862e38e04b52ec5e4e1b77e87b183200..d89a4171e93e3a006180225ab8786b41d921f2e9 100644 --- a/crates/zed/src/languages/python.rs +++ b/crates/zed/src/languages/python.rs @@ -89,7 +89,9 @@ impl LspAdapter for PythonLspAdapter { // to allow our own fuzzy score to be used to break ties. // // see https://github.com/microsoft/pyright/blob/95ef4e103b9b2f129c9320427e51b73ea7cf78bd/packages/pyright-internal/src/languageService/completionProvider.ts#LL2873 - let Some(sort_text) = &mut item.sort_text else { return }; + let Some(sort_text) = &mut item.sort_text else { + return; + }; let mut parts = sort_text.split('.'); let Some(first) = parts.next() else { return }; let Some(second) = parts.next() else { return }; @@ -208,7 +210,7 @@ mod tests { }); cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "", cx).with_language(language, cx); + let mut buffer = Buffer::new(0, cx.model_id() as u64, "").with_language(language, cx); let append = |buffer: &mut Buffer, text: &str, cx: &mut ModelContext| { let ix = buffer.len(); buffer.edit([(ix..ix, text)], Some(AutoindentMode::EachLine), cx); diff --git a/crates/zed/src/languages/rust.rs b/crates/zed/src/languages/rust.rs index 3c7f84fec7dced7f8241ff7009160b0d748191f4..d550d126bb1ee03a61b33740a0dc36a286eb84b9 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/zed/src/languages/rust.rs @@ -474,7 +474,7 @@ mod tests { let language = crate::languages::language("rust", tree_sitter_rust::language(), None).await; cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "", cx).with_language(language, cx); + let mut buffer = Buffer::new(0, cx.model_id() as u64, "").with_language(language, cx); // indent between braces buffer.set_text("fn a() {}", cx); diff --git a/crates/zed/src/languages/typescript.rs b/crates/zed/src/languages/typescript.rs index 0a47d365b598aa41df1c1fa50aedd7d718aceb87..34a512f300584f38eaac4905af4b6a772a012ab7 100644 --- a/crates/zed/src/languages/typescript.rs +++ b/crates/zed/src/languages/typescript.rs @@ -356,8 +356,9 @@ mod tests { "# .unindent(); - let buffer = - cx.add_model(|cx| language::Buffer::new(0, text, cx).with_language(language, cx)); + let buffer = cx.add_model(|cx| { + language::Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx) + }); let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None).unwrap()); assert_eq!( outline diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index deef3b85eba1cadc377be2e9d8dd9249cb5aa636..3e0a8a7a073fcd75dae725f900fe09d328b092b5 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -3,13 +3,12 @@ use anyhow::{anyhow, Context, Result}; use backtrace::Backtrace; +use channel::ChannelStore; use cli::{ ipc::{self, IpcSender}, CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME, }; -use client::{ - self, ChannelStore, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN, -}; +use client::{self, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN}; use db::kvp::KEY_VALUE_STORE; use editor::{scroll::autoscroll::Autoscroll, Editor}; use futures::{ @@ -32,7 +31,7 @@ use std::{ env, ffi::OsStr, fs::OpenOptions, - io::Write as _, + io::{IsTerminal, Write as _}, os::unix::prelude::OsStrExt, panic, path::{Path, PathBuf}, @@ -54,8 +53,6 @@ use uuid::Uuid; use welcome::{show_welcome_experience, FIRST_OPEN}; use fs::RealFs; -#[cfg(debug_assertions)] -use staff_mode::StaffMode; use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt}; use workspace::AppState; use zed::{ @@ -123,7 +120,10 @@ fn main() { cx.set_global(*RELEASE_CHANNEL); #[cfg(debug_assertions)] - cx.set_global(StaffMode(true)); + { + use feature_flags::FeatureFlagAppExt; + cx.set_staff(true); + } let mut store = SettingsStore::default(); store @@ -159,6 +159,7 @@ fn main() { outline::init(cx); project_symbols::init(cx); project_panel::init(Assets, cx); + channel::init(&client); diagnostics::init(cx); search::init(cx); semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx); @@ -166,6 +167,7 @@ fn main() { terminal_view::init(cx); copilot::init(http.clone(), node_runtime, cx); ai::init(cx); + component_test::init(cx); cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach(); cx.spawn(|_| watch_languages(fs.clone(), languages.clone())) @@ -633,8 +635,7 @@ async fn load_login_shell_environment() -> Result<()> { } fn stdout_is_a_pty() -> bool { - std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_none() - && unsafe { libc::isatty(libc::STDOUT_FILENO as i32) != 0 } + std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_none() && std::io::stdout().is_terminal() } fn collect_path_args() -> Vec { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index de05c259c81d7c63ff856ab93d4162a34f992511..9ea406fc3e91c00ab6b7d3d8ebdfed918099a535 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1706,6 +1706,8 @@ mod tests { .remove_file(Path::new("/root/a/file2"), Default::default()) .await .unwrap(); + cx.foreground().run_until_parked(); + workspace .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) .await diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 50003020e9baf17e3e9e0b50babb19c354356e15..7ed8b98280ca394966621a55b80d3101ac6854c8 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.71" +channel = "1.72" components = [ "rustfmt" ] targets = [ "x86_64-apple-darwin", "aarch64-apple-darwin", "wasm32-wasi" ] diff --git a/script/bundle b/script/bundle index 9f50862cd590e12fb30a3cc896aaa403652b45ac..49da1072ceaff28bff27e101caadf85e58d2e2c4 100755 --- a/script/bundle +++ b/script/bundle @@ -5,11 +5,29 @@ set -e build_flag="--release" target_dir="release" open_result=false +local_only=false +overwrite_local_app=false +bundle_name="" + +# Function for displaying help info +help_info() { + echo " +Usage: ${0##*/} [options] [bundle_name] +Build the application bundle. + +Options: + -d Compile in debug mode and print the app bundle's path. + -l Compile for local architecture only and copy bundle to /Applications. + -o Open the resulting DMG or the app itself in local mode. + -f Overwrite the local app bundle if it exists. + -h Display this help and exit. + " +} # If -o option is specified, the folder of the resulting dmg will be opened in finder # If -d is specified, Zed will be compiled in debug mode and the application's path printed # If -od or -do is specified Zed will be bundled in debug and the application will be run. -while getopts 'od' flag +while getopts 'dlfoh' flag do case "${flag}" in o) open_result=true;; @@ -17,9 +35,21 @@ do build_flag=""; target_dir="debug" ;; + l) local_only=true;; + f) overwrite_local_app=true;; + h) + help_info + exit 0 + ;; esac done +shift $((OPTIND-1)) + +if [ "$1" ]; then + bundle_name=$1 +fi + export ZED_BUNDLE=true export MACOSX_DEPLOYMENT_TARGET=10.15.7 @@ -33,14 +63,24 @@ rustup target add wasm32-wasi # Deal with versions of macOS that don't include libstdc++ headers export CXXFLAGS="-stdlib=libc++" -echo "Compiling zed binary for aarch64-apple-darwin" -cargo build ${build_flag} --package zed --target aarch64-apple-darwin -echo "Compiling zed binary for x86_64-apple-darwin" -cargo build ${build_flag} --package zed --target x86_64-apple-darwin -echo "Compiling cli binary for aarch64-apple-darwin" -cargo build ${build_flag} --package cli --target aarch64-apple-darwin -echo "Compiling cli binary for x86_64-apple-darwin" -cargo build ${build_flag} --package cli --target x86_64-apple-darwin +version_info=$(rustc --version --verbose) +host_line=$(echo "$version_info" | grep host) +local_target_triple=${host_line#*: } + +if [ "$local_only" = true ]; then + echo "Building for local target only." + cargo build ${build_flag} --package zed + cargo build ${build_flag} --package cli +else + echo "Compiling zed binary for aarch64-apple-darwin" + cargo build ${build_flag} --package zed --target aarch64-apple-darwin + echo "Compiling zed binary for x86_64-apple-darwin" + cargo build ${build_flag} --package zed --target x86_64-apple-darwin + echo "Compiling cli binary for aarch64-apple-darwin" + cargo build ${build_flag} --package cli --target aarch64-apple-darwin + echo "Compiling cli binary for x86_64-apple-darwin" + cargo build ${build_flag} --package cli --target x86_64-apple-darwin +fi echo "Creating application bundle" pushd crates/zed @@ -50,27 +90,34 @@ sed \ -i .backup \ "s/package.metadata.bundle-${channel}/package.metadata.bundle/" \ Cargo.toml -app_path=$(cargo bundle ${build_flag} --target x86_64-apple-darwin --select-workspace-root | xargs) + +if [ "$local_only" = true ]; then + app_path=$(cargo bundle ${build_flag} --select-workspace-root | xargs) +else + app_path=$(cargo bundle ${build_flag} --target x86_64-apple-darwin --select-workspace-root | xargs) +fi mv Cargo.toml.backup Cargo.toml popd echo "Bundled ${app_path}" -echo "Creating fat binaries" -lipo \ - -create \ - target/{x86_64-apple-darwin,aarch64-apple-darwin}/${target_dir}/Zed \ - -output \ - "${app_path}/Contents/MacOS/zed" -lipo \ - -create \ - target/{x86_64-apple-darwin,aarch64-apple-darwin}/${target_dir}/cli \ - -output \ - "${app_path}/Contents/MacOS/cli" +if [ "$local_only" = false ]; then + echo "Creating fat binaries" + lipo \ + -create \ + target/{x86_64-apple-darwin,aarch64-apple-darwin}/${target_dir}/Zed \ + -output \ + "${app_path}/Contents/MacOS/zed" + lipo \ + -create \ + target/{x86_64-apple-darwin,aarch64-apple-darwin}/${target_dir}/cli \ + -output \ + "${app_path}/Contents/MacOS/cli" +fi echo "Copying WebRTC.framework into the frameworks folder" mkdir "${app_path}/Contents/Frameworks" -cp -R target/x86_64-apple-darwin/${target_dir}/WebRTC.framework "${app_path}/Contents/Frameworks/" +cp -R target/${local_target_triple}/${target_dir}/WebRTC.framework "${app_path}/Contents/Frameworks/" if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then echo "Signing bundle with Apple-issued certificate" @@ -99,31 +146,55 @@ if [ "$target_dir" = "debug" ]; then exit 0 fi -dmg_target_directory="target/${target_dir}" -dmg_source_directory="${dmg_target_directory}/dmg" -dmg_file_path="${dmg_target_directory}/Zed.dmg" - -echo "Creating DMG" -rm -rf ${dmg_source_directory} -mkdir -p ${dmg_source_directory} -mv "${app_path}" "${dmg_source_directory}" - -ln -s /Applications ${dmg_source_directory} -hdiutil create -volname Zed -srcfolder "${dmg_source_directory}" -ov -format UDZO "${dmg_file_path}" -# If someone runs this bundle script locally, a symlink will be placed in `dmg_source_directory`. -# This symlink causes CPU issues with Zed if the Zed codebase is the project being worked on, so we simply remove it for now. -rm ${dmg_source_directory}/Applications +if [ "$local_only" = true ]; then + # If bundle_name is not set or empty, use the basename of $app_path + if [ -z "$bundle_name" ]; then + bundle_name=$(basename "$app_path") + else + # If bundle_name doesn't end in .app, append it + if [[ "$bundle_name" != *.app ]]; then + bundle_name="$bundle_name.app" + fi + fi -echo "Adding license agreement to DMG" -npm install --global dmg-license minimist -dmg-license script/eula/eula.json "${dmg_file_path}" + if [ "$overwrite_local_app" = true ]; then + rm -rf "/Applications/$bundle_name" + fi + mv "$app_path" "/Applications/$bundle_name" -if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then - echo "Notarizing DMG with Apple" - npm install -g notarize-cli - npx notarize-cli --file "${dmg_file_path}" --bundle-id dev.zed.Zed --username "$APPLE_NOTARIZATION_USERNAME" --password "$APPLE_NOTARIZATION_PASSWORD" -fi + if [ "$open_result" = true ]; then + open "/Applications/$bundle_name" + else + echo "Installed application bundle:" + echo "/Applications/$bundle_name" + fi +else + echo "Creating DMG" + dmg_target_directory="target/${target_dir}" + dmg_source_directory="${dmg_target_directory}/dmg" + dmg_file_path="${dmg_target_directory}/Zed.dmg" + + rm -rf ${dmg_source_directory} + mkdir -p ${dmg_source_directory} + mv "${app_path}" "${dmg_source_directory}" + + ln -s /Applications ${dmg_source_directory} + hdiutil create -volname Zed -srcfolder "${dmg_source_directory}" -ov -format UDZO "${dmg_file_path}" + # If someone runs this bundle script locally, a symlink will be placed in `dmg_source_directory`. + # This symlink causes CPU issues with Zed if the Zed codebase is the project being worked on, so we simply remove it for now. + rm ${dmg_source_directory}/Applications + + echo "Adding license agreement to DMG" + npm install --global dmg-license minimist + dmg-license script/eula/eula.json "${dmg_file_path}" + + if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then + echo "Notarizing DMG with Apple" + npm install -g notarize-cli + npx notarize-cli --file "${dmg_file_path}" --bundle-id dev.zed.Zed --username "$APPLE_NOTARIZATION_USERNAME" --password "$APPLE_NOTARIZATION_PASSWORD" + fi -if [ "$open_result" = true ]; then - open $dmg_target_directory + if [ "$open_result" = true ]; then + open $dmg_target_directory + fi fi diff --git a/styles/src/component/icon_button.ts b/styles/src/component/icon_button.ts index 1a2d0bcec491abdad0bc43a7c5d1599052aca622..935909afdbb714dda754f003a01ed264f1f9721c 100644 --- a/styles/src/component/icon_button.ts +++ b/styles/src/component/icon_button.ts @@ -44,10 +44,10 @@ export function icon_button({ color, margin, layer, variant, size }: IconButtonO } const padding = { - top: size === Button.size.Small ? 0 : 2, - bottom: size === Button.size.Small ? 0 : 2, - left: size === Button.size.Small ? 0 : 4, - right: size === Button.size.Small ? 0 : 4, + top: size === Button.size.Small ? 2 : 2, + bottom: size === Button.size.Small ? 2 : 2, + left: size === Button.size.Small ? 2 : 4, + right: size === Button.size.Small ? 2 : 4, } return interactive({ @@ -55,10 +55,10 @@ export function icon_button({ color, margin, layer, variant, size }: IconButtonO corner_radius: 6, padding: padding, margin: m, - icon_width: 14, + icon_width: 12, icon_height: 14, - button_width: 20, - button_height: 16, + button_width: size === Button.size.Small ? 16 : 20, + button_height: 14, }, state: { default: { diff --git a/styles/src/component/label_button.ts b/styles/src/component/label_button.ts deleted file mode 100644 index 3f1c54a7f684198d00f30f294307b3d9d6b9d472..0000000000000000000000000000000000000000 --- a/styles/src/component/label_button.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { Interactive, interactive, toggleable, Toggleable } from "../element" -import { TextStyle, background, text } from "../style_tree/components" -import { useTheme } from "../theme" -import { Button } from "./button" - -type LabelButtonStyle = { - corder_radius: number - background: string | null - padding: { - top: number - bottom: number - left: number - right: number - }, - margin: Button.Options['margin'] - button_height: number -} & TextStyle - -/** Styles an Interactive<ContainedText> */ -export function label_button_style( - options: Partial = { - variant: Button.variant.Default, - shape: Button.shape.Rectangle, - states: { - hovered: true, - pressed: true - } - } -): Interactive { - const theme = useTheme() - - const base = Button.button_base(options) - const layer = options.layer ?? theme.middle - const color = options.color ?? "base" - - const default_state = { - ...base, - ...text(layer ?? theme.lowest, "sans", color), - font_size: Button.FONT_SIZE, - } - - return interactive({ - base: default_state, - state: { - hovered: { - background: background(layer, options.background ?? color, "hovered") - }, - clicked: { - background: background(layer, options.background ?? color, "pressed") - } - } - }) -} - -/** Styles an Toggleable<Interactive<ContainedText>> */ -export function toggle_label_button_style( - options: Partial = { - variant: Button.variant.Default, - shape: Button.shape.Rectangle, - states: { - hovered: true, - pressed: true - } - } -): Toggleable> { - const activeOptions = { - ...options, - color: options.active_color || options.color, - background: options.active_background || options.background - } - - return toggleable({ - state: { - inactive: label_button_style(options), - active: label_button_style(activeOptions), - }, - }) -} diff --git a/styles/src/component/margin.ts b/styles/src/component/margin.ts new file mode 100644 index 0000000000000000000000000000000000000000..f6262405f0b150b06085a6e0b639b405991fe6f0 --- /dev/null +++ b/styles/src/component/margin.ts @@ -0,0 +1,34 @@ +type MarginOptions = { + all?: number + left?: number + right?: number + top?: number + bottom?: number +} + +export type MarginStyle = { + top: number + bottom: number + left: number + right: number +} + +export const margin_style = (options: MarginOptions): MarginStyle => { + const { all, top, bottom, left, right } = options + + if (all !== undefined) return { + top: all, + bottom: all, + left: all, + right: all + } + + if (top === undefined && bottom === undefined && left === undefined && right === undefined) throw new Error("Margin must have at least one value") + + return { + top: top || 0, + bottom: bottom || 0, + left: left || 0, + right: right || 0 + } +} diff --git a/styles/src/component/padding.ts b/styles/src/component/padding.ts new file mode 100644 index 0000000000000000000000000000000000000000..96792bf7661263d39e058310c836c2e34aae5378 --- /dev/null +++ b/styles/src/component/padding.ts @@ -0,0 +1,34 @@ +type PaddingOptions = { + all?: number + left?: number + right?: number + top?: number + bottom?: number +} + +export type PaddingStyle = { + top: number + bottom: number + left: number + right: number +} + +export const padding_style = (options: PaddingOptions): PaddingStyle => { + const { all, top, bottom, left, right } = options + + if (all !== undefined) return { + top: all, + bottom: all, + left: all, + right: all + } + + if (top === undefined && bottom === undefined && left === undefined && right === undefined) throw new Error("Padding must have at least one value") + + return { + top: top || 0, + bottom: bottom || 0, + left: left || 0, + right: right || 0 + } +} diff --git a/styles/src/component/text_button.ts b/styles/src/component/text_button.ts index b911cd5b778c54b2f4b58cd8aa7911b0fd9d553d..ead017a80324d1600c2bb32600216ed7879d5285 100644 --- a/styles/src/component/text_button.ts +++ b/styles/src/component/text_button.ts @@ -17,6 +17,7 @@ interface TextButtonOptions { variant?: Button.Variant color?: keyof Theme["lowest"] margin?: Partial + disabled?: boolean text_properties?: TextProperties } @@ -29,6 +30,7 @@ export function text_button({ color, layer, margin, + disabled, text_properties, }: TextButtonOptions = {}) { const theme = useTheme() @@ -65,13 +67,17 @@ export function text_button({ state: { default: { background: background_color, - color: foreground(layer ?? theme.lowest, color), + color: + disabled + ? foreground(layer ?? theme.lowest, "disabled") + : foreground(layer ?? theme.lowest, color), }, - hovered: { - background: background(layer ?? theme.lowest, color, "hovered"), - color: foreground(layer ?? theme.lowest, color, "hovered"), - }, - clicked: { + hovered: + disabled ? {} : { + background: background(layer ?? theme.lowest, color, "hovered"), + color: foreground(layer ?? theme.lowest, color, "hovered"), + }, + clicked: disabled ? {} : { background: background(layer ?? theme.lowest, color, "pressed"), color: foreground(layer ?? theme.lowest, color, "pressed"), }, diff --git a/styles/src/style_tree/app.ts b/styles/src/style_tree/app.ts index ee5e19e11137a48097c6873dfca03d00c68d2279..cf7e0538a72874674700410b9d892fd057bfa0ca 100644 --- a/styles/src/style_tree/app.ts +++ b/styles/src/style_tree/app.ts @@ -12,7 +12,6 @@ import simple_message_notification from "./simple_message_notification" import project_shared_notification from "./project_shared_notification" import tooltip from "./tooltip" import terminal from "./terminal" -import contact_finder from "./contact_finder" import collab_panel from "./collab_panel" import toolbar_dropdown_menu from "./toolbar_dropdown_menu" import incoming_call_notification from "./incoming_call_notification" @@ -22,6 +21,7 @@ import assistant from "./assistant" import { titlebar } from "./titlebar" import editor from "./editor" import feedback from "./feedback" +import component_test from "./component_test" import { useTheme } from "../common" export default function app(): any { @@ -54,6 +54,7 @@ export default function app(): any { tooltip: tooltip(), terminal: terminal(), assistant: assistant(), - feedback: feedback() + feedback: feedback(), + component_test: component_test(), } } diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index a102ee769194d4b4e0189f5b2541de53c3408180..07f367c8afe077fd3d0bbca3ab424ecd07006078 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -14,6 +14,7 @@ import { indicator } from "../component/indicator" export default function contacts_panel(): any { const theme = useTheme() + const CHANNEL_SPACING = 4 as const const NAME_MARGIN = 6 as const const SPACING = 12 as const const INDENT_SIZE = 8 as const @@ -152,6 +153,10 @@ export default function contacts_panel(): any { return { ...collab_modals(), + disclosure: { + button: icon_button({ variant: "ghost", size: "sm" }), + spacing: CHANNEL_SPACING, + }, log_in_button: interactive({ base: { background: background(theme.middle), @@ -194,7 +199,7 @@ export default function contacts_panel(): any { add_channel_button: header_icon_button, leave_call_button: header_icon_button, row_height: ITEM_HEIGHT, - channel_indent: INDENT_SIZE * 2, + channel_indent: INDENT_SIZE * 2 + 2, section_icon_size: 14, header_row: { ...text(layer, "sans", { size: "sm", weight: "bold" }), @@ -264,7 +269,7 @@ export default function contacts_panel(): any { channel_name: { ...text(layer, "sans", { size: "sm" }), margin: { - left: NAME_MARGIN, + left: CHANNEL_SPACING, }, }, list_empty_label_container: { diff --git a/styles/src/style_tree/component_test.ts b/styles/src/style_tree/component_test.ts new file mode 100644 index 0000000000000000000000000000000000000000..e2bb0915c172d946bfd882aa768f3db54e82cb63 --- /dev/null +++ b/styles/src/style_tree/component_test.ts @@ -0,0 +1,27 @@ + +import { useTheme } from "../common" +import { text_button } from "../component/text_button" +import { icon_button } from "../component/icon_button" +import { text } from "./components" +import { toggleable } from "../element" + +export default function contacts_panel(): any { + const theme = useTheme() + + return { + button: text_button({}), + toggle: toggleable({ + base: text_button({}), + state: { + active: { + ...text_button({ color: "accent" }) + } + } + }), + disclosure: { + ...text(theme.lowest, "sans", "base"), + button: icon_button({ variant: "ghost" }), + spacing: 4, + } + } +} diff --git a/styles/src/style_tree/editor.ts b/styles/src/style_tree/editor.ts index 9ad008f38d4dd928af5b49c46b148df575b1c6a3..9277a2e7a1966c7d4eeac6cc7d3129bd52244141 100644 --- a/styles/src/style_tree/editor.ts +++ b/styles/src/style_tree/editor.ts @@ -184,6 +184,7 @@ export default function editor(): any { theme.players[6], theme.players[7], ], + absent_selection: theme.players[7], autocomplete: { background: background(theme.middle), corner_radius: 8, diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 4493634a8e8a73da36a6f00ac40c56110ad50b88..c37a4e4b9a1bedec7c159c6f729dab0e9a882362 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -2,9 +2,23 @@ import { with_opacity } from "../theme/color" import { background, border, foreground, text } from "./components" import { interactive, toggleable } from "../element" import { useTheme } from "../theme" +import { text_button } from "../component/text_button" + +const search_results = () => { + const theme = useTheme() + + return { + // TODO: Add an activeMatchBackground on the rust side to differentiate between active and inactive + match_background: with_opacity( + foreground(theme.highest, "accent"), + 0.4 + ), + } +} export default function search(): any { const theme = useTheme() + const SEARCH_ROW_SPACING = 12 // Search input const editor = { @@ -34,12 +48,8 @@ export default function search(): any { } return { - padding: { top: 16, bottom: 16, left: 16, right: 16 }, - // TODO: Add an activeMatchBackground on the rust side to differentiate between active and inactive - match_background: with_opacity( - foreground(theme.highest, "accent"), - 0.4 - ), + padding: { top: 4, bottom: 4 }, + option_button: toggleable({ base: interactive({ base: { @@ -153,47 +163,13 @@ export default function search(): any { }, }, }), + // Search tool buttons + // HACK: This is not how disabled elements should be created + // Disabled elements should use a disabled state of an interactive element, not a toggleable element with the inactive state being disabled action_button: toggleable({ - base: interactive({ - base: { - ...text(theme.highest, "mono", "disabled"), - background: background(theme.highest, "disabled"), - corner_radius: 6, - border: border(theme.highest, "disabled"), - padding: { - // bottom: 2, - left: 10, - right: 10, - // top: 2, - }, - margin: { - right: 9, - } - }, - state: { - hovered: {} - }, - }), state: { - active: interactive({ - base: { - ...text(theme.highest, "mono", "on"), - background: background(theme.highest, "on"), - border: border(theme.highest, "on"), - }, - state: { - hovered: { - ...text(theme.highest, "mono", "on", "hovered"), - background: background(theme.highest, "on", "hovered"), - border: border(theme.highest, "on", "hovered"), - }, - clicked: { - ...text(theme.highest, "mono", "on", "pressed"), - background: background(theme.highest, "on", "pressed"), - border: border(theme.highest, "on", "pressed"), - }, - }, - }) + inactive: text_button({ variant: "ghost", layer: theme.highest, disabled: true, margin: { right: SEARCH_ROW_SPACING }, text_properties: { size: "sm" } }), + active: text_button({ variant: "ghost", layer: theme.highest, margin: { right: SEARCH_ROW_SPACING }, text_properties: { size: "sm" } }) } }), editor, @@ -207,15 +183,15 @@ export default function search(): any { border: border(theme.highest, "negative"), }, match_index: { - ...text(theme.highest, "mono", "variant"), + ...text(theme.highest, "mono", { size: "sm" }), padding: { - left: 9, + right: SEARCH_ROW_SPACING, }, }, option_button_group: { padding: { - left: 12, - right: 12, + left: SEARCH_ROW_SPACING, + right: SEARCH_ROW_SPACING, }, }, include_exclude_inputs: { @@ -232,52 +208,26 @@ export default function search(): any { ...text(theme.highest, "mono", "variant"), size: 13, }, - dismiss_button: interactive({ - base: { - color: foreground(theme.highest, "variant"), - icon_width: 14, - button_width: 32, - corner_radius: 6, - padding: { - // // top: 10, - // bottom: 10, - left: 10, - right: 10, - }, - - background: background(theme.highest, "variant"), - - border: border(theme.highest, "on"), - }, - state: { - hovered: { - color: foreground(theme.highest, "hovered"), - background: background(theme.highest, "variant", "hovered") - }, - clicked: { - color: foreground(theme.highest, "pressed"), - background: background(theme.highest, "variant", "pressed") - }, - }, - }), + // Input Icon editor_icon: { icon: { - color: foreground(theme.highest, "variant"), - asset: "icons/magnifying_glass_12.svg", + color: foreground(theme.highest, "disabled"), + asset: "icons/magnifying_glass.svg", dimensions: { - width: 12, - height: 12, + width: 14, + height: 14, } }, container: { - margin: { right: 6 }, - padding: { left: 2, right: 2 }, + margin: { right: 4 }, + padding: { left: 1, right: 1 }, } }, + // Toggle group buttons - Text | Regex | Semantic mode_button: toggleable({ base: interactive({ base: { - ...text(theme.highest, "mono", "variant"), + ...text(theme.highest, "mono", "variant", { size: "sm" }), background: background(theme.highest, "variant"), border: { @@ -285,21 +235,24 @@ export default function search(): any { left: false, right: false }, - + margin: { + top: 1, + bottom: 1, + }, padding: { - left: 10, - right: 10, + left: 12, + right: 12, }, corner_radius: 6, }, state: { hovered: { - ...text(theme.highest, "mono", "variant", "hovered"), + ...text(theme.highest, "mono", "variant", "hovered", { size: "sm" }), background: background(theme.highest, "variant", "hovered"), border: border(theme.highest, "on", "hovered"), }, clicked: { - ...text(theme.highest, "mono", "variant", "pressed"), + ...text(theme.highest, "mono", "variant", "pressed", { size: "sm" }), background: background(theme.highest, "variant", "pressed"), border: border(theme.highest, "on", "pressed"), }, @@ -308,20 +261,23 @@ export default function search(): any { state: { active: { default: { - ...text(theme.highest, "mono", "on"), + ...text(theme.highest, "mono", "on", { size: "sm" }), background: background(theme.highest, "on") }, hovered: { - ...text(theme.highest, "mono", "on", "hovered"), + ...text(theme.highest, "mono", "on", "hovered", { size: "sm" }), background: background(theme.highest, "on", "hovered") }, clicked: { - ...text(theme.highest, "mono", "on", "pressed"), + ...text(theme.highest, "mono", "on", "pressed", { size: "sm" }), background: background(theme.highest, "on", "pressed") }, }, }, }), + // Next/Previous Match buttons + // HACK: This is not how disabled elements should be created + // Disabled elements should use a disabled state of an interactive element, not a toggleable element with the inactive state being disabled nav_button: toggleable({ state: { inactive: interactive({ @@ -334,7 +290,10 @@ export default function search(): any { left: false, right: false, }, - + margin: { + top: 1, + bottom: 1, + }, padding: { left: 10, right: 10, @@ -354,7 +313,10 @@ export default function search(): any { left: false, right: false, }, - + margin: { + top: 1, + bottom: 1, + }, padding: { left: 10, right: 10, @@ -375,13 +337,10 @@ export default function search(): any { }) } }), - search_bar_row_height: 32, + search_bar_row_height: 34, + search_row_spacing: 8, option_button_height: 22, - modes_container: { - margin: { - right: 9 - } - } - + modes_container: {}, + ...search_results() } } diff --git a/styles/src/style_tree/workspace.ts b/styles/src/style_tree/workspace.ts index ecfb572f7e892944485fe223eceb210e76c653fc..43a6cec58537ebf3cefd6eb3ee3e5c44d0c2066d 100644 --- a/styles/src/style_tree/workspace.ts +++ b/styles/src/style_tree/workspace.ts @@ -129,7 +129,7 @@ export default function workspace(): any { status_bar: statusBar(), titlebar: titlebar(), toolbar: { - height: 34, + height: 42, background: background(theme.highest), border: border(theme.highest, { bottom: true }), item_spacing: 8, @@ -138,7 +138,7 @@ export default function workspace(): any { variant: "ghost", active_color: "accent", }), - padding: { left: 8, right: 8, top: 4, bottom: 4 }, + padding: { left: 8, right: 8 }, }, breadcrumb_height: 24, breadcrumbs: interactive({ diff --git a/styles/tsconfig.json b/styles/tsconfig.json index 281bd74b215bd16426bb6a8f9d68ddeb5a5bea43..c7eaa50eedd05d201a8eb4d99a562c418f9cdb08 100644 --- a/styles/tsconfig.json +++ b/styles/tsconfig.json @@ -21,8 +21,7 @@ "experimentalDecorators": true, "strictPropertyInitialization": false, "skipLibCheck": true, - "useUnknownInCatchVariables": false, - "baseUrl": "." + "useUnknownInCatchVariables": false }, "exclude": [ "node_modules"