diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000..8d16a59bc1 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,5 @@ +[[PR Description]] + +Release Notes: + +* [[Added foo / Fixed bar / No notes]] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b7cb97efa..2cd717d5c1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,6 +42,7 @@ jobs: runs-on: - self-hosted - test + needs: rustfmt env: RUSTFLAGS: -D warnings steps: @@ -62,6 +63,9 @@ jobs: clean: false submodules: 'recursive' + - name: Limit target directory size + run: script/clear-target-dir-if-larger-than 70 + - name: Run check run: cargo check --workspace @@ -82,7 +86,7 @@ jobs: runs-on: - self-hosted - bundle - if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') }} + if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }} needs: tests env: MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} @@ -110,6 +114,9 @@ jobs: clean: false submodules: 'recursive' + - name: Limit target directory size + run: script/clear-target-dir-if-larger-than 70 + - name: Determine version and release channel if: ${{ startsWith(github.ref, 'refs/tags/v') }} run: | @@ -141,11 +148,11 @@ jobs: - name: Create app bundle run: script/bundle - - name: Upload app bundle to workflow run if main branch + - name: Upload app bundle to workflow run if main branch or specifi label uses: actions/upload-artifact@v2 - if: ${{ github.ref == 'refs/heads/main' }} + if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }} with: - name: Zed.dmg + name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg path: target/release/Zed.dmg - uses: softprops/action-gh-release@v1 diff --git a/.github/workflows/release_actions.yml b/.github/workflows/release_actions.yml index 4a9d777769..5feb29e469 100644 --- a/.github/workflows/release_actions.yml +++ b/.github/workflows/release_actions.yml @@ -14,7 +14,7 @@ jobs: content: | 📣 Zed ${{ github.event.release.tag_name }} was just released! - Restart your Zed or head to https://zed.dev/releases/latest to grab it. + Restart your Zed or head to https://zed.dev/releases/stable/latest to grab it. ```md # Changelog diff --git a/Cargo.lock b/Cargo.lock index eee0873e5b..2aea69e90b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,12 +14,13 @@ version = "0.1.0" dependencies = [ "auto_update", "editor", - "futures 0.3.25", + "futures 0.3.28", "gpui", "language", "project", "settings", "smallvec", + "theme", "util", "workspace", ] @@ -30,7 +31,16 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" dependencies = [ - "gimli", + "gimli 0.26.2", +] + +[[package]] +name = "addr2line" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" +dependencies = [ + "gimli 0.27.2", ] [[package]] @@ -51,7 +61,18 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ - "getrandom 0.2.8", + "getrandom 0.2.9", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if 1.0.0", "once_cell", "version_check", ] @@ -65,6 +86,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "aho-corasick" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" +dependencies = [ + "memchr", +] + [[package]] name = "alacritty_config" version = "0.1.1-dev" @@ -82,7 +112,7 @@ source = "git+https://github.com/zed-industries/alacritty?rev=a51dbe25d67e84d6ed dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -92,7 +122,7 @@ source = "git+https://github.com/zed-industries/alacritty?rev=a51dbe25d67e84d6ed dependencies = [ "alacritty_config", "alacritty_config_derive", - "base64", + "base64 0.13.1", "bitflags", "dirs 4.0.0", "libc", @@ -145,15 +175,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.66" +version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" [[package]] name = "arrayref" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" [[package]] name = "arrayvec" @@ -232,9 +262,9 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17adb73da160dfb475c183343c8cccd80721ea5a605d3eb57125f0a7b7a92d0b" +checksum = "6fa3dc5f2a8564f07759c008b9109dc0d39de92a88d5588b8a5036d286383afb" dependencies = [ "async-lock", "async-task", @@ -273,32 +303,31 @@ dependencies = [ [[package]] name = "async-io" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c374dda1ed3e7d8f0d9ba58715f924862c63eae6849c92d3a18e7fbde9e2794" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" dependencies = [ "async-lock", "autocfg 1.1.0", + "cfg-if 1.0.0", "concurrent-queue", "futures-lite", - "libc", "log", "parking", "polling", + "rustix 0.37.19", "slab", "socket2", "waker-fn", - "windows-sys 0.42.0", ] [[package]] name = "async-lock" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8101efe8695a6c17e02911402145357e718ac92d3ff88ae8419e84b1707b685" +checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7" dependencies = [ "event-listener", - "futures-lite", ] [[package]] @@ -318,15 +347,15 @@ name = "async-pipe" version = "0.1.3" source = "git+https://github.com/zed-industries/async-pipe-rs?rev=82d00a04211cf4e1236029aa03e6b6ce2a74c553#82d00a04211cf4e1236029aa03e6b6ce2a74c553" dependencies = [ - "futures 0.3.25", + "futures 0.3.28", "log", ] [[package]] name = "async-process" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6381ead98388605d0d9ff86371043b5aa922a3905824244de40dc263a14fcba4" +checksum = "7a9d28b1d97e08915212e2e45310d47854eafa69600756fc735fb788f75199c9" dependencies = [ "async-io", "async-lock", @@ -335,9 +364,9 @@ dependencies = [ "cfg-if 1.0.0", "event-listener", "futures-lite", - "libc", + "rustix 0.37.19", "signal-hook", - "windows-sys 0.42.0", + "windows-sys 0.48.0", ] [[package]] @@ -348,18 +377,18 @@ checksum = "d7d78656ba01f1b93024b7c3a0467f1608e4be67d725749fdcd7d2c7678fd7a2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] name = "async-recursion" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cda8f4bcc10624c4e85bc66b3f452cca98cfa5ca002dc83a16aad2367641bea" +checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.15", ] [[package]] @@ -372,7 +401,7 @@ dependencies = [ "async-global-executor", "async-io", "async-lock", - "crossbeam-utils 0.8.14", + "crossbeam-utils 0.8.15", "futures-channel", "futures-core", "futures-io", @@ -390,23 +419,24 @@ dependencies = [ [[package]] name = "async-stream" -version = "0.3.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dad5c83079eae9969be7fadefe640a1c566901f05ff91ab221de4b6f68d9507e" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" dependencies = [ "async-stream-impl", "futures-core", + "pin-project-lite 0.2.9", ] [[package]] name = "async-stream-impl" -version = "0.3.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.15", ] [[package]] @@ -419,7 +449,7 @@ dependencies = [ "filetime", "libc", "pin-project", - "redox_syscall", + "redox_syscall 0.2.16", "xattr", ] @@ -443,13 +473,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.59" +version = "0.1.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6e93155431f3931513b243d371981bb2770112b370c82745a1d19d2f99364" +checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.15", ] [[package]] @@ -486,9 +516,9 @@ dependencies = [ [[package]] name = "atomic-waker" -version = "1.0.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a" +checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3" [[package]] name = "atty" @@ -548,9 +578,9 @@ checksum = "acee9fd5073ab6b045a275b3e709c163dd36c90685219cb21804a147b58dba43" dependencies = [ "async-trait", "axum-core", - "base64", + "base64 0.13.1", "bitflags", - "bytes 1.3.0", + "bytes 1.4.0", "futures-util", "headers", "http", @@ -582,7 +612,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37e5939e02c56fecd5c017c37df4238c0a839fa76b7f97acdd7efb804fd181cc" dependencies = [ "async-trait", - "bytes 1.3.0", + "bytes 1.4.0", "futures-util", "http", "http-body", @@ -598,7 +628,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69034b3b0fd97923eee2ce8a47540edb21e07f48f87f67d44bb4271cec622bdb" dependencies = [ "axum", - "bytes 1.3.0", + "bytes 1.4.0", "futures-util", "http", "mime", @@ -614,16 +644,16 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.66" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7" +checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" dependencies = [ - "addr2line", + "addr2line 0.19.0", "cc", "cfg-if 1.0.0", "libc", - "miniz_oxide 0.5.4", - "object 0.29.0", + "miniz_oxide 0.6.2", + "object 0.30.3", "rustc-demangle", ] @@ -637,7 +667,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -647,10 +677,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] -name = "base64ct" -version = "1.5.3" +name = "base64" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b645a089122eccb6111b4f81cbc1a49f5900ac4666bb93ac027feaecf15607bf" +checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bincode" @@ -671,7 +707,7 @@ dependencies = [ "cexpr", "clang-sys", "clap 2.34.0", - "env_logger", + "env_logger 0.9.3", "lazy_static", "lazycell", "log", @@ -707,18 +743,18 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.10.3" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "blocking" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c67b173a56acffd6d2326fb7ab938ba0b00a71480e14902b2591c87bc5741e8" +checksum = "77231a1c8f801696fc0123ec6150ce92cffb8e164a02afb9c8ddee0e9b65ad65" dependencies = [ "async-channel", "async-lock", @@ -726,51 +762,52 @@ dependencies = [ "atomic-waker", "fastrand", "futures-lite", + "log", ] [[package]] name = "borsh" -version = "0.9.3" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15bf3650200d8bffa99015595e10f1fbd17de07abbc25bb067da79e769939bfa" +checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" dependencies = [ "borsh-derive", - "hashbrown 0.11.2", + "hashbrown 0.13.2", ] [[package]] name = "borsh-derive" -version = "0.9.3" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6441c552f230375d18e3cc377677914d2ca2b0d36e52129fe15450a2dce46775" +checksum = "0754613691538d51f329cce9af41d7b7ca150bc973056f1156611489475f54f7" dependencies = [ "borsh-derive-internal", "borsh-schema-derive-internal", "proc-macro-crate", "proc-macro2", - "syn", + "syn 1.0.109", ] [[package]] name = "borsh-derive-internal" -version = "0.9.3" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5449c28a7b352f2d1e592a8a28bf139bc71afb0764a14f3c02500935d8c44065" +checksum = "afb438156919598d2c7bad7e1c0adf3d26ed3840dbc010db1a882a65583ca2fb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] name = "borsh-schema-derive-internal" -version = "0.9.3" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdbd5696d8bfa21d53d9fe39a714a18538bad11492a42d066dbbc395fb1951c0" +checksum = "634205cc43f74a1b9046ef87c4540ebda95696ec0f315024860cad7c5b0f5ccd" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -803,45 +840,47 @@ dependencies = [ [[package]] name = "bstr" -version = "0.2.17" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +checksum = "c3d4260bcc2e8fc9df1eac4919a720effeb63a3f0952f5bf4944adfa18897f09" dependencies = [ "memchr", + "serde", ] [[package]] name = "bumpalo" -version = "3.11.1" +version = "3.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" +checksum = "3c6ed94e98ecff0c12dd1b04c15ec0d7d9458ca8fe806cea6f12954efe74c63b" [[package]] name = "bytecheck" -version = "0.6.9" +version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d11cac2c12b5adc6570dad2ee1b87eff4955dac476fe12d81e5fdd352e52406f" +checksum = "13fe11640a23eb24562225322cd3e452b93a3d4091d62fab69c70542fcd17d1f" dependencies = [ "bytecheck_derive", "ptr_meta", + "simdutf8", ] [[package]] name = "bytecheck_derive" -version = "0.6.9" +version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e576ebe98e605500b3c8041bb888e966653577172df6dd97398714eb30b9bf" +checksum = "e31225543cb46f81a7e224762764f4a6a0f097b1db0b175f69e8065efaa42de5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] name = "bytemuck" -version = "1.12.3" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaa3a8d9a1ca92e282c96a32d6511b695d7d994d1d102ba85d279f9b2756947f" +checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" [[package]] name = "byteorder" @@ -861,9 +900,9 @@ dependencies = [ [[package]] name = "bytes" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" [[package]] name = "call" @@ -874,7 +913,7 @@ dependencies = [ "client", "collections", "fs", - "futures 0.3.25", + "futures 0.3.28", "gpui", "language", "live_kit_client", @@ -894,7 +933,7 @@ checksum = "e54b86398b5852ddd45784b1d9b196b98beb39171821bad4b8b44534a1e87927" dependencies = [ "cap-primitives", "cap-std", - "io-lifetimes", + "io-lifetimes 0.5.3", "winapi 0.3.9", ] @@ -905,13 +944,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb8fca3e81fae1d91a36e9784ca22a39ef623702b5f7904d89dc31f10184a178" dependencies = [ "ambient-authority", - "errno", + "errno 0.2.8", "fs-set-times", "io-extras", - "io-lifetimes", + "io-lifetimes 0.5.3", "ipnet", "maybe-owned", - "rustix", + "rustix 0.33.7", "winapi 0.3.9", "winapi-util", "winx", @@ -935,9 +974,9 @@ checksum = "2247568946095c7765ad2b441a56caffc08027734c634a6d5edda648f04e32eb" dependencies = [ "cap-primitives", "io-extras", - "io-lifetimes", + "io-lifetimes 0.5.3", "ipnet", - "rustix", + "rustix 0.33.7", ] [[package]] @@ -948,7 +987,7 @@ checksum = "c50472b6ebc302af0401fa3fb939694cd8ff00e0d4c9182001e434fc822ab83a" dependencies = [ "cap-primitives", "once_cell", - "rustix", + "rustix 0.33.7", "winx", ] @@ -960,9 +999,9 @@ checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6" [[package]] name = "cc" -version = "1.0.77" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" dependencies = [ "jobserver", ] @@ -990,9 +1029,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.23" +version = "0.4.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" dependencies = [ "iana-time-zone", "js-sys", @@ -1006,9 +1045,9 @@ dependencies = [ [[package]] name = "chunked_transfer" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" +checksum = "cca491388666e04d7248af3f60f0c40cfb0991c72205595d7c396e3510207d1a" [[package]] name = "cipher" @@ -1021,9 +1060,9 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.4.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa2e27ae6ab525c3d369ded447057bca5438d86dc3a68f6faafb8269ba82ebf3" +checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" dependencies = [ "glob", "libc", @@ -1047,9 +1086,9 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.23" +version = "3.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" dependencies = [ "atty", "bitflags", @@ -1064,15 +1103,15 @@ dependencies = [ [[package]] name = "clap_derive" -version = "3.2.18" +version = "3.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" +checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008" dependencies = [ - "heck 0.4.0", + "heck 0.4.1", "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -1089,7 +1128,7 @@ name = "cli" version = "0.1.0" dependencies = [ "anyhow", - "clap 3.2.23", + "clap 3.2.25", "core-foundation", "core-services", "dirs 3.0.2", @@ -1097,6 +1136,7 @@ dependencies = [ "plist", "serde", "serde_derive", + "util", ] [[package]] @@ -1108,7 +1148,7 @@ dependencies = [ "async-tungstenite", "collections", "db", - "futures 0.3.25", + "futures 0.3.28", "gpui", "image", "lazy_static", @@ -1117,6 +1157,7 @@ dependencies = [ "postage", "rand 0.8.5", "rpc", + "schemars", "serde", "serde_derive", "settings", @@ -1125,11 +1166,11 @@ dependencies = [ "sum_tree", "tempfile", "thiserror", - "time 0.3.17", + "time 0.3.21", "tiny_http", "url", "util", - "uuid 1.2.2", + "uuid 1.3.2", ] [[package]] @@ -1141,9 +1182,9 @@ dependencies = [ [[package]] name = "cmake" -version = "0.1.49" +version = "0.1.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db34956e100b30725f2eb215f90d4871051239535632f84fea3bc92722c66b7c" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" dependencies = [ "cc", ] @@ -1189,24 +1230,24 @@ dependencies = [ [[package]] name = "collab" -version = "0.12.0" +version = "0.12.4" dependencies = [ "anyhow", "async-tungstenite", "axum", "axum-extra", - "base64", + "base64 0.13.1", "call", - "clap 3.2.23", + "clap 3.2.25", "client", "collections", "ctor", "dashmap", "editor", - "env_logger", + "env_logger 0.9.3", "envy", "fs", - "futures 0.3.25", + "futures 0.3.28", "git", "gpui", "hyper", @@ -1236,7 +1277,7 @@ dependencies = [ "sha-1 0.9.8", "sqlx", "theme", - "time 0.3.17", + "time 0.3.21", "tokio", "tokio-tungstenite", "toml", @@ -1263,7 +1304,7 @@ dependencies = [ "context_menu", "editor", "feedback", - "futures 0.3.25", + "futures 0.3.28", "fuzzy", "gpui", "log", @@ -1299,9 +1340,10 @@ dependencies = [ "collections", "ctor", "editor", - "env_logger", + "env_logger 0.9.3", "fuzzy", "gpui", + "language", "picker", "project", "serde_json", @@ -1313,13 +1355,19 @@ dependencies = [ [[package]] name = "concurrent-queue" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd7bef69dc86e3c610e4e7aed41035e2a7ed12e72dd7530f61327a6579a4390b" +checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" dependencies = [ - "crossbeam-utils 0.8.14", + "crossbeam-utils 0.8.15", ] +[[package]] +name = "const-cstr" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3d0b5ff30645a68f35ece8cea4556ca14ef8a1651455f789a099a0513532a6" + [[package]] name = "context_menu" version = "0.1.0" @@ -1342,7 +1390,7 @@ dependencies = [ "collections", "context_menu", "fs", - "futures 0.3.25", + "futures 0.3.28", "gpui", "language", "log", @@ -1366,8 +1414,10 @@ dependencies = [ "context_menu", "copilot", "editor", - "futures 0.3.25", + "fs", + "futures 0.3.28", "gpui", + "language", "settings", "smol", "theme", @@ -1445,9 +1495,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.5" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" dependencies = [ "libc", ] @@ -1472,7 +1522,7 @@ dependencies = [ "cranelift-codegen-shared", "cranelift-entity", "cranelift-isle", - "gimli", + "gimli 0.26.2", "log", "regalloc2", "smallvec", @@ -1550,18 +1600,18 @@ dependencies = [ [[package]] name = "crc" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53757d12b596c16c78b83458d732a5d1a17ab3f53f2f7412f6fb57cc8a140ab3" +checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" dependencies = [ "crc-catalog", ] [[package]] name = "crc-catalog" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d0165d2900ae6778e36e80bbc4da3b5eefccee9ba939761f9c2882a5d9af3ff" +checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" [[package]] name = "crc32fast" @@ -1584,35 +1634,35 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.6" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" dependencies = [ "cfg-if 1.0.0", - "crossbeam-utils 0.8.14", + "crossbeam-utils 0.8.15", ] [[package]] name = "crossbeam-deque" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" dependencies = [ "cfg-if 1.0.0", "crossbeam-epoch", - "crossbeam-utils 0.8.14", + "crossbeam-utils 0.8.15", ] [[package]] name = "crossbeam-epoch" -version = "0.9.13" +version = "0.9.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" +checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695" dependencies = [ "autocfg 1.1.0", "cfg-if 1.0.0", - "crossbeam-utils 0.8.14", - "memoffset 0.7.1", + "crossbeam-utils 0.8.15", + "memoffset 0.8.0", "scopeguard", ] @@ -1623,7 +1673,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" dependencies = [ "cfg-if 1.0.0", - "crossbeam-utils 0.8.14", + "crossbeam-utils 0.8.15", ] [[package]] @@ -1639,9 +1689,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.14" +version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" +checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" dependencies = [ "cfg-if 1.0.0", ] @@ -1673,7 +1723,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" dependencies = [ "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -1693,9 +1743,9 @@ dependencies = [ [[package]] name = "curl-sys" -version = "0.4.59+curl-7.86.0" +version = "0.4.61+curl-8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cfce34829f448b08f55b7db6d0009e23e2e86a34e8c2b366269bf5799b4a407" +checksum = "14d05c10f541ae6f3bc5b3d923c20001f47db7d5f0b2bc6ad16490133842db79" dependencies = [ "cc", "libc", @@ -1709,9 +1759,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.83" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdf07d07d6531bfcdbe9b8b739b104610c6508dcc4d63b410585faf338241daf" +checksum = "f61f1b6389c3fe1c316bf8a4dccc90a38208354b330925bce1f74a6c4756eb93" dependencies = [ "cc", "cxxbridge-flags", @@ -1721,9 +1771,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.83" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2eb5b96ecdc99f72657332953d4d9c50135af1bac34277801cc3937906ebd39" +checksum = "12cee708e8962df2aeb38f594aae5d827c022b6460ac71a7a3e2c3c2aae5a07b" dependencies = [ "cc", "codespan-reporting", @@ -1731,24 +1781,24 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn", + "syn 2.0.15", ] [[package]] name = "cxxbridge-flags" -version = "1.0.83" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac040a39517fd1674e0f32177648334b0f4074625b5588a64519804ba0553b12" +checksum = "7944172ae7e4068c533afbb984114a56c46e9ccddda550499caa222902c7f7bb" [[package]] name = "cxxbridge-macro" -version = "1.0.83" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1362b0ddcfc4eb0a1f57b68bd77dd99f0e826958a96abd0ae9bd092e114ffed6" +checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.15", ] [[package]] @@ -1761,7 +1811,7 @@ dependencies = [ "hashbrown 0.12.3", "lock_api", "once_cell", - "parking_lot_core 0.9.5", + "parking_lot_core 0.9.7", ] [[package]] @@ -1780,7 +1830,7 @@ dependencies = [ "anyhow", "async-trait", "collections", - "env_logger", + "env_logger 0.9.3", "gpui", "indoc", "lazy_static", @@ -1864,7 +1914,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ - "block-buffer 0.10.3", + "block-buffer 0.10.4", "crypto-common", "subtle", ] @@ -1930,10 +1980,19 @@ dependencies = [ ] [[package]] -name = "dotenvy" -version = "0.15.6" +name = "dlib" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d8c417d7a8cb362e0c37e5d815f5eb7c37f79ff93707329d5a194e42e54ca0" +checksum = "ac1b7517328c04c2aa68422fc60a41b92208182142ed04a25879c26c8f878794" +dependencies = [ + "libloading", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] name = "drag_and_drop" @@ -1957,15 +2016,15 @@ dependencies = [ [[package]] name = "dyn-clone" -version = "1.0.9" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f94fa09c2aeea5b8839e414b7b841bf429fd25b9c522116ac97ee87856d88b2" +checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30" [[package]] name = "editor" version = "0.1.0" dependencies = [ - "aho-corasick", + "aho-corasick 0.7.20", "anyhow", "client", "clock", @@ -1975,11 +2034,10 @@ dependencies = [ "ctor", "db", "drag_and_drop", - "env_logger", - "futures 0.3.25", + "env_logger 0.9.3", + "futures 0.3.28", "fuzzy", "git", - "glob", "gpui", "indoc", "itertools", @@ -1994,6 +2052,7 @@ dependencies = [ "pulldown-cmark", "rand 0.8.5", "rpc", + "schemars", "serde", "serde_derive", "settings", @@ -2008,7 +2067,7 @@ dependencies = [ "tree-sitter-html", "tree-sitter-javascript", "tree-sitter-rust", - "tree-sitter-typescript 0.20.2", + "tree-sitter-typescript 0.20.2 (git+https://github.com/tree-sitter/tree-sitter-typescript?rev=5d20856f34315b068c41edaee2ac8a100081d259)", "unindent", "util", "workspace", @@ -2016,15 +2075,15 @@ dependencies = [ [[package]] name = "either" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" [[package]] name = "encoding_rs" -version = "0.8.31" +version = "0.8.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" dependencies = [ "cfg-if 1.0.0", ] @@ -2042,6 +2101,19 @@ dependencies = [ "termcolor", ] +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal 0.4.7", + "log", + "regex", + "termcolor", +] + [[package]] name = "envy" version = "0.4.2" @@ -2053,9 +2125,9 @@ dependencies = [ [[package]] name = "erased-serde" -version = "0.3.23" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54558e0ba96fbe24280072642eceb9d7d442e32c7ec0ea9e7ecd7b4ea2cf4e11" +checksum = "4f2b0c2380453a92ea8b6c8e5f64ecaafccddde8ceab55ff7a8ac1029f894569" dependencies = [ "serde", ] @@ -2071,6 +2143,17 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "errno-dragonfly" version = "0.1.2" @@ -2093,9 +2176,9 @@ dependencies = [ [[package]] name = "euclid" -version = "0.22.7" +version = "0.22.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b52c2ef4a78da0ba68fbe1fd920627411096d2ac478f7f4c9f3a54ba6705bade" +checksum = "87f253bc5c813ca05792837a0ff4b3a580336b224512d48f7eda1d7dd9210787" dependencies = [ "num-traits", ] @@ -2106,16 +2189,6 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" -[[package]] -name = "expat-sys" -version = "2.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658f19728920138342f68408b7cf7644d90d4784353d8ebc32e7e8663dbe45fa" -dependencies = [ - "cmake", - "pkg-config", -] - [[package]] name = "fallible-iterator" version = "0.2.0" @@ -2124,9 +2197,9 @@ checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" [[package]] name = "fastrand" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" dependencies = [ "instant", ] @@ -2138,7 +2211,7 @@ dependencies = [ "anyhow", "client", "editor", - "futures 0.3.25", + "futures 0.3.28", "gpui", "human_bytes", "isahc", @@ -2162,11 +2235,11 @@ dependencies = [ [[package]] name = "file-per-thread-logger" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e16290574b39ee41c71aeb90ae960c504ebaf1e2a1c87bd52aa56ed6e1a02f" +checksum = "84f2e425d9790201ba4af4630191feac6dcc98765b118d4d18e91d23c2353866" dependencies = [ - "env_logger", + "env_logger 0.10.0", "log", ] @@ -2176,15 +2249,17 @@ version = "0.1.0" dependencies = [ "ctor", "editor", - "env_logger", + "env_logger 0.9.3", "fuzzy", "gpui", + "language", "menu", "picker", "postage", "project", "serde_json", "settings", + "text", "theme", "util", "workspace", @@ -2192,14 +2267,14 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.19" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e884668cd0c7480504233e951174ddc3b382f7c2666e3b7310b5c4e7b0c37f9" +checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall", - "windows-sys 0.42.0", + "redox_syscall 0.2.16", + "windows-sys 0.48.0", ] [[package]] @@ -2210,12 +2285,12 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" dependencies = [ "crc32fast", - "miniz_oxide 0.6.2", + "miniz_oxide 0.7.1", ] [[package]] @@ -2239,7 +2314,7 @@ dependencies = [ "futures-core", "futures-sink", "pin-project", - "spin 0.9.4", + "spin 0.9.8", ] [[package]] @@ -2250,8 +2325,8 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "font-kit" -version = "0.10.0" -source = "git+https://github.com/zed-industries/font-kit?rev=8eaf7a918eafa28b0a37dc759e2e0e7683fa24f1#8eaf7a918eafa28b0a37dc759e2e0e7683fa24f1" +version = "0.11.0" +source = "git+https://github.com/zed-industries/font-kit?rev=b2f77d56f450338aa4f7dd2f0197d8c9acb0cf18#b2f77d56f450338aa4f7dd2f0197d8c9acb0cf18" dependencies = [ "bitflags", "byteorder", @@ -2267,9 +2342,9 @@ dependencies = [ "log", "pathfinder_geometry", "pathfinder_simd", - "servo-fontconfig", "walkdir", "winapi 0.3.9", + "yeslogic-fontconfig-sys", ] [[package]] @@ -2336,7 +2411,7 @@ dependencies = [ "async-trait", "collections", "fsevent", - "futures 0.3.25", + "futures 0.3.28", "git2", "gpui", "lazy_static", @@ -2350,6 +2425,7 @@ dependencies = [ "serde_derive", "serde_json", "smol", + "sum_tree", "tempfile", "util", ] @@ -2360,8 +2436,8 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df62ee66ee2d532ea8d567b5a3f0d03ecd64636b98bad5be1e93dcc918b92aa" dependencies = [ - "io-lifetimes", - "rustix", + "io-lifetimes 0.5.3", + "rustix 0.33.7", "winapi 0.3.9", ] @@ -2414,9 +2490,9 @@ checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" [[package]] name = "futures" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" dependencies = [ "futures-channel", "futures-core", @@ -2429,9 +2505,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" dependencies = [ "futures-core", "futures-sink", @@ -2439,15 +2515,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" [[package]] name = "futures-executor" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" dependencies = [ "futures-core", "futures-task", @@ -2467,15 +2543,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" [[package]] name = "futures-lite" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" dependencies = [ "fastrand", "futures-core", @@ -2488,32 +2564,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.15", ] [[package]] name = "futures-sink" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" [[package]] name = "futures-task" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" [[package]] name = "futures-util" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ "futures 0.1.31", "futures-channel", @@ -2548,9 +2624,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.6" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -2569,9 +2645,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" dependencies = [ "cfg-if 1.0.0", "libc", @@ -2599,6 +2675,12 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "gimli" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" + [[package]] name = "git" version = "0.1.0" @@ -2607,7 +2689,7 @@ dependencies = [ "async-trait", "clock", "collections", - "futures 0.3.25", + "futures 0.3.28", "git2", "lazy_static", "log", @@ -2640,11 +2722,11 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "globset" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a" +checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc" dependencies = [ - "aho-corasick", + "aho-corasick 0.7.20", "bstr", "fnv", "log", @@ -2673,6 +2755,8 @@ dependencies = [ "postage", "settings", "text", + "theme", + "util", "workspace", ] @@ -2693,11 +2777,11 @@ dependencies = [ "core-text", "ctor", "dhat", - "env_logger", + "env_logger 0.9.3", "etagere", "font-kit", "foreign-types", - "futures 0.3.25", + "futures 0.3.28", "gpui_macros", "image", "itertools", @@ -2726,11 +2810,11 @@ dependencies = [ "smol", "sqlez", "sum_tree", - "time 0.3.17", + "time 0.3.21", "tiny-skia", "usvg", "util", - "uuid 1.2.2", + "uuid 1.3.2", "waker-fn", ] @@ -2740,16 +2824,16 @@ version = "0.1.0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] name = "h2" -version = "0.3.15" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" +checksum = "17f8a914c2987b688368b5138aa05321db91f4090cf26118185672ad588bce21" dependencies = [ - "bytes 1.3.0", + "bytes 1.4.0", "fnv", "futures-core", "futures-sink", @@ -2758,7 +2842,7 @@ dependencies = [ "indexmap", "slab", "tokio", - "tokio-util 0.7.4", + "tokio-util 0.7.8", "tracing", ] @@ -2768,7 +2852,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" dependencies = [ - "ahash", + "ahash 0.7.6", ] [[package]] @@ -2777,7 +2861,16 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash", + "ahash 0.7.6", +] + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash 0.8.3", ] [[package]] @@ -2795,9 +2888,9 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" dependencies = [ - "base64", + "base64 0.13.1", "bitflags", - "bytes 1.3.0", + "bytes 1.4.0", "headers-core", "http", "httpdate", @@ -2825,9 +2918,9 @@ dependencies = [ [[package]] name = "heck" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" dependencies = [ "unicode-segmentation", ] @@ -2850,6 +2943,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + [[package]] name = "hex" version = "0.4.3" @@ -2886,11 +2985,11 @@ dependencies = [ [[package]] name = "http" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" dependencies = [ - "bytes 1.3.0", + "bytes 1.4.0", "fnv", "itoa", ] @@ -2901,7 +3000,7 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ - "bytes 1.3.0", + "bytes 1.4.0", "http", "pin-project-lite 0.2.9", ] @@ -2926,9 +3025,9 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] name = "human_bytes" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39b528196c838e8b3da8b665e08c30958a6f2ede91d79f2ffcd0d4664b9c64eb" +checksum = "27e2b089f28ad15597b48d8c0a8fe94eeb1c1cb26ca99b6f66ac9582ae10c5e6" [[package]] name = "humantime" @@ -2938,11 +3037,11 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.23" +version = "0.14.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c" +checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" dependencies = [ - "bytes 1.3.0", + "bytes 1.4.0", "futures-channel", "futures-core", "futures-util", @@ -2978,7 +3077,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ - "bytes 1.3.0", + "bytes 1.4.0", "hyper", "native-tls", "tokio", @@ -2987,16 +3086,16 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.53" +version = "0.1.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "winapi 0.3.9", + "windows", ] [[package]] @@ -3021,11 +3120,10 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.18" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d" +checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492" dependencies = [ - "crossbeam-utils 0.8.14", "globset", "lazy_static", "log", @@ -3058,9 +3156,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.9.2" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg 1.1.0", "hashbrown 0.12.3", @@ -3069,9 +3167,9 @@ dependencies = [ [[package]] name = "indoc" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adab1eaa3408fb7f0c777a73e7465fd5656136fc93b670eb6df3c88c2c1344e3" +checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" [[package]] name = "install_cli" @@ -3099,7 +3197,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0c937cc9891c12eaa8c63ad347e4a288364b1328b924886970b47a14ab8f8f8" dependencies = [ - "io-lifetimes", + "io-lifetimes 0.5.3", "winapi 0.3.9", ] @@ -3113,6 +3211,17 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "io-lifetimes" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" +dependencies = [ + "hermit-abi 0.3.1", + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "iovec" version = "0.1.4" @@ -3143,9 +3252,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.5.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f88c5561171189e69df9d98bcf18fd5f9558300f7ea7b801eb8a0fd748bd8745" +checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" [[package]] name = "is-terminal" @@ -3154,11 +3263,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c89a757e762896bdbdfadf2860d0f8b0cea5e363d8cf3e7bdfeb63d1d976352" dependencies = [ "hermit-abi 0.2.6", - "io-lifetimes", - "rustix", + "io-lifetimes 0.5.3", + "rustix 0.33.7", "winapi 0.3.9", ] +[[package]] +name = "is-terminal" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" +dependencies = [ + "hermit-abi 0.3.1", + "io-lifetimes 1.0.10", + "rustix 0.37.19", + "windows-sys 0.48.0", +] + [[package]] name = "isahc" version = "1.7.2" @@ -3167,7 +3288,7 @@ checksum = "334e04b4d781f436dc315cb1e7515bd96826426345d498149e4bde36b67f8ee9" dependencies = [ "async-channel", "castaway", - "crossbeam-utils 0.8.14", + "crossbeam-utils 0.8.15", "curl", "curl-sys", "encoding_rs", @@ -3197,9 +3318,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.4" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] name = "ittapi-rs" @@ -3212,9 +3333,9 @@ dependencies = [ [[package]] name = "jobserver" -version = "0.1.25" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" +checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" dependencies = [ "libc", ] @@ -3229,6 +3350,8 @@ dependencies = [ "editor", "gpui", "log", + "schemars", + "serde", "settings", "shellexpand", "util", @@ -3246,9 +3369,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.60" +version = "0.3.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +checksum = "68c16e1bfd491478ab155fd8b4896b86f9ede344949b641e61501e07c2b8b4d5" dependencies = [ "wasm-bindgen", ] @@ -3265,7 +3388,7 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6204285f77fe7d9784db3fdc449ecce1a0114927a51d5a41c4c7a292011c015f" dependencies = [ - "base64", + "base64 0.13.1", "crypto-common", "digest 0.10.6", "hmac 0.12.1", @@ -3313,11 +3436,12 @@ dependencies = [ "clock", "collections", "ctor", - "env_logger", + "env_logger 0.9.3", "fs", - "futures 0.3.25", + "futures 0.3.28", "fuzzy", "git", + "globset", "gpui", "indoc", "lazy_static", @@ -3328,6 +3452,7 @@ dependencies = [ "rand 0.8.5", "regex", "rpc", + "schemars", "serde", "serde_derive", "serde_json", @@ -3347,7 +3472,7 @@ dependencies = [ "tree-sitter-python", "tree-sitter-ruby", "tree-sitter-rust", - "tree-sitter-typescript 0.20.1", + "tree-sitter-typescript 0.20.2 (registry+https://github.com/rust-lang/crates.io-index)", "unicase", "unindent", "util", @@ -3393,15 +3518,15 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" [[package]] name = "libc" -version = "0.2.138" +version = "0.2.144" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8" +checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" [[package]] name = "libgit2-sys" -version = "0.14.0+1.5.0" +version = "0.14.2+1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47a00859c70c8a4f7218e6d1cc32875c4b55f6799445b842b0d8ed5e4c3d959b" +checksum = "7f3d95f6b51075fe9810a7ae22c7095f12b98005ab364d8544797a825ce946a4" dependencies = [ "cc", "libc", @@ -3448,9 +3573,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf" +checksum = "56ee889ecc9568871456d42f603d6a0ce59ff328d291063a45cbdf0036baf6db" dependencies = [ "cc", "libc", @@ -3469,9 +3594,9 @@ dependencies = [ [[package]] name = "link-cplusplus" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369" +checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" dependencies = [ "cc", ] @@ -3488,6 +3613,12 @@ version = "0.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5284f00d480e1c39af34e72f8ad60b94f47007e3481cd3b731c1d67190ddc7b7" +[[package]] +name = "linux-raw-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ece97ea872ece730aed82664c424eb4c8291e1ff2480247ccf7409044bc6479f" + [[package]] name = "lipsum" version = "0.8.2" @@ -3507,13 +3638,13 @@ dependencies = [ "async-trait", "block", "byteorder", - "bytes 1.3.0", + "bytes 1.4.0", "cocoa", "collections", "core-foundation", "core-graphics", "foreign-types", - "futures 0.3.25", + "futures 0.3.28", "gpui", "hmac 0.12.1", "jwt", @@ -3538,7 +3669,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", - "futures 0.3.25", + "futures 0.3.28", "hmac 0.12.1", "jwt", "log", @@ -3580,8 +3711,8 @@ dependencies = [ "async-pipe", "collections", "ctor", - "env_logger", - "futures 0.3.25", + "env_logger 0.9.3", + "futures 0.3.28", "gpui", "log", "lsp-types", @@ -3615,7 +3746,7 @@ dependencies = [ "anyhow", "collections", "editor", - "futures 0.3.25", + "futures 0.3.28", "gpui", "language", "lsp", @@ -3657,9 +3788,9 @@ dependencies = [ [[package]] name = "matches" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" [[package]] name = "matchit" @@ -3695,7 +3826,7 @@ dependencies = [ "anyhow", "bindgen", "block", - "bytes 1.3.0", + "bytes 1.4.0", "core-foundation", "foreign-types", "metal", @@ -3737,9 +3868,9 @@ dependencies = [ [[package]] name = "memoffset" -version = "0.7.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" dependencies = [ "autocfg 1.1.0", ] @@ -3767,9 +3898,9 @@ dependencies = [ [[package]] name = "mime" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "minimal-lexical" @@ -3798,18 +3929,18 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.5.4" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" dependencies = [ "adler", ] [[package]] name = "miniz_oxide" -version = "0.6.2" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ "adler", ] @@ -3845,14 +3976,14 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" +checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" dependencies = [ "libc", "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.42.0", + "windows-sys 0.45.0", ] [[package]] @@ -3981,7 +4112,7 @@ dependencies = [ "anyhow", "async-compression", "async-tar", - "futures 0.3.25", + "futures 0.3.28", "gpui", "parking_lot 0.11.2", "serde", @@ -3993,9 +4124,9 @@ dependencies = [ [[package]] name = "nom" -version = "7.1.1" +version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", @@ -4012,9 +4143,9 @@ dependencies = [ [[package]] name = "ntapi" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc51db7b362b205941f71232e56c625156eb9a929f8cf74a428fd5bc094a4afc" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" dependencies = [ "winapi 0.3.9", ] @@ -4102,11 +4233,11 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" dependencies = [ - "hermit-abi 0.1.19", + "hermit-abi 0.2.6", "libc", ] @@ -4116,13 +4247,13 @@ version = "0.5.0" source = "git+https://github.com/KillTheMule/nvim-rs?branch=master#d701c2790dcb2579f8f4d7003ba30e2100a7d25b" dependencies = [ "async-trait", - "futures 0.3.25", + "futures 0.3.28", "log", "parity-tokio-ipc", "rmp", "rmpv", "tokio", - "tokio-util 0.7.4", + "tokio-util 0.7.8", ] [[package]] @@ -4158,18 +4289,18 @@ dependencies = [ [[package]] name = "object" -version = "0.29.0" +version = "0.30.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53" +checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.16.0" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" [[package]] name = "opaque-debug" @@ -4179,9 +4310,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.43" +version = "0.10.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020433887e44c27ff16365eaa2d380547a94544ad509aff6eb5b6e3e0b27b376" +checksum = "01b8574602df80f7b85fdfc5392fa884a4e3b3f4f35402c070ab34c3d3f78d56" dependencies = [ "bitflags", "cfg-if 1.0.0", @@ -4194,13 +4325,13 @@ dependencies = [ [[package]] name = "openssl-macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.15", ] [[package]] @@ -4211,11 +4342,10 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.78" +version = "0.9.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07d5c8cb6e57b3a3612064d7b18b117912b4ce70955c2504d4b741c9e244b132" +checksum = "8e17f59264b2809d77ae94f0e1ebabc434773f370d6ca667bd223ea10e06cc7e" dependencies = [ - "autocfg 1.1.0", "cc", "libc", "pkg-config", @@ -4233,15 +4363,15 @@ dependencies = [ [[package]] name = "os_str_bytes" -version = "6.4.1" +version = "6.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" +checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" [[package]] name = "ouroboros" -version = "0.15.5" +version = "0.15.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbb50b356159620db6ac971c6d5c9ab788c9cc38a6f49619fca2a27acb062ca" +checksum = "e1358bd1558bd2a083fed428ffeda486fbfb323e698cdda7794259d592ca72db" dependencies = [ "aliasable", "ouroboros_macro", @@ -4249,15 +4379,15 @@ dependencies = [ [[package]] name = "ouroboros_macro" -version = "0.15.5" +version = "0.15.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0d9d1a6191c4f391f87219d1ea42b23f09ee84d64763cd05ee6ea88d9f384d" +checksum = "5f7d21ccd03305a674437ee1248f3ab5d4b1db095cf1caf49f1713ddf61956b7" dependencies = [ "Inflector", "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -4274,6 +4404,7 @@ dependencies = [ "settings", "smol", "text", + "theme", "workspace", ] @@ -4298,7 +4429,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9981e32fb75e004cc148f5fb70342f393830e0a4aa62e3cc93b50976218d42b6" dependencies = [ - "futures 0.3.25", + "futures 0.3.28", "libc", "log", "rand 0.7.3", @@ -4308,9 +4439,9 @@ dependencies = [ [[package]] name = "parking" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" +checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e" [[package]] name = "parking_lot" @@ -4320,7 +4451,7 @@ checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", "lock_api", - "parking_lot_core 0.8.5", + "parking_lot_core 0.8.6", ] [[package]] @@ -4330,34 +4461,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core 0.9.5", + "parking_lot_core 0.9.7", ] [[package]] name = "parking_lot_core" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" dependencies = [ "cfg-if 1.0.0", "instant", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "smallvec", "winapi 0.3.9", ] [[package]] name = "parking_lot_core" -version = "0.9.5" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba" +checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "smallvec", - "windows-sys 0.42.0", + "windows-sys 0.45.0", ] [[package]] @@ -4373,9 +4504,9 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.9" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1" +checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" [[package]] name = "pathfinder_color" @@ -4426,7 +4557,7 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd56cbd21fea48d0c440b41cd69c589faacade08c992d9a54e471b79d0fd13eb" dependencies = [ - "base64", + "base64 0.13.1", "once_cell", "regex", ] @@ -4439,9 +4570,9 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pest" -version = "2.5.1" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc8bed3549e0f9b0a2a78bf7c0018237a2cdf085eecbbc048e52612438e4e9d0" +checksum = "e68e84bfb01f0507134eac1e9b410a12ba379d064eab48c50ba4ce329a527b70" dependencies = [ "thiserror", "ucd-trie", @@ -4449,9 +4580,9 @@ dependencies = [ [[package]] name = "petgraph" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5014253a1331579ce62aa67443b4a658c5e7dd03d4bc6d302b94474888143" +checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4" dependencies = [ "fixedbitset", "indexmap", @@ -4463,7 +4594,7 @@ version = "0.1.0" dependencies = [ "ctor", "editor", - "env_logger", + "env_logger 0.9.3", "gpui", "menu", "parking_lot 0.11.2", @@ -4497,7 +4628,7 @@ checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -4520,22 +4651,22 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "plist" -version = "1.3.1" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd39bc6cdc9355ad1dc5eeedefee696bb35c34caf21768741e81826c0bbd7225" +checksum = "9bd9647b268a3d3e14ff09c23201133a62589c658db02bb7388c7246aafe0590" dependencies = [ - "base64", + "base64 0.21.0", "indexmap", "line-wrap", + "quick-xml", "serde", - "time 0.3.17", - "xml-rs", + "time 0.3.21", ] [[package]] @@ -4557,7 +4688,7 @@ dependencies = [ "quote", "serde", "serde_derive", - "syn", + "syn 1.0.109", ] [[package]] @@ -4590,16 +4721,18 @@ dependencies = [ [[package]] name = "polling" -version = "2.5.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "166ca89eb77fd403230b9c156612965a81e094ec6ec3aa13663d4c8b113fa748" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" dependencies = [ "autocfg 1.1.0", + "bitflags", "cfg-if 1.0.0", + "concurrent-queue", "libc", "log", - "wepoll-ffi", - "windows-sys 0.42.0", + "pin-project-lite 0.2.9", + "windows-sys 0.48.0", ] [[package]] @@ -4616,7 +4749,7 @@ checksum = "af3fb618632874fb76937c2361a7f22afd393c982a2165595407edc75b06d3c1" dependencies = [ "atomic", "crossbeam-queue", - "futures 0.3.25", + "futures 0.3.28", "log", "parking_lot 0.12.1", "pin-project", @@ -4661,7 +4794,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "syn", + "syn 1.0.109", "version_check", ] @@ -4678,9 +4811,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.47" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" +checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" dependencies = [ "unicode-ident", ] @@ -4700,7 +4833,7 @@ dependencies = [ name = "project" version = "0.1.0" dependencies = [ - "aho-corasick", + "aho-corasick 0.7.20", "anyhow", "async-trait", "backtrace", @@ -4710,13 +4843,14 @@ dependencies = [ "copilot", "ctor", "db", - "env_logger", + "env_logger 0.9.3", "fs", "fsevent", - "futures 0.3.25", + "futures 0.3.28", "fuzzy", "git", - "glob", + "git2", + "globset", "gpui", "ignore", "itertools", @@ -4730,6 +4864,7 @@ dependencies = [ "rand 0.8.5", "regex", "rpc", + "schemars", "serde", "serde_derive", "serde_json", @@ -4751,14 +4886,18 @@ dependencies = [ name = "project_panel" version = "0.1.0" dependencies = [ + "client", "context_menu", "drag_and_drop", "editor", - "futures 0.3.25", + "futures 0.3.28", "gpui", + "language", "menu", "postage", "project", + "schemars", + "serde", "serde_json", "settings", "theme", @@ -4773,7 +4912,7 @@ version = "0.1.0" dependencies = [ "anyhow", "editor", - "futures 0.3.25", + "futures 0.3.28", "fuzzy", "gpui", "language", @@ -4785,6 +4924,7 @@ dependencies = [ "settings", "smol", "text", + "theme", "util", "workspace", ] @@ -4810,7 +4950,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de5e2533f59d08fcf364fd374ebda0692a70bd6d7e66ef97f306f45c6c5d8020" dependencies = [ - "bytes 1.3.0", + "bytes 1.4.0", "prost-derive 0.8.0", ] @@ -4820,7 +4960,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" dependencies = [ - "bytes 1.3.0", + "bytes 1.4.0", "prost-derive 0.9.0", ] @@ -4830,7 +4970,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62941722fb675d463659e49c4f3fe1fe792ff24fe5bbaa9c08cd3b98a1c354f5" dependencies = [ - "bytes 1.3.0", + "bytes 1.4.0", "heck 0.3.3", "itertools", "lazy_static", @@ -4854,7 +4994,7 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -4867,7 +5007,7 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -4876,7 +5016,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "603bbd6394701d13f3f25aada59c7de9d35a6a5887cfc156181234a44002771b" dependencies = [ - "bytes 1.3.0", + "bytes 1.4.0", "prost 0.8.0", ] @@ -4886,7 +5026,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "534b7a0e836e3c482d2693070f982e39e7611da9695d4d1f5a4b186b51faef0a" dependencies = [ - "bytes 1.3.0", + "bytes 1.4.0", "prost 0.9.0", ] @@ -4922,7 +5062,7 @@ checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -4937,10 +5077,19 @@ dependencies = [ ] [[package]] -name = "quote" -version = "1.0.21" +name = "quick-xml" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" dependencies = [ "proc-macro2", ] @@ -5032,7 +5181,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.8", + "getrandom 0.2.9", ] [[package]] @@ -5046,24 +5195,23 @@ dependencies = [ [[package]] name = "rayon" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e060280438193c554f654141c9ea9417886713b7acd75974c85b18a69a88e0b" +checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" dependencies = [ - "crossbeam-deque", "either", "rayon-core", ] [[package]] name = "rayon-core" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cac410af5d00ab6884528b4ab69d1e8e146e8d471201800fa1b4524126de6ad3" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" dependencies = [ - "crossbeam-channel 0.5.6", + "crossbeam-channel 0.5.8", "crossbeam-deque", - "crossbeam-utils 0.8.14", + "crossbeam-utils 0.8.15", "num_cpus", ] @@ -5097,6 +5245,7 @@ dependencies = [ "settings", "smol", "text", + "theme", "util", "workspace", ] @@ -5110,14 +5259,23 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags", +] + [[package]] name = "redox_users" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ - "getrandom 0.2.8", - "redox_syscall", + "getrandom 0.2.9", + "redox_syscall 0.2.16", "thiserror", ] @@ -5135,13 +5293,13 @@ dependencies = [ [[package]] name = "regex" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" +checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" dependencies = [ - "aho-corasick", + "aho-corasick 1.0.1", "memchr", - "regex-syntax", + "regex-syntax 0.7.1", ] [[package]] @@ -5150,14 +5308,20 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" dependencies = [ - "regex-syntax", + "regex-syntax 0.6.29", ] [[package]] name = "regex-syntax" -version = "0.6.28" +version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" [[package]] name = "region" @@ -5182,21 +5346,21 @@ dependencies = [ [[package]] name = "rend" -version = "0.3.6" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79af64b4b6362ffba04eef3a4e10829718a4896dac19daa741851c86781edf95" +checksum = "581008d2099240d37fb08d77ad713bcaec2c4d89d50b5b21a8bb1996bbab68ab" dependencies = [ "bytecheck", ] [[package]] name = "reqwest" -version = "0.11.13" +version = "0.11.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68cc60575865c7831548863cc02356512e3f1dc2f3f82cb837d7fc4cc8f3c97c" +checksum = "13293b639a097af28fc8a90f22add145a9c954e49d77da06263d58cf44d5fb91" dependencies = [ - "base64", - "bytes 1.3.0", + "base64 0.21.0", + "bytes 1.4.0", "encoding_rs", "futures-core", "futures-util", @@ -5244,9 +5408,9 @@ dependencies = [ [[package]] name = "rgb" -version = "0.8.34" +version = "0.8.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3603b7d71ca82644f79b5a06d1220e9a58ede60bd32255f698cb1af8838b8db3" +checksum = "20ec2d3e3fc7a92ced357df9cebd5a10b6fb2aa1ee797bf7e9ce2f17dffc8f59" dependencies = [ "bytemuck", ] @@ -5268,9 +5432,9 @@ dependencies = [ [[package]] name = "rkyv" -version = "0.7.39" +version = "0.7.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cec2b3485b07d96ddfd3134767b8a447b45ea4eb91448d0a35180ec0ffd5ed15" +checksum = "21499ed91807f07ae081880aabb2ccc0235e9d88011867d984525e9a4c3cfa3e" dependencies = [ "bytecheck", "hashbrown 0.12.3", @@ -5282,13 +5446,13 @@ dependencies = [ [[package]] name = "rkyv_derive" -version = "0.7.39" +version = "0.7.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eaedadc88b53e36dd32d940ed21ae4d850d5916f2581526921f553a72ac34c4" +checksum = "ac1c672430eb41556291981f45ca900a0239ad007242d1cb4b4167af842db666" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -5342,12 +5506,12 @@ dependencies = [ "anyhow", "async-lock", "async-tungstenite", - "base64", + "base64 0.13.1", "clock", "collections", "ctor", - "env_logger", - "futures 0.3.25", + "env_logger 0.9.3", + "futures 0.3.28", "gpui", "parking_lot 0.11.2", "prost 0.8.0", @@ -5386,9 +5550,9 @@ dependencies = [ [[package]] name = "rust-embed" -version = "6.4.2" +version = "6.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "283ffe2f866869428c92e0d61c2f35dfb4355293cdfdc48f49e895c15f1333d1" +checksum = "1b68543d5527e158213414a92832d2aab11a84d2571a5eb021ebe22c43aab066" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -5397,22 +5561,22 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "6.3.1" +version = "6.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31ab23d42d71fb9be1b643fe6765d292c5e14d46912d13f3ae2815ca048ea04d" +checksum = "4d4e0f0ced47ded9a68374ac145edd65a6c1fa13a96447b873660b2a568a0fd7" dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn", + "syn 1.0.109", "walkdir", ] [[package]] name = "rust-embed-utils" -version = "7.3.0" +version = "7.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1669d81dfabd1b5f8e2856b8bbe146c6192b0ba22162edc738ac0a5de18f054" +checksum = "512b0ab6853f7e14e3c8754acb43d6f748bb9ced66aa5915a6553ac8213f7731" dependencies = [ "globset", "sha2 0.10.6", @@ -5421,15 +5585,15 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.27.0" +version = "1.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c321ee4e17d2b7abe12b5d20c1231db708dd36185c8a21e9de5fed6da4dbe9" +checksum = "26bd36b60561ee1fb5ec2817f198b6fd09fa571c897a5e86d1487cfc2b096dfc" dependencies = [ "arrayvec 0.7.2", "borsh", "bytecheck", "byteorder", - "bytes 1.3.0", + "bytes 1.4.0", "num-traits", "rand 0.8.5", "rkyv", @@ -5439,9 +5603,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.21" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustc-hash" @@ -5465,22 +5629,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "938a344304321a9da4973b9ff4f9f8db9caf4597dfd9dda6a60b523340a0fff0" dependencies = [ "bitflags", - "errno", - "io-lifetimes", + "errno 0.2.8", + "io-lifetimes 0.5.3", "itoa", "libc", - "linux-raw-sys", + "linux-raw-sys 0.0.42", "once_cell", "winapi 0.3.9", ] +[[package]] +name = "rustix" +version = "0.37.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" +dependencies = [ + "bitflags", + "errno 0.3.1", + "io-lifetimes 1.0.10", + "libc", + "linux-raw-sys 0.3.7", + "windows-sys 0.48.0", +] + [[package]] name = "rustls" version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" dependencies = [ - "base64", + "base64 0.13.1", "log", "ring", "sct 0.6.1", @@ -5489,9 +5667,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.20.7" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "539a2bfe908f471bfa933876bd1eb6a19cf2176d375f82ef7f99530a40e48c2c" +checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" dependencies = [ "log", "ring", @@ -5501,18 +5679,18 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55" +checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" dependencies = [ - "base64", + "base64 0.21.0", ] [[package]] name = "rustversion" -version = "1.0.9" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" +checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" [[package]] name = "rustybuzz" @@ -5532,9 +5710,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.11" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" [[package]] name = "safe_arch" @@ -5571,19 +5749,18 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" +checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" dependencies = [ - "lazy_static", - "windows-sys 0.36.1", + "windows-sys 0.42.0", ] [[package]] name = "schemars" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a5fb6c61f29e723026dc8e923d94c694313212abbecbbe5f55a7748eec5b307" +checksum = "02c613288622e5f0c3fdc5dbd4db1c5fbe752746b1d1a56a0630b78fd00de44f" dependencies = [ "dyn-clone", "schemars_derive", @@ -5593,14 +5770,14 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f188d036977451159430f3b8dc82ec76364a42b7e289c2b18a9a18f4470058e9" +checksum = "109da1e6b197438deb6db99952990c7f959572794b80ff93707d55a232545e7c" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn", + "syn 1.0.109", ] [[package]] @@ -5617,9 +5794,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "scratch" -version = "1.0.2" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898" +checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" [[package]] name = "scrypt" @@ -5663,7 +5840,7 @@ dependencies = [ "async-stream", "async-trait", "chrono", - "futures 0.3.25", + "futures 0.3.28", "futures-util", "log", "ouroboros", @@ -5676,10 +5853,10 @@ dependencies = [ "serde_json", "sqlx", "thiserror", - "time 0.3.17", + "time 0.3.21", "tracing", "url", - "uuid 1.2.2", + "uuid 1.3.2", ] [[package]] @@ -5691,7 +5868,7 @@ dependencies = [ "heck 0.3.3", "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -5704,8 +5881,8 @@ dependencies = [ "rust_decimal", "sea-query-derive", "serde_json", - "time 0.3.17", - "uuid 1.2.2", + "time 0.3.21", + "uuid 1.3.2", ] [[package]] @@ -5719,8 +5896,8 @@ dependencies = [ "sea-query", "serde_json", "sqlx", - "time 0.3.17", - "uuid 1.2.2", + "time 0.3.21", + "uuid 1.3.2", ] [[package]] @@ -5732,7 +5909,7 @@ dependencies = [ "heck 0.3.3", "proc-macro2", "quote", - "syn", + "syn 1.0.109", "thiserror", ] @@ -5755,7 +5932,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn", + "syn 1.0.109", ] [[package]] @@ -5769,10 +5946,11 @@ name = "search" version = "0.1.0" dependencies = [ "anyhow", + "client", "collections", "editor", - "futures 0.3.25", - "glob", + "futures 0.3.28", + "globset", "gpui", "language", "log", @@ -5793,9 +5971,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.7.0" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" +checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" dependencies = [ "bitflags", "core-foundation", @@ -5806,9 +5984,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.6.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4" dependencies = [ "core-foundation-sys", "libc", @@ -5840,22 +6018,22 @@ checksum = "5a9f47faea3cad316faa914d013d24f471cd90bfca1a0c70f05a3f42c6441e99" [[package]] name = "serde" -version = "1.0.148" +version = "1.0.162" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e53f64bb4ba0191d6d0676e1b141ca55047d83b74f5607e6d8eb88126c52c2dc" +checksum = "71b2f6e1ab5c2b98c05f0f35b236b22e8df7ead6ffbf51d7808da7f8817e7ab6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.148" +version = "1.0.162" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55492425aa53521babf6137309e7d34c20bbfbbfcfe2c7f3a047fd1f6b92c0c" +checksum = "a2a0814352fd64b58489904a44ea8d90cb1a91dcb6b4f5ebabc32c8318e93cb6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.15", ] [[package]] @@ -5866,23 +6044,23 @@ checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] name = "serde_fmt" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2963a69a2b3918c1dc75a45a18bd3fcd1120e31d3f59deb1b2f9b5d5ffb8baa4" +checksum = "e1d4ddca14104cd60529e8c7f7ba71a2c8acd8f7f5cfcdc2faf97eeb7c3010a4" dependencies = [ "serde", ] [[package]] name = "serde_json" -version = "1.0.89" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db" +checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" dependencies = [ "indexmap", "itoa", @@ -5892,13 +6070,13 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.9" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fe39d9fbb0ebf5eb2c7cb7e2a47e4f462fad1379f1166b8ae49ad9eae89a7ca" +checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.15", ] [[package]] @@ -5925,27 +6103,6 @@ dependencies = [ "yaml-rust", ] -[[package]] -name = "servo-fontconfig" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e3e22fe5fd73d04ebf0daa049d3efe3eae55369ce38ab16d07ddd9ac5c217c" -dependencies = [ - "libc", - "servo-fontconfig-sys", -] - -[[package]] -name = "servo-fontconfig-sys" -version = "5.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e36b879db9892dfa40f95da1c38a835d41634b825fbd8c4c418093d53c24b388" -dependencies = [ - "expat-sys", - "freetype-sys", - "pkg-config", -] - [[package]] name = "settings" version = "0.1.0" @@ -5954,8 +6111,7 @@ dependencies = [ "assets", "collections", "fs", - "futures 0.3.25", - "glob", + "futures 0.3.28", "gpui", "json_comments", "lazy_static", @@ -5965,9 +6121,9 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "smallvec", "sqlez", "staff_mode", - "theme", "toml", "tree-sitter", "tree-sitter-json 0.19.0", @@ -6060,9 +6216,9 @@ checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" [[package]] name = "signal-hook" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d" +checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" dependencies = [ "libc", "signal-hook-registry", @@ -6082,13 +6238,19 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" dependencies = [ "libc", ] +[[package]] +name = "simdutf8" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" + [[package]] name = "similar" version = "1.3.0" @@ -6135,18 +6297,18 @@ checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" [[package]] name = "slab" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" dependencies = [ "autocfg 1.1.0", ] [[package]] name = "slice-group-by" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03b634d87b960ab1a38c4fe143b508576f075e7c978bfad18217645ebfdfa2ec" +checksum = "826167069c09b99d56f31e9ae5c99049e932a98c9dc2dac47645b08dbbf76ba7" [[package]] name = "sluice" @@ -6202,9 +6364,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" dependencies = [ "libc", "winapi 0.3.9", @@ -6218,9 +6380,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "spin" -version = "0.9.4" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6002a767bff9e83f8eeecf883ecb8011875a21ae8da43bffb817a57e78cc09" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" dependencies = [ "lock_api", ] @@ -6236,14 +6398,14 @@ name = "sqlez" version = "0.1.0" dependencies = [ "anyhow", - "futures 0.3.25", + "futures 0.3.28", "indoc", "lazy_static", "libsqlite3-sys", "parking_lot 0.11.2", "smol", "thread_local", - "uuid 1.2.2", + "uuid 1.3.2", ] [[package]] @@ -6255,14 +6417,14 @@ dependencies = [ "quote", "sqlez", "sqlformat", - "syn", + "syn 1.0.109", ] [[package]] name = "sqlformat" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87e292b4291f154971a43c3774364e2cbcaec599d3f5bf6fa9d122885dbc38a" +checksum = "0c12bc9199d1db8234678b7051747c07f517cdcf019262d1847b94ec8b1aee3e" dependencies = [ "itertools", "nom", @@ -6271,9 +6433,9 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "788841def501aabde58d3666fcea11351ec3962e6ea75dbcd05c84a71d68bcd1" +checksum = "f8de3b03a925878ed54a954f621e64bf55a3c1bd29652d0d1a17830405350188" dependencies = [ "sqlx-core", "sqlx-macros", @@ -6281,16 +6443,16 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcbc16ddba161afc99e14d1713a453747a2b07fc097d2009f4c300ec99286105" +checksum = "fa8241483a83a3f33aa5fff7e7d9def398ff9990b2752b6c6112b83c6d246029" dependencies = [ - "ahash", + "ahash 0.7.6", "atoi", - "base64", + "base64 0.13.1", "bitflags", "byteorder", - "bytes 1.3.0", + "bytes 1.4.0", "chrono", "crc", "crossbeam-queue", @@ -6321,7 +6483,7 @@ dependencies = [ "percent-encoding", "rand 0.8.5", "rust_decimal", - "rustls 0.20.7", + "rustls 0.20.8", "rustls-pemfile", "serde", "serde_json", @@ -6332,23 +6494,23 @@ dependencies = [ "sqlx-rt", "stringprep", "thiserror", - "time 0.3.17", + "time 0.3.21", "tokio-stream", "url", - "uuid 1.2.2", - "webpki-roots 0.22.5", + "uuid 1.3.2", + "webpki-roots 0.22.6", "whoami", ] [[package]] name = "sqlx-macros" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b850fa514dc11f2ee85be9d055c512aa866746adfacd1cb42d867d68e6a5b0d9" +checksum = "9966e64ae989e7e575b19d7265cb79d7fc3cbbdf179835cb0d716f294c2049c9" dependencies = [ "dotenvy", "either", - "heck 0.4.0", + "heck 0.4.1", "once_cell", "proc-macro2", "quote", @@ -6356,15 +6518,15 @@ dependencies = [ "sha2 0.10.6", "sqlx-core", "sqlx-rt", - "syn", + "syn 1.0.109", "url", ] [[package]] name = "sqlx-rt" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24c5b2d25fa654cc5f841750b8e1cdedbe21189bf9a9382ee90bfa9dd3562396" +checksum = "804d3f245f894e61b1e6263c84b23ca675d96753b5abfd5cc8597d86806e8024" dependencies = [ "once_cell", "tokio", @@ -6425,7 +6587,7 @@ version = "0.1.0" dependencies = [ "arrayvec 0.7.2", "ctor", - "env_logger", + "env_logger 0.9.3", "log", "rand 0.8.5", ] @@ -6467,9 +6629,20 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.105" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" dependencies = [ "proc-macro2", "quote", @@ -6478,21 +6651,9 @@ dependencies = [ [[package]] name = "sync_wrapper" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8" - -[[package]] -name = "synstructure" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "unicode-xid", -] +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] name = "sys-info" @@ -6506,14 +6667,14 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.27.3" +version = "0.27.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1620f9573034c573376acc550f3b9a2be96daeb08abb3c12c8523e1cee06e80f" +checksum = "a902e9050fca0a5d6877550b769abd2bd1ce8c04634b941dbe2809735e1a1e33" dependencies = [ "cfg-if 1.0.0", "core-foundation-sys", "libc", - "ntapi 0.4.0", + "ntapi 0.4.1", "once_cell", "rayon", "winapi 0.3.9", @@ -6529,17 +6690,23 @@ dependencies = [ "bitflags", "cap-fs-ext", "cap-std", - "io-lifetimes", - "rustix", + "io-lifetimes 0.5.3", + "rustix 0.33.7", "winapi 0.3.9", "winx", ] [[package]] -name = "target-lexicon" -version = "0.12.5" +name = "take-until" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9410d0f6853b1d94f0e519fb95df60f29d2c1eff2d921ffdf01a4c8a3b54f12d" +checksum = "8bdb6fa0dfa67b38c1e66b7041ba9dcf23b99d8121907cd31c807a332f7a0bbb" + +[[package]] +name = "target-lexicon" +version = "0.12.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd1ba337640d60c3e96bc6f0638a939b9c9a7f2c316a1598c279828b3d1dc8c5" [[package]] name = "tempdir" @@ -6553,16 +6720,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.3.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" dependencies = [ "cfg-if 1.0.0", "fastrand", - "libc", - "redox_syscall", - "remove_dir_all", - "winapi 0.3.9", + "redox_syscall 0.3.5", + "rustix 0.37.19", + "windows-sys 0.45.0", ] [[package]] @@ -6582,7 +6748,7 @@ dependencies = [ "anyhow", "db", "dirs 4.0.0", - "futures 0.3.25", + "futures 0.3.28", "gpui", "itertools", "lazy_static", @@ -6591,6 +6757,7 @@ dependencies = [ "ordered-float", "procinfo", "rand 0.8.5", + "schemars", "serde", "serde_derive", "settings", @@ -6612,7 +6779,7 @@ dependencies = [ "db", "dirs 4.0.0", "editor", - "futures 0.3.25", + "futures 0.3.28", "gpui", "itertools", "language", @@ -6645,7 +6812,7 @@ dependencies = [ "collections", "ctor", "digest 0.9.0", - "env_logger", + "env_logger 0.9.3", "fs", "gpui", "lazy_static", @@ -6680,13 +6847,17 @@ name = "theme" version = "0.1.0" dependencies = [ "anyhow", + "fs", "gpui", "indexmap", "parking_lot 0.11.2", + "schemars", "serde", "serde_derive", "serde_json", + "settings", "toml", + "util", ] [[package]] @@ -6694,6 +6865,7 @@ name = "theme_selector" version = "0.1.0" dependencies = [ "editor", + "fs", "fuzzy", "gpui", "log", @@ -6722,22 +6894,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.37" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.37" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.15", ] [[package]] @@ -6748,10 +6920,11 @@ checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820" [[package]] name = "thread_local" -version = "1.1.4" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" dependencies = [ + "cfg-if 1.0.0", "once_cell", ] @@ -6779,9 +6952,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.17" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" +checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" dependencies = [ "itoa", "serde", @@ -6791,15 +6964,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.6" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2" +checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" dependencies = [ "time-core", ] @@ -6842,28 +7015,27 @@ dependencies = [ [[package]] name = "tinyvec_macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.22.0" +version = "1.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76ce4a75fb488c605c54bf610f221cea8b0dafb53333c1a67e8ee199dcd2ae3" +checksum = "0aa32867d44e6f2ce3385e89dceb990188b8bb0fb25b0cf576647a6f98ac5105" dependencies = [ "autocfg 1.1.0", - "bytes 1.3.0", + "bytes 1.4.0", "libc", - "memchr", - "mio 0.8.5", + "mio 0.8.6", "num_cpus", "parking_lot 0.12.1", "pin-project-lite 0.2.9", "signal-hook-registry", "socket2", "tokio-macros", - "winapi 0.3.9", + "windows-sys 0.48.0", ] [[package]] @@ -6889,20 +7061,20 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.8.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.15", ] [[package]] name = "tokio-native-tls" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ "native-tls", "tokio", @@ -6914,16 +7086,16 @@ version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" dependencies = [ - "rustls 0.20.7", + "rustls 0.20.8", "tokio", "webpki 0.22.0", ] [[package]] name = "tokio-stream" -version = "0.1.11" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" dependencies = [ "futures-core", "pin-project-lite 0.2.9", @@ -6948,7 +7120,7 @@ version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" dependencies = [ - "bytes 1.3.0", + "bytes 1.4.0", "futures-core", "futures-sink", "log", @@ -6958,11 +7130,11 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.4" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" dependencies = [ - "bytes 1.3.0", + "bytes 1.4.0", "futures-core", "futures-io", "futures-sink", @@ -6973,9 +7145,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.5.9" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" dependencies = [ "serde", ] @@ -6988,8 +7160,8 @@ checksum = "ff08f4649d10a70ffa3522ca559031285d8e421d727ac85c60825761818f5d0a" dependencies = [ "async-stream", "async-trait", - "base64", - "bytes 1.3.0", + "base64 0.13.1", + "bytes 1.4.0", "futures-core", "futures-util", "h2", @@ -7025,7 +7197,7 @@ dependencies = [ "rand 0.8.5", "slab", "tokio", - "tokio-util 0.7.4", + "tokio-util 0.7.8", "tower-layer", "tower-service", "tracing", @@ -7038,7 +7210,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f873044bf02dd1e8239e9c1293ea39dad76dc594ec16185d0a1bf31d8dc8d858" dependencies = [ "bitflags", - "bytes 1.3.0", + "bytes 1.4.0", "futures-core", "futures-util", "http", @@ -7077,13 +7249,13 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.15", ] [[package]] @@ -7129,9 +7301,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" dependencies = [ "matchers", "nu-ansi-term", @@ -7331,9 +7503,9 @@ dependencies = [ [[package]] name = "tree-sitter-typescript" -version = "0.20.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e8ed0ecb931cdff13c6a13f45ccd615156e2779d9ffb0395864e05505e6e86d" +checksum = "079c695c32d39ad089101c66393aeaca30e967fba3486a91f573d2f0e12d290a" dependencies = [ "cc", "tree-sitter", @@ -7359,9 +7531,9 @@ dependencies = [ [[package]] name = "try-lock" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "ttf-parser" @@ -7381,9 +7553,9 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ad3713a14ae247f22a728a0456a545df14acf3867f905adff84be99e23b3ad1" dependencies = [ - "base64", + "base64 0.13.1", "byteorder", - "bytes 1.3.0", + "bytes 1.4.0", "http", "httparse", "log", @@ -7400,9 +7572,9 @@ version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0" dependencies = [ - "base64", + "base64 0.13.1", "byteorder", - "bytes 1.3.0", + "bytes 1.4.0", "http", "httparse", "log", @@ -7415,9 +7587,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" [[package]] name = "ucd-trie" @@ -7436,9 +7608,9 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.8" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-bidi-mirroring" @@ -7460,9 +7632,9 @@ checksum = "7f9af028e052a610d99e066b33304625dea9613170a2563314490a4e6ec5cf7f" [[package]] name = "unicode-ident" -version = "1.0.5" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" [[package]] name = "unicode-normalization" @@ -7481,9 +7653,9 @@ checksum = "7d817255e1bed6dfd4ca47258685d14d2bdcfbc64fdc9e3819bd5848057b8ecc" [[package]] name = "unicode-segmentation" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" [[package]] name = "unicode-vo" @@ -7497,12 +7669,6 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" -[[package]] -name = "unicode-xid" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" - [[package]] name = "unicode_categories" version = "0.1.1" @@ -7511,9 +7677,9 @@ checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" [[package]] name = "unindent" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58ee9362deb4a96cef4d437d1ad49cffc9b9e92d202b6995674e928ce684f112" +checksum = "e1766d682d402817b5ac4490b3c3002d91dfa0d22812f341609f97b08757359c" [[package]] name = "untrusted" @@ -7545,7 +7711,7 @@ version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef8352f317d8f9a918ba5154797fb2a93e2730244041cf7d5be35148266adfa5" dependencies = [ - "base64", + "base64 0.13.1", "data-url", "flate2", "fontdb", @@ -7574,9 +7740,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "utf8parse" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "util" @@ -7585,7 +7751,7 @@ dependencies = [ "anyhow", "backtrace", "dirs 3.0.2", - "futures 0.3.25", + "futures 0.3.28", "git2", "isahc", "lazy_static", @@ -7594,6 +7760,7 @@ dependencies = [ "serde", "serde_json", "smol", + "take-until", "tempdir", "url", ] @@ -7610,16 +7777,16 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ - "getrandom 0.2.8", + "getrandom 0.2.9", ] [[package]] name = "uuid" -version = "1.2.2" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" +checksum = "4dad5567ad0cf5b760e5665964bec1b47dfd077ba8a2544b513f3556d3d239a2" dependencies = [ - "getrandom 0.2.8", + "getrandom 0.2.9", "serde", ] @@ -7665,6 +7832,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" name = "vim" version = "0.1.0" dependencies = [ + "anyhow", "assets", "async-compat", "async-trait", @@ -7718,12 +7886,11 @@ checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" [[package]] name = "walkdir" -version = "2.3.2" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" dependencies = [ "same-file", - "winapi 0.3.9", "winapi-util", ] @@ -7769,10 +7936,10 @@ dependencies = [ "cap-time-ext", "fs-set-times", "io-extras", - "io-lifetimes", - "is-terminal", + "io-lifetimes 0.5.3", + "is-terminal 0.1.0", "lazy_static", - "rustix", + "rustix 0.33.7", "system-interface", "tracing", "wasi-common", @@ -7790,7 +7957,7 @@ dependencies = [ "cap-rand", "cap-std", "io-extras", - "rustix", + "rustix 0.33.7", "thiserror", "tracing", "wiggle", @@ -7799,9 +7966,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.83" +version = "0.2.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +checksum = "5b6cb788c4e39112fbe1822277ef6fb3c55cd86b95cb3d3c4c1c9597e4ac74b4" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", @@ -7809,24 +7976,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.83" +version = "0.2.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +checksum = "35e522ed4105a9d626d885b35d62501b30d9666283a5c8be12c14a8bdafe7822" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.15", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.33" +version = "0.4.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d" +checksum = "083abe15c5d88556b77bdf7aef403625be9e327ad37c62c4e4129af740168163" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -7836,9 +8003,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.83" +version = "0.2.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +checksum = "358a79a0cb89d21db8120cbfb91392335913e4890665b1a7981d9e956903b434" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7846,28 +8013,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.83" +version = "0.2.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +checksum = "4783ce29f09b9d93134d41297aded3a712b7b979e9c6f28c32cb88c973a94869" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.15", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.83" +version = "0.2.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" +checksum = "a901d592cafaa4d711bc324edfaff879ac700b19c3dfd60058d2b445be2691eb" [[package]] name = "wasm-encoder" -version = "0.20.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05632e0a66a6ed8cca593c24223aabd6262f256c3693ad9822c315285f010614" +checksum = "d05d0b6fcd0aeb98adf16e7975331b3c17222aa815148f5b976370ce589d80ef" dependencies = [ "leb128", ] @@ -7922,12 +8089,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1df23c642e1376892f3b72f311596976979cbf8b85469680cdd3a8a063d12a2" dependencies = [ "anyhow", - "base64", + "base64 0.13.1", "bincode", "directories-next", "file-per-thread-logger", "log", - "rustix", + "rustix 0.33.7", "serde", "sha2 0.9.9", "toml", @@ -7947,7 +8114,7 @@ dependencies = [ "cranelift-frontend", "cranelift-native", "cranelift-wasm", - "gimli", + "gimli 0.26.2", "log", "more-asserts", "object 0.28.4", @@ -7965,7 +8132,7 @@ checksum = "839d2820e4b830f4b9e7aa08d4c0acabf4a5036105d639f6dfa1c6891c73bdc6" dependencies = [ "anyhow", "cranelift-entity", - "gimli", + "gimli 0.26.2", "indexmap", "log", "more-asserts", @@ -7984,7 +8151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3248be3c4911233535356025f6562193614a40155ee9094bb6a2b43f0dc82803" dependencies = [ "cc", - "rustix", + "rustix 0.33.7", "winapi 0.3.9", ] @@ -7994,18 +8161,18 @@ version = "0.38.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef0a0bcbfa18b946d890078ba0e1bc76bcc53eccfb40806c0020ec29dcd1bd49" dependencies = [ - "addr2line", + "addr2line 0.17.0", "anyhow", "bincode", "cfg-if 1.0.0", "cpp_demangle", - "gimli", + "gimli 0.26.2", "ittapi-rs", "log", "object 0.28.4", "region", "rustc-demangle", - "rustix", + "rustix 0.33.7", "serde", "target-lexicon", "thiserror", @@ -8023,7 +8190,7 @@ checksum = "4f4779d976206c458edd643d1ac622b6c37e4a0800a8b1d25dfbf245ac2f2cac" dependencies = [ "lazy_static", "object 0.28.4", - "rustix", + "rustix 0.33.7", ] [[package]] @@ -8045,7 +8212,7 @@ dependencies = [ "more-asserts", "rand 0.8.5", "region", - "rustix", + "rustix 0.33.7", "thiserror", "wasmtime-environ", "wasmtime-fiber", @@ -8089,9 +8256,9 @@ dependencies = [ [[package]] name = "wast" -version = "50.0.0" +version = "57.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2cbb59d4ac799842791fe7e806fa5dbbf6b5554d538e51cc8e176db6ff0ae34" +checksum = "6eb0f5ed17ac4421193c7477da05892c2edafd67f9639e3c11a82086416662dc" dependencies = [ "leb128", "memchr", @@ -8101,18 +8268,18 @@ dependencies = [ [[package]] name = "wat" -version = "1.0.52" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "584aaf7a1ecf4d383bbe1a25eeab0cbb8ff96acc6796707ff65cde48f4632f15" +checksum = "ab9ab0d87337c3be2bb6fc5cd331c4ba9fd6bcb4ee85048a0dd59ed9ecf92e53" dependencies = [ - "wast 50.0.0", + "wast 57.0.0", ] [[package]] name = "web-sys" -version = "0.3.60" +version = "0.3.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" +checksum = "16b5f940c7edfdc6d12126d98c9ef4d1b3d470011c47c76a6581df47ad9ba721" dependencies = [ "js-sys", "wasm-bindgen", @@ -8149,9 +8316,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.22.5" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368bfe657969fb01238bb756d351dcade285e0f6fcbd36dcb23359a5169975be" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" dependencies = [ "webpki 0.22.0", ] @@ -8167,14 +8334,18 @@ name = "welcome" version = "0.1.0" dependencies = [ "anyhow", + "client", "db", "editor", + "fs", "fuzzy", "gpui", "install_cli", "log", "picker", "project", + "schemars", + "serde", "settings", "theme", "theme_selector", @@ -8182,20 +8353,11 @@ dependencies = [ "workspace", ] -[[package]] -name = "wepoll-ffi" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" -dependencies = [ - "cc", -] - [[package]] name = "which" -version = "4.3.0" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c831fbbee9e129a8cf93e7747a82da9d95ba8e16621cae60ec2cdc849bacb7b" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" dependencies = [ "either", "libc", @@ -8204,11 +8366,10 @@ dependencies = [ [[package]] name = "whoami" -version = "1.2.3" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6631b6a2fd59b1841b622e8f1a7ad241ef0a46f2d580464ce8140ac94cbd571" +checksum = "2c70234412ca409cc04e864e89523cb0fc37f5e1344ebed5a3ebf4192b6b9f68" dependencies = [ - "bumpalo", "wasm-bindgen", "web-sys", ] @@ -8235,11 +8396,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63a1dccd6b3fbd9a27417f5d30ce9aa3ee9cf529aad453abbf88a49c5d605b79" dependencies = [ "anyhow", - "heck 0.4.0", + "heck 0.4.1", "proc-macro2", "quote", "shellexpand", - "syn", + "syn 1.0.109", "witx", ] @@ -8251,7 +8412,7 @@ checksum = "f1c368d57d9560c34deaa67e06b0953ccf65edb906c525e5a2c866c849b48ec2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", "wiggle-generate", ] @@ -8299,16 +8460,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows-sys" -version = "0.36.1" +name = "windows" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows_aarch64_msvc 0.36.1", - "windows_i686_gnu 0.36.1", - "windows_i686_msvc 0.36.1", - "windows_x86_64_gnu 0.36.1", - "windows_x86_64_msvc 0.36.1", + "windows-targets 0.48.0", ] [[package]] @@ -8317,86 +8474,146 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc 0.42.0", - "windows_i686_gnu 0.42.0", - "windows_i686_msvc 0.42.0", - "windows_x86_64_gnu 0.42.0", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc 0.42.0", + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +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", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" [[package]] name = "windows_aarch64_msvc" -version = "0.36.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" -version = "0.42.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" [[package]] name = "windows_i686_gnu" -version = "0.36.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" -version = "0.42.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" [[package]] name = "windows_i686_msvc" -version = "0.36.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" -version = "0.42.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" [[package]] name = "windows_x86_64_gnu" -version = "0.36.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" -version = "0.42.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" [[package]] name = "windows_x86_64_msvc" -version = "0.36.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" -version = "0.42.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "winreg" @@ -8414,7 +8631,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d5973cb8cd94a77d03ad7e23bbe14889cb29805da1cec0e4aff75e21aebded" dependencies = [ "bitflags", - "io-lifetimes", + "io-lifetimes 0.5.3", "winapi 0.3.9", ] @@ -8445,7 +8662,7 @@ version = "0.1.0" dependencies = [ "anyhow", "assets", - "async-recursion 1.0.0", + "async-recursion 1.0.4", "bincode", "call", "client", @@ -8453,12 +8670,13 @@ dependencies = [ "context_menu", "db", "drag_and_drop", - "env_logger", + "env_logger 0.9.3", "fs", - "futures 0.3.25", + "futures 0.3.28", "gpui", "indoc", "install_cli", + "itertools", "language", "lazy_static", "log", @@ -8466,6 +8684,7 @@ dependencies = [ "parking_lot 0.11.2", "postage", "project", + "schemars", "serde", "serde_derive", "serde_json", @@ -8474,7 +8693,7 @@ dependencies = [ "terminal", "theme", "util", - "uuid 1.2.2", + "uuid 1.3.2", ] [[package]] @@ -8496,12 +8715,6 @@ dependencies = [ "libc", ] -[[package]] -name = "xml-rs" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" - [[package]] name = "xmlparser" version = "0.13.5" @@ -8529,9 +8742,21 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" +[[package]] +name = "yeslogic-fontconfig-sys" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2bbd69036d397ebbff671b1b8e4d918610c181c5a16073b96f984a38d08c386" +dependencies = [ + "const-cstr", + "dlib", + "once_cell", + "pkg-config", +] + [[package]] name = "zed" -version = "0.86.0" +version = "0.88.0" dependencies = [ "activity_indicator", "anyhow", @@ -8558,12 +8783,12 @@ dependencies = [ "db", "diagnostics", "editor", - "env_logger", + "env_logger 0.9.3", "feedback", "file_finder", "fs", "fsevent", - "futures 0.3.25", + "futures 0.3.28", "fuzzy", "go_to_line", "gpui", @@ -8631,13 +8856,13 @@ dependencies = [ "tree-sitter-rust", "tree-sitter-scheme", "tree-sitter-toml", - "tree-sitter-typescript 0.20.2", + "tree-sitter-typescript 0.20.2 (git+https://github.com/tree-sitter/tree-sitter-typescript?rev=5d20856f34315b068c41edaee2ac8a100081d259)", "tree-sitter-yaml", "unindent", "url", "urlencoding", "util", - "uuid 1.2.2", + "uuid 1.3.2", "vim", "welcome", "workspace", @@ -8654,14 +8879,13 @@ dependencies = [ [[package]] name = "zeroize_derive" -version = "1.3.3" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44bf07cb3e50ea2003396695d58bf46bc9887a1f362260446fad6bc4e79bd36c" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn", - "synstructure", + "syn 2.0.15", ] [[package]] @@ -8685,10 +8909,11 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.4+zstd.1.5.2" +version = "2.0.8+zstd.1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fa202f2ef00074143e219d15b62ffc317d17cc33909feac471c044087cad7b0" +checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" dependencies = [ "cc", "libc", + "pkg-config", ] diff --git a/Cargo.toml b/Cargo.toml index 15df687d41..4854be0c7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,7 +77,7 @@ async-trait = { version = "0.1" } ctor = { version = "0.1" } env_logger = { version = "0.9" } futures = { version = "0.3" } -glob = { version = "0.3.1" } +globset = { version = "0.4" } lazy_static = { version = "1.4.0" } log = { version = "0.4.16", features = ["kv_unstable_serde"] } ordered-float = { version = "2.1.1" } @@ -85,6 +85,7 @@ parking_lot = { version = "0.11.1" } postage = { version = "0.5", features = ["futures-traits"] } rand = { version = "0.8.5" } regex = { version = "1.5" } +schemars = { version = "0.8" } serde = { version = "1.0", features = ["derive", "rc"] } serde_derive = { version = "1.0", features = ["deserialize_in_place"] } serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] } @@ -93,6 +94,7 @@ smol = { version = "1.2" } tempdir = { version = "0.3.7" } thiserror = { version = "1.0.29" } time = { version = "0.3", features = ["serde", "serde-well-known"] } +toml = { version = "0.5" } unindent = { version = "0.1.7" } [patch.crates-io] diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 1fca19a83f..02f6a2b0fa 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -192,7 +192,7 @@ } }, { - "context": "BufferSearchBar > Editor", + "context": "BufferSearchBar", "bindings": { "escape": "buffer_search::Dismiss", "tab": "buffer_search::FocusEditor", @@ -201,13 +201,13 @@ } }, { - "context": "ProjectSearchBar > Editor", + "context": "ProjectSearchBar", "bindings": { "escape": "project_search::ToggleFocus" } }, { - "context": "ProjectSearchView > Editor", + "context": "ProjectSearchView", "bindings": { "escape": "project_search::ToggleFocus" } diff --git a/assets/keymaps/jetbrains.json b/assets/keymaps/jetbrains.json index 69a583e1e9..4825d3e8b5 100644 --- a/assets/keymaps/jetbrains.json +++ b/assets/keymaps/jetbrains.json @@ -11,6 +11,7 @@ "ctrl->": "zed::IncreaseBufferFontSize", "ctrl-<": "zed::DecreaseBufferFontSize", "cmd-d": "editor::DuplicateLine", + "cmd-backspace": "editor::DeleteLine", "cmd-pagedown": "editor::MovePageDown", "cmd-pageup": "editor::MovePageUp", "ctrl-alt-shift-b": "editor::SelectToPreviousWordStart", @@ -33,6 +34,7 @@ ], "shift-alt-up": "editor::MoveLineUp", "shift-alt-down": "editor::MoveLineDown", + "cmd-alt-l": "editor::Format", "cmd-[": "pane::GoBack", "cmd-]": "pane::GoForward", "alt-f7": "editor::FindAllReferences", @@ -63,6 +65,7 @@ { "context": "Workspace", "bindings": { + "cmd-shift-o": "file_finder::Toggle", "cmd-shift-a": "command_palette::Toggle", "cmd-alt-o": "project_symbols::Toggle", "cmd-1": "workspace::ToggleLeftDock", diff --git a/assets/settings/default.json b/assets/settings/default.json index d1a6499655..3e4c59e806 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1,6 +1,15 @@ { // The name of the Zed theme to use for the UI "theme": "One Dark", + // The name of a base set of key bindings to use. + // This setting can take four values, each named after another + // text editor: + // + // 1. "VSCode" + // 2. "JetBrains" + // 3. "SublimeText" + // 4. "Atom" + "base_keymap": "VSCode", // Features that can be globally enabled or disabled "features": { // Show Copilot icon in status bar @@ -43,6 +52,19 @@ // 3. Draw all invisible symbols: // "all" "show_whitespaces": "selection", + // Whether to show the scrollbar in the editor. + // This setting can take four values: + // + // 1. Show the scrollbar if there's important information or + // follow the system's configured behavior (default): + // "auto" + // 2. Match the system's configured behavior: + // "system" + // 3. Always show the scrollbar: + // "always" + // 4. Never show the scrollbar: + // "never" + "show_scrollbars": "auto", // Whether the screen sharing icon is shown in the os status bar. "show_call_status_icon": true, // Whether to use language servers to provide code intelligence. @@ -106,11 +128,14 @@ }, // Automatically update Zed "auto_update": true, - // Git gutter behavior configuration. + // Settings specific to the project panel "project_panel": { - "dock": "left", - "default_width": 240 + // Where to dock project panel. Can be 'left' or 'right'. + "dock": "left", + // Default width of the project panel. + "default_width": 240 }, + // Git gutter behavior configuration. "git": { // Control whether the git gutter is shown. May take 2 values: // 1. Show the gutter @@ -155,6 +180,10 @@ "shell": "system", // Where to dock terminals panel. Can be 'left', 'right', 'bottom'. "dock": "bottom", + // Default width when the terminal is docked to the left or right. + "default_width": 640, + // Default height when the terminal is docked to the bottom. + "default_height": 320, // What working directory to use when launching the terminal. // May take 4 values: // 1. Use the current file's project directory. Will Fallback to the diff --git a/crates/activity_indicator/Cargo.toml b/crates/activity_indicator/Cargo.toml index 629aa2c032..43d16e6b9b 100644 --- a/crates/activity_indicator/Cargo.toml +++ b/crates/activity_indicator/Cargo.toml @@ -16,6 +16,11 @@ gpui = { path = "../gpui" } project = { path = "../project" } settings = { path = "../settings" } util = { path = "../util" } +theme = { path = "../theme" } workspace = { path = "../workspace" } + futures.workspace = true smallvec.workspace = true + +[dev-dependencies] +editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index d5ee1364b3..801c8f7172 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -9,7 +9,6 @@ use gpui::{ }; use language::{LanguageRegistry, LanguageServerBinaryStatus}; use project::{LanguageServerProgress, Project}; -use settings::Settings; use smallvec::SmallVec; use std::{cmp::Reverse, fmt::Write, sync::Arc}; use util::ResultExt; @@ -325,12 +324,7 @@ impl View for ActivityIndicator { } = self.content_to_render(cx); let mut element = MouseEventHandler::::new(0, cx, |state, cx| { - let theme = &cx - .global::() - .theme - .workspace - .status_bar - .lsp_status; + let theme = &theme::current(cx).workspace.status_bar.lsp_status; let style = if state.hovered() && on_click.is_some() { theme.hover.as_ref().unwrap_or(&theme.default) } else { diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 68d3776e1c..822886b580 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -1,7 +1,7 @@ mod update_notification; use anyhow::{anyhow, Context, Result}; -use client::{Client, ZED_APP_PATH, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN}; +use client::{Client, TelemetrySettings, ZED_APP_PATH, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN}; use db::kvp::KEY_VALUE_STORE; use gpui::{ actions, platform::AppVersion, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, @@ -10,7 +10,7 @@ use gpui::{ use isahc::AsyncBody; use serde::Deserialize; use serde_derive::Serialize; -use settings::Settings; +use settings::{Setting, SettingsStore}; use smol::{fs::File, io::AsyncReadExt, process::Command}; use std::{ffi::OsString, sync::Arc, time::Duration}; use update_notification::UpdateNotification; @@ -58,18 +58,37 @@ impl Entity for AutoUpdater { type Event = (); } +struct AutoUpdateSetting(bool); + +impl Setting for AutoUpdateSetting { + const KEY: Option<&'static str> = Some("auto_update"); + + type FileContent = Option; + + fn load( + default_value: &Option, + user_values: &[&Option], + _: &AppContext, + ) -> Result { + Ok(Self( + Self::json_merge(default_value, user_values)?.ok_or_else(Self::missing_default)?, + )) + } +} + pub fn init(http_client: Arc, server_url: String, cx: &mut AppContext) { + settings::register::(cx); + if let Some(version) = (*ZED_APP_VERSION).or_else(|| cx.platform().app_version().ok()) { let auto_updater = cx.add_model(|cx| { let updater = AutoUpdater::new(version, http_client, server_url); - let mut update_subscription = cx - .global::() - .auto_update + let mut update_subscription = settings::get::(cx) + .0 .then(|| updater.start_polling(cx)); - cx.observe_global::(move |updater, cx| { - if cx.global::().auto_update { + cx.observe_global::(move |updater, cx| { + if settings::get::(cx).0 { if update_subscription.is_none() { update_subscription = Some(updater.start_polling(cx)) } @@ -102,7 +121,7 @@ fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) { { format!("{server_url}/releases/preview/latest") } else { - format!("{server_url}/releases/latest") + format!("{server_url}/releases/stable/latest") }; cx.platform().open_url(&latest_release_url); } @@ -262,7 +281,7 @@ impl AutoUpdater { let release_channel = cx .has_global::() .then(|| cx.global::().display_name()); - let telemetry = cx.global::().telemetry().metrics(); + let telemetry = settings::get::(cx).metrics; (installation_id, release_channel, telemetry) }); diff --git a/crates/auto_update/src/update_notification.rs b/crates/auto_update/src/update_notification.rs index b48ac2a413..6f31df614d 100644 --- a/crates/auto_update/src/update_notification.rs +++ b/crates/auto_update/src/update_notification.rs @@ -5,7 +5,6 @@ use gpui::{ Element, Entity, View, ViewContext, }; use menu::Cancel; -use settings::Settings; use util::channel::ReleaseChannel; use workspace::notifications::Notification; @@ -27,7 +26,7 @@ impl View for UpdateNotification { } fn render(&mut self, cx: &mut gpui::ViewContext) -> gpui::AnyElement { - let theme = cx.global::().theme.clone(); + let theme = theme::current(cx).clone(); let theme = &theme.update_notification; let app_name = cx.global::().display_name(); diff --git a/crates/breadcrumbs/src/breadcrumbs.rs b/crates/breadcrumbs/src/breadcrumbs.rs index f3be60f8de..906d70abb7 100644 --- a/crates/breadcrumbs/src/breadcrumbs.rs +++ b/crates/breadcrumbs/src/breadcrumbs.rs @@ -4,7 +4,6 @@ use gpui::{ }; use itertools::Itertools; use search::ProjectSearchView; -use settings::Settings; use workspace::{ item::{ItemEvent, ItemHandle}, ToolbarItemLocation, ToolbarItemView, Workspace, @@ -50,7 +49,7 @@ impl View for Breadcrumbs { }; let not_editor = active_item.downcast::().is_none(); - let theme = cx.global::().theme.clone(); + let theme = theme::current(cx).clone(); let style = &theme.workspace.breadcrumbs; let breadcrumbs = match active_item.breadcrumbs(&theme, cx) { diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 9b8009dd69..2b4a375a5b 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -19,6 +19,7 @@ dirs = "3.0" ipc-channel = "0.16" serde.workspace = true serde_derive.workspace = true +util = { path = "../util" } [target.'cfg(target_os = "macos")'.dependencies] core-foundation = "0.9" diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs index 7cad42b534..3a0abbaec7 100644 --- a/crates/cli/src/cli.rs +++ b/crates/cli/src/cli.rs @@ -1,6 +1,5 @@ pub use ipc_channel::ipc; use serde::{Deserialize, Serialize}; -use std::path::PathBuf; #[derive(Serialize, Deserialize)] pub struct IpcHandshake { @@ -10,7 +9,12 @@ pub struct IpcHandshake { #[derive(Debug, Serialize, Deserialize)] pub enum CliRequest { - Open { paths: Vec, wait: bool }, + // The filed is named `path` for compatibility, but now CLI can request + // opening a path at a certain row and/or column: `some/path:123` and `some/path:123:456`. + // + // Since Zed CLI has to be installed separately, there can be situations when old CLI is + // querying new Zed editors, support both formats by using `String` here and parsing it on Zed side later. + Open { paths: Vec, wait: bool }, } #[derive(Debug, Serialize, Deserialize)] @@ -20,3 +24,7 @@ pub enum CliResponse { Stderr { message: String }, Exit { status: i32 }, } + +/// When Zed started not as an *.app but as a binary (e.g. local development), +/// there's a possibility to tell it to behave "regularly". +pub const FORCE_CLI_MODE_ENV_VAR_NAME: &str = "ZED_FORCE_CLI_MODE"; diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index a31e59587f..feebbff61b 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -1,6 +1,6 @@ -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; use clap::Parser; -use cli::{CliRequest, CliResponse, IpcHandshake}; +use cli::{CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME}; use core_foundation::{ array::{CFArray, CFIndex}, string::kCFStringEncodingUTF8, @@ -16,16 +16,20 @@ use std::{ path::{Path, PathBuf}, ptr, }; +use util::paths::PathLikeWithPosition; #[derive(Parser)] #[clap(name = "zed", global_setting(clap::AppSettings::NoAutoVersion))] struct Args { - /// Wait for all of the given paths to be closed before exiting. + /// Wait for all of the given paths to be opened/closed before exiting. #[clap(short, long)] wait: bool, /// A sequence of space-separated paths that you want to open. - #[clap()] - paths: Vec, + /// + /// Use `path:line:row` syntax to open a file at a specific location. + /// Non-existing paths and directories will ignore `:line:row` suffix. + #[clap(value_parser = parse_path_with_position)] + paths_with_position: Vec>, /// Print Zed's version and the app path. #[clap(short, long)] version: bool, @@ -34,6 +38,14 @@ struct Args { bundle_path: Option, } +fn parse_path_with_position( + argument_str: &str, +) -> Result, std::convert::Infallible> { + PathLikeWithPosition::parse_str(argument_str, |path_str| { + Ok(Path::new(path_str).to_path_buf()) + }) +} + #[derive(Debug, Deserialize)] struct InfoPlist { #[serde(rename = "CFBundleShortVersionString")] @@ -43,37 +55,37 @@ struct InfoPlist { fn main() -> Result<()> { let args = Args::parse(); - let bundle_path = if let Some(bundle_path) = args.bundle_path { - bundle_path.canonicalize()? - } else { - locate_bundle()? - }; + let bundle = Bundle::detect(args.bundle_path.as_deref()).context("Bundle detection")?; if args.version { - let plist_path = bundle_path.join("Contents/Info.plist"); - let plist = plist::from_file::<_, InfoPlist>(plist_path)?; - println!( - "Zed {} – {}", - plist.bundle_short_version_string, - bundle_path.to_string_lossy() - ); + println!("{}", bundle.zed_version_string()); return Ok(()); } - for path in args.paths.iter() { + for path in args + .paths_with_position + .iter() + .map(|path_with_position| &path_with_position.path_like) + { if !path.exists() { touch(path.as_path())?; } } - let (tx, rx) = launch_app(bundle_path)?; + let (tx, rx) = bundle.launch()?; tx.send(CliRequest::Open { paths: args - .paths + .paths_with_position .into_iter() - .map(|path| fs::canonicalize(path).map_err(|error| anyhow!(error))) - .collect::>>()?, + .map(|path_with_position| { + let path_with_position = path_with_position.map_path_like(|path| { + fs::canonicalize(&path) + .with_context(|| format!("path {path:?} canonicalization")) + })?; + Ok(path_with_position.to_string(|path| path.display().to_string())) + }) + .collect::>()?, wait: args.wait, })?; @@ -89,6 +101,148 @@ fn main() -> Result<()> { Ok(()) } +enum Bundle { + App { + app_bundle: PathBuf, + plist: InfoPlist, + }, + LocalPath { + executable: PathBuf, + plist: InfoPlist, + }, +} + +impl Bundle { + fn detect(args_bundle_path: Option<&Path>) -> anyhow::Result { + let bundle_path = if let Some(bundle_path) = args_bundle_path { + bundle_path + .canonicalize() + .with_context(|| format!("Args bundle path {bundle_path:?} canonicalization"))? + } else { + locate_bundle().context("bundle autodiscovery")? + }; + + match bundle_path.extension().and_then(|ext| ext.to_str()) { + Some("app") => { + let plist_path = bundle_path.join("Contents/Info.plist"); + let plist = plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| { + format!("Reading *.app bundle plist file at {plist_path:?}") + })?; + Ok(Self::App { + app_bundle: bundle_path, + plist, + }) + } + _ => { + println!("Bundle path {bundle_path:?} has no *.app extension, attempting to locate a dev build"); + let plist_path = bundle_path + .parent() + .with_context(|| format!("Bundle path {bundle_path:?} has no parent"))? + .join("WebRTC.framework/Resources/Info.plist"); + let plist = plist::from_file::<_, InfoPlist>(&plist_path) + .with_context(|| format!("Reading dev bundle plist file at {plist_path:?}"))?; + Ok(Self::LocalPath { + executable: bundle_path, + plist, + }) + } + } + } + + fn plist(&self) -> &InfoPlist { + match self { + Self::App { plist, .. } => plist, + Self::LocalPath { plist, .. } => plist, + } + } + + fn path(&self) -> &Path { + match self { + Self::App { app_bundle, .. } => app_bundle, + Self::LocalPath { + executable: excutable, + .. + } => excutable, + } + } + + fn launch(&self) -> anyhow::Result<(IpcSender, IpcReceiver)> { + let (server, server_name) = + IpcOneShotServer::::new().context("Handshake before Zed spawn")?; + let url = format!("zed-cli://{server_name}"); + + match self { + Self::App { app_bundle, .. } => { + let app_path = app_bundle; + + let status = unsafe { + let app_url = CFURL::from_path(app_path, true) + .with_context(|| format!("invalid app path {app_path:?}"))?; + let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes( + ptr::null(), + url.as_ptr(), + url.len() as CFIndex, + kCFStringEncodingUTF8, + ptr::null(), + )); + let urls_to_open = CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]); + LSOpenFromURLSpec( + &LSLaunchURLSpec { + appURL: app_url.as_concrete_TypeRef(), + itemURLs: urls_to_open.as_concrete_TypeRef(), + passThruParams: ptr::null(), + launchFlags: kLSLaunchDefaults, + asyncRefCon: ptr::null_mut(), + }, + ptr::null_mut(), + ) + }; + + anyhow::ensure!( + status == 0, + "cannot start app bundle {}", + self.zed_version_string() + ); + } + Self::LocalPath { executable, .. } => { + let executable_parent = executable + .parent() + .with_context(|| format!("Executable {executable:?} path has no parent"))?; + let subprocess_stdout_file = + fs::File::create(executable_parent.join("zed_dev.log")) + .with_context(|| format!("Log file creation in {executable_parent:?}"))?; + let subprocess_stdin_file = + subprocess_stdout_file.try_clone().with_context(|| { + format!("Cloning descriptor for file {subprocess_stdout_file:?}") + })?; + let mut command = std::process::Command::new(executable); + let command = command + .env(FORCE_CLI_MODE_ENV_VAR_NAME, "") + .stderr(subprocess_stdout_file) + .stdout(subprocess_stdin_file) + .arg(url); + + command + .spawn() + .with_context(|| format!("Spawning {command:?}"))?; + } + } + + let (_, handshake) = server.accept().context("Handshake after Zed spawn")?; + Ok((handshake.requests, handshake.responses)) + } + + fn zed_version_string(&self) -> String { + let is_dev = matches!(self, Self::LocalPath { .. }); + format!( + "Zed {}{} – {}", + self.plist().bundle_short_version_string, + if is_dev { " (dev)" } else { "" }, + self.path().display(), + ) + } +} + fn touch(path: &Path) -> io::Result<()> { match OpenOptions::new().create(true).write(true).open(path) { Ok(_) => Ok(()), @@ -106,38 +260,3 @@ fn locate_bundle() -> Result { } Ok(app_path) } - -fn launch_app(app_path: PathBuf) -> Result<(IpcSender, IpcReceiver)> { - let (server, server_name) = IpcOneShotServer::::new()?; - let url = format!("zed-cli://{server_name}"); - - let status = unsafe { - let app_url = - CFURL::from_path(&app_path, true).ok_or_else(|| anyhow!("invalid app path"))?; - let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes( - ptr::null(), - url.as_ptr(), - url.len() as CFIndex, - kCFStringEncodingUTF8, - ptr::null(), - )); - let urls_to_open = CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]); - LSOpenFromURLSpec( - &LSLaunchURLSpec { - appURL: app_url.as_concrete_TypeRef(), - itemURLs: urls_to_open.as_concrete_TypeRef(), - passThruParams: ptr::null(), - launchFlags: kLSLaunchDefaults, - asyncRefCon: ptr::null_mut(), - }, - ptr::null_mut(), - ) - }; - - if status == 0 { - let (_, handshake) = server.accept()?; - Ok((handshake.requests, handshake.responses)) - } else { - Err(anyhow!("cannot start {:?}", app_path)) - } -} diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 99c492d638..3ecc515986 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -31,6 +31,7 @@ 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 diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 18a0f32ed6..311d9a2b88 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -15,19 +15,17 @@ use futures::{ TryStreamExt, }; use gpui::{ - actions, - platform::AppVersion, - serde_json::{self}, - AnyModelHandle, AnyWeakModelHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, - ModelHandle, Task, View, ViewContext, WeakViewHandle, + actions, platform::AppVersion, serde_json, AnyModelHandle, AnyWeakModelHandle, + AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Task, View, ViewContext, + WeakViewHandle, }; use lazy_static::lazy_static; use parking_lot::RwLock; use postage::watch; use rand::prelude::*; use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage}; -use serde::Deserialize; -use settings::Settings; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; use std::{ any::TypeId, collections::HashMap, @@ -72,25 +70,34 @@ pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5); actions!(client, [SignIn, SignOut]); -pub fn init(client: Arc, cx: &mut AppContext) { +pub fn init_settings(cx: &mut AppContext) { + settings::register::(cx); +} + +pub fn init(client: &Arc, cx: &mut AppContext) { + init_settings(cx); + + let client = Arc::downgrade(client); cx.add_global_action({ let client = client.clone(); move |_: &SignIn, cx| { - let client = client.clone(); - cx.spawn( - |cx| async move { client.authenticate_and_connect(true, &cx).log_err().await }, - ) - .detach(); + if let Some(client) = client.upgrade() { + cx.spawn( + |cx| async move { client.authenticate_and_connect(true, &cx).log_err().await }, + ) + .detach(); + } } }); cx.add_global_action({ let client = client.clone(); move |_: &SignOut, cx| { - let client = client.clone(); - cx.spawn(|cx| async move { - client.disconnect(&cx); - }) - .detach(); + if let Some(client) = client.upgrade() { + cx.spawn(|cx| async move { + client.disconnect(&cx); + }) + .detach(); + } } }); } @@ -326,6 +333,42 @@ impl Drop for PendingEntitySubscription { } } +#[derive(Copy, Clone)] +pub struct TelemetrySettings { + pub diagnostics: bool, + pub metrics: bool, +} + +#[derive(Clone, Serialize, Deserialize, JsonSchema)] +pub struct TelemetrySettingsContent { + pub diagnostics: Option, + pub metrics: Option, +} + +impl settings::Setting for TelemetrySettings { + const KEY: Option<&'static str> = Some("telemetry"); + + type FileContent = TelemetrySettingsContent; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &AppContext, + ) -> Result { + Ok(Self { + diagnostics: user_values.first().and_then(|v| v.diagnostics).unwrap_or( + default_value + .diagnostics + .ok_or_else(Self::missing_default)?, + ), + metrics: user_values + .first() + .and_then(|v| v.metrics) + .unwrap_or(default_value.metrics.ok_or_else(Self::missing_default)?), + }) + } +} + impl Client { pub fn new(http: Arc, cx: &AppContext) -> Arc { Arc::new(Self { @@ -447,9 +490,7 @@ impl Client { })); } Status::SignedOut | Status::UpgradeRequired => { - let telemetry_settings = cx.read(|cx| cx.global::().telemetry()); - self.telemetry - .set_authenticated_user_info(None, false, telemetry_settings); + cx.read(|cx| self.telemetry.set_authenticated_user_info(None, false, cx)); state._reconnect_task.take(); } _ => {} @@ -740,7 +781,7 @@ impl Client { self.telemetry().report_mixpanel_event( "read credentials from keychain", Default::default(), - cx.global::().telemetry(), + *settings::get::(cx), ); }); } @@ -1033,7 +1074,8 @@ impl Client { let executor = cx.background(); let telemetry = self.telemetry.clone(); let http = self.http.clone(); - let metrics_enabled = cx.read(|cx| cx.global::().telemetry()); + + let telemetry_settings = cx.read(|cx| *settings::get::(cx)); executor.clone().spawn(async move { // Generate a pair of asymmetric encryption keys. The public key will be used by the @@ -1120,7 +1162,7 @@ impl Client { telemetry.report_mixpanel_event( "authenticate with browser", Default::default(), - metrics_enabled, + telemetry_settings, ); Ok(Credentials { diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 7151dcd7bb..b3bdc72c91 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -1,4 +1,4 @@ -use crate::{ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL}; +use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL}; use db::kvp::KEY_VALUE_STORE; use gpui::{ executor::Background, @@ -9,7 +9,6 @@ use lazy_static::lazy_static; use parking_lot::Mutex; use serde::Serialize; use serde_json::json; -use settings::TelemetrySettings; use std::{ io::Write, mem, @@ -86,6 +85,11 @@ pub enum ClickhouseEvent { copilot_enabled: bool, copilot_enabled_for_language: bool, }, + Copilot { + suggestion_id: Option, + suggestion_accepted: bool, + file_extension: Option, + }, } #[derive(Serialize, Debug)] @@ -241,9 +245,9 @@ impl Telemetry { self: &Arc, metrics_id: Option, is_staff: bool, - telemetry_settings: TelemetrySettings, + cx: &AppContext, ) { - if !telemetry_settings.metrics() { + if !settings::get::(cx).metrics { return; } @@ -285,7 +289,7 @@ impl Telemetry { event: ClickhouseEvent, telemetry_settings: TelemetrySettings, ) { - if !telemetry_settings.metrics() { + if !telemetry_settings.metrics { return; } @@ -321,7 +325,7 @@ impl Telemetry { properties: Value, telemetry_settings: TelemetrySettings, ) { - if !telemetry_settings.metrics() { + if !telemetry_settings.metrics { return; } diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 6b3aa7e442..4c2721ffeb 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -5,7 +5,6 @@ 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 settings::Settings; use staff_mode::StaffMode; use std::sync::{Arc, Weak}; use util::http::HttpClient; @@ -144,11 +143,13 @@ impl UserStore { let fetch_metrics_id = client.request(proto::GetPrivateUserInfo {}).log_err(); let (user, info) = futures::join!(fetch_user, fetch_metrics_id); - 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.read(|cx| cx.global::().telemetry()), - ); + 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, _| { diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index a980fdc13e..f2202618f4 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.12.0" +version = "0.12.4" publish = false [[bin]] @@ -51,7 +51,7 @@ tokio = { version = "1", features = ["full"] } tokio-tungstenite = "0.17" tonic = "0.6" tower = "0.4" -toml = "0.5.8" +toml.workspace = true tracing = "0.1.34" tracing-log = "0.1.3" tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] } diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 684b6bffe0..7c6a49f179 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -86,8 +86,8 @@ CREATE TABLE "worktree_repositories" ( "project_id" INTEGER NOT NULL, "worktree_id" INTEGER NOT NULL, "work_directory_id" INTEGER NOT NULL, - "scan_id" INTEGER NOT NULL, "branch" VARCHAR, + "scan_id" INTEGER NOT NULL, "is_deleted" BOOL NOT NULL, PRIMARY KEY(project_id, worktree_id, work_directory_id), FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE, @@ -96,6 +96,23 @@ CREATE TABLE "worktree_repositories" ( CREATE INDEX "index_worktree_repositories_on_project_id" ON "worktree_repositories" ("project_id"); CREATE INDEX "index_worktree_repositories_on_project_id_and_worktree_id" ON "worktree_repositories" ("project_id", "worktree_id"); +CREATE TABLE "worktree_repository_statuses" ( + "project_id" INTEGER NOT NULL, + "worktree_id" INTEGER NOT NULL, + "work_directory_id" INTEGER NOT NULL, + "repo_path" VARCHAR NOT NULL, + "status" INTEGER NOT NULL, + "scan_id" INTEGER NOT NULL, + "is_deleted" BOOL NOT NULL, + PRIMARY KEY(project_id, worktree_id, work_directory_id, repo_path), + FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE, + FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE +); +CREATE INDEX "index_worktree_repository_statuses_on_project_id" ON "worktree_repository_statuses" ("project_id"); +CREATE INDEX "index_worktree_repository_statuses_on_project_id_and_worktree_id" ON "worktree_repository_statuses" ("project_id", "worktree_id"); +CREATE INDEX "index_worktree_repository_statuses_on_project_id_and_worktree_id_and_work_directory_id" ON "worktree_repository_statuses" ("project_id", "worktree_id", "work_directory_id"); + + CREATE TABLE "worktree_diagnostic_summaries" ( "project_id" INTEGER NOT NULL, "worktree_id" INTEGER NOT NULL, diff --git a/crates/collab/migrations/20230511004019_add_repository_statuses.sql b/crates/collab/migrations/20230511004019_add_repository_statuses.sql new file mode 100644 index 0000000000..862561c686 --- /dev/null +++ b/crates/collab/migrations/20230511004019_add_repository_statuses.sql @@ -0,0 +1,15 @@ +CREATE TABLE "worktree_repository_statuses" ( + "project_id" INTEGER NOT NULL, + "worktree_id" INT8 NOT NULL, + "work_directory_id" INT8 NOT NULL, + "repo_path" VARCHAR NOT NULL, + "status" INT8 NOT NULL, + "scan_id" INT8 NOT NULL, + "is_deleted" BOOL NOT NULL, + PRIMARY KEY(project_id, worktree_id, work_directory_id, repo_path), + FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE, + FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE +); +CREATE INDEX "index_wt_repos_statuses_on_project_id" ON "worktree_repository_statuses" ("project_id"); +CREATE INDEX "index_wt_repos_statuses_on_project_id_and_wt_id" ON "worktree_repository_statuses" ("project_id", "worktree_id"); +CREATE INDEX "index_wt_repos_statuses_on_project_id_and_wt_id_and_wd_id" ON "worktree_repository_statuses" ("project_id", "worktree_id", "work_directory_id"); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index bc5b816abf..fd28fb9101 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -15,6 +15,7 @@ mod worktree; mod worktree_diagnostic_summary; mod worktree_entry; mod worktree_repository; +mod worktree_repository_statuses; use crate::executor::Executor; use crate::{Error, Result}; @@ -1513,6 +1514,7 @@ impl Database { let mut db_entries = worktree_entry::Entity::find() .filter( Condition::all() + .add(worktree_entry::Column::ProjectId.eq(project.id)) .add(worktree_entry::Column::WorktreeId.eq(worktree.id)) .add(entry_filter), ) @@ -1552,6 +1554,7 @@ impl Database { let mut db_repositories = worktree_repository::Entity::find() .filter( Condition::all() + .add(worktree_repository::Column::ProjectId.eq(project.id)) .add(worktree_repository::Column::WorktreeId.eq(worktree.id)) .add(repository_entry_filter), ) @@ -1568,6 +1571,54 @@ impl Database { worktree.updated_repositories.push(proto::RepositoryEntry { work_directory_id: db_repository.work_directory_id as u64, branch: db_repository.branch, + removed_repo_paths: Default::default(), + updated_statuses: Default::default(), + }); + } + } + } + + // Repository Status Entries + for repository in worktree.updated_repositories.iter_mut() { + let repository_status_entry_filter = + if let Some(rejoined_worktree) = rejoined_worktree { + worktree_repository_statuses::Column::ScanId + .gt(rejoined_worktree.scan_id) + } else { + worktree_repository_statuses::Column::IsDeleted.eq(false) + }; + + let mut db_repository_statuses = + worktree_repository_statuses::Entity::find() + .filter( + Condition::all() + .add( + worktree_repository_statuses::Column::ProjectId + .eq(project.id), + ) + .add( + worktree_repository_statuses::Column::WorktreeId + .eq(worktree.id), + ) + .add( + worktree_repository_statuses::Column::WorkDirectoryId + .eq(repository.work_directory_id), + ) + .add(repository_status_entry_filter), + ) + .stream(&*tx) + .await?; + + while let Some(db_status_entry) = db_repository_statuses.next().await { + let db_status_entry = db_status_entry?; + if db_status_entry.is_deleted { + repository + .removed_repo_paths + .push(db_status_entry.repo_path); + } else { + repository.updated_statuses.push(proto::StatusEntry { + repo_path: db_status_entry.repo_path, + status: db_status_entry.status as i32, }); } } @@ -2395,6 +2446,68 @@ impl Database { ) .exec(&*tx) .await?; + + for repository in update.updated_repositories.iter() { + if !repository.updated_statuses.is_empty() { + worktree_repository_statuses::Entity::insert_many( + repository.updated_statuses.iter().map(|status_entry| { + worktree_repository_statuses::ActiveModel { + project_id: ActiveValue::set(project_id), + worktree_id: ActiveValue::set(worktree_id), + work_directory_id: ActiveValue::set( + repository.work_directory_id as i64, + ), + repo_path: ActiveValue::set(status_entry.repo_path.clone()), + status: ActiveValue::set(status_entry.status as i64), + scan_id: ActiveValue::set(update.scan_id as i64), + is_deleted: ActiveValue::set(false), + } + }), + ) + .on_conflict( + OnConflict::columns([ + worktree_repository_statuses::Column::ProjectId, + worktree_repository_statuses::Column::WorktreeId, + worktree_repository_statuses::Column::WorkDirectoryId, + worktree_repository_statuses::Column::RepoPath, + ]) + .update_columns([ + worktree_repository_statuses::Column::ScanId, + worktree_repository_statuses::Column::Status, + worktree_repository_statuses::Column::IsDeleted, + ]) + .to_owned(), + ) + .exec(&*tx) + .await?; + } + + if !repository.removed_repo_paths.is_empty() { + worktree_repository_statuses::Entity::update_many() + .filter( + worktree_repository_statuses::Column::ProjectId + .eq(project_id) + .and( + worktree_repository_statuses::Column::WorktreeId + .eq(worktree_id), + ) + .and( + worktree_repository_statuses::Column::WorkDirectoryId + .eq(repository.work_directory_id as i64), + ) + .and(worktree_repository_statuses::Column::RepoPath.is_in( + repository.removed_repo_paths.iter().map(String::as_str), + )), + ) + .set(worktree_repository_statuses::ActiveModel { + is_deleted: ActiveValue::Set(true), + scan_id: ActiveValue::Set(update.scan_id as i64), + ..Default::default() + }) + .exec(&*tx) + .await?; + } + } } if !update.removed_repositories.is_empty() { @@ -2645,10 +2758,42 @@ impl Database { if let Some(worktree) = worktrees.get_mut(&(db_repository_entry.worktree_id as u64)) { - worktree.repository_entries.push(proto::RepositoryEntry { - work_directory_id: db_repository_entry.work_directory_id as u64, - branch: db_repository_entry.branch, - }); + worktree.repository_entries.insert( + db_repository_entry.work_directory_id as u64, + proto::RepositoryEntry { + work_directory_id: db_repository_entry.work_directory_id as u64, + branch: db_repository_entry.branch, + removed_repo_paths: Default::default(), + updated_statuses: Default::default(), + }, + ); + } + } + } + + { + let mut db_status_entries = worktree_repository_statuses::Entity::find() + .filter( + Condition::all() + .add(worktree_repository_statuses::Column::ProjectId.eq(project_id)) + .add(worktree_repository_statuses::Column::IsDeleted.eq(false)), + ) + .stream(&*tx) + .await?; + + while let Some(db_status_entry) = db_status_entries.next().await { + let db_status_entry = db_status_entry?; + if let Some(worktree) = worktrees.get_mut(&(db_status_entry.worktree_id as u64)) + { + if let Some(repository_entry) = worktree + .repository_entries + .get_mut(&(db_status_entry.work_directory_id as u64)) + { + repository_entry.updated_statuses.push(proto::StatusEntry { + repo_path: db_status_entry.repo_path, + status: db_status_entry.status as i32, + }); + } } } } @@ -3390,7 +3535,7 @@ pub struct Worktree { pub root_name: String, pub visible: bool, pub entries: Vec, - pub repository_entries: Vec, + pub repository_entries: BTreeMap, pub diagnostic_summaries: Vec, pub scan_id: u64, pub completed_scan_id: u64, diff --git a/crates/collab/src/db/worktree_repository_statuses.rs b/crates/collab/src/db/worktree_repository_statuses.rs new file mode 100644 index 0000000000..fc15efc816 --- /dev/null +++ b/crates/collab/src/db/worktree_repository_statuses.rs @@ -0,0 +1,23 @@ +use super::ProjectId; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "worktree_repository_statuses")] +pub struct Model { + #[sea_orm(primary_key)] + pub project_id: ProjectId, + #[sea_orm(primary_key)] + pub worktree_id: i64, + #[sea_orm(primary_key)] + pub work_directory_id: i64, + #[sea_orm(primary_key)] + pub repo_path: String, + pub status: i64, + pub scan_id: i64, + pub is_deleted: bool, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 23935904d3..ac86f8c171 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -51,7 +51,7 @@ use std::{ atomic::{AtomicBool, Ordering::SeqCst}, Arc, }, - time::Duration, + time::{Duration, Instant}, }; use tokio::sync::{watch, Semaphore}; use tower::ServiceBuilder; @@ -397,10 +397,16 @@ impl Server { "message received" ); }); + let start_time = Instant::now(); let future = (handler)(*envelope, session); async move { - if let Err(error) = future.await { - tracing::error!(%error, "error handling message"); + let result = future.await; + let duration_ms = start_time.elapsed().as_micros() as f64 / 1000.0; + match result { + Err(error) => { + tracing::error!(%error, ?duration_ms, "error handling message") + } + Ok(()) => tracing::info!(?duration_ms, "finished handling message"), } } .instrument(span) @@ -1385,7 +1391,7 @@ async fn join_project( removed_entries: Default::default(), scan_id: worktree.scan_id, is_last_update: worktree.scan_id == worktree.completed_scan_id, - updated_repositories: worktree.repository_entries, + updated_repositories: worktree.repository_entries.into_values().collect(), removed_repositories: Default::default(), }; for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) { diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index cbc4b3bd45..b51c5240a8 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -19,7 +19,7 @@ use gpui::{ use language::LanguageRegistry; use parking_lot::Mutex; use project::{Project, WorktreeId}; -use settings::Settings; +use settings::SettingsStore; use std::{ cell::{Ref, RefCell, RefMut}, env, @@ -30,7 +30,6 @@ use std::{ Arc, }, }; -use theme::ThemeRegistry; use util::http::FakeHttpClient; use workspace::Workspace; @@ -102,7 +101,7 @@ impl TestServer { async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient { cx.update(|cx| { - cx.set_global(Settings::test(cx)); + cx.set_global(SettingsStore::test(cx)); }); let http = FakeHttpClient::with_404_response(); @@ -191,15 +190,18 @@ impl TestServer { client: client.clone(), user_store: user_store.clone(), languages: Arc::new(LanguageRegistry::test()), - themes: ThemeRegistry::new((), cx.font_cache()), fs: fs.clone(), build_window_options: |_, _, _| Default::default(), initialize_workspace: |_, _, _, _| unimplemented!(), background_actions: || &[], }); - Project::init(&client); cx.update(|cx| { + theme::init((), cx); + Project::init(&client, cx); + client::init(&client, cx); + language::init(cx); + editor::init_settings(cx); workspace::init(app_state.clone(), cx); call::init(client.clone(), user_store.clone(), cx); }); diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index e3b5b0be7e..807510d705 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -10,7 +10,7 @@ use editor::{ ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToOffset, ToggleCodeActions, Undo, }; -use fs::{FakeFs, Fs as _, LineEnding, RemoveOptions}; +use fs::{repository::GitFileStatus, FakeFs, Fs as _, LineEnding, RemoveOptions}; use futures::StreamExt as _; use gpui::{ executor::Deterministic, geometry::vector::vec2f, test::EmptyView, AppContext, ModelHandle, @@ -18,6 +18,7 @@ use gpui::{ }; use indoc::indoc; use language::{ + language_settings::{AllLanguageSettings, Formatter}, tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, LanguageConfig, OffsetRangeExt, Point, Rope, }; @@ -26,7 +27,7 @@ use lsp::LanguageServerId; use project::{search::SearchQuery, DiagnosticSummary, HoverBlockKind, Project, ProjectPath}; use rand::prelude::*; use serde_json::json; -use settings::{Formatter, Settings}; +use settings::SettingsStore; use std::{ cell::{Cell, RefCell}, env, future, mem, @@ -1438,7 +1439,6 @@ async fn test_host_disconnect( cx_b: &mut TestAppContext, cx_c: &mut TestAppContext, ) { - cx_b.update(editor::init); deterministic.forbid_parking(); let mut server = TestServer::start(&deterministic).await; let client_a = server.create_client(cx_a, "user_a").await; @@ -1448,6 +1448,8 @@ async fn test_host_disconnect( .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) .await; + cx_b.update(editor::init); + client_a .fs .insert_tree( @@ -1545,7 +1547,6 @@ async fn test_project_reconnect( cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - cx_b.update(editor::init); deterministic.forbid_parking(); let mut server = TestServer::start(&deterministic).await; let client_a = server.create_client(cx_a, "user_a").await; @@ -1554,6 +1555,8 @@ async fn test_project_reconnect( .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; + cx_b.update(editor::init); + client_a .fs .insert_tree( @@ -2434,7 +2437,7 @@ async fn test_git_diff_base_change( buffer_local_a.read_with(cx_a, |buffer, _| { assert_eq!(buffer.diff_base(), Some(diff_base.as_ref())); git::diff::assert_hunks( - buffer.snapshot().git_diff_hunks_in_row_range(0..4, false), + buffer.snapshot().git_diff_hunks_in_row_range(0..4), &buffer, &diff_base, &[(1..2, "", "two\n")], @@ -2454,7 +2457,7 @@ async fn test_git_diff_base_change( buffer_remote_a.read_with(cx_b, |buffer, _| { assert_eq!(buffer.diff_base(), Some(diff_base.as_ref())); git::diff::assert_hunks( - buffer.snapshot().git_diff_hunks_in_row_range(0..4, false), + buffer.snapshot().git_diff_hunks_in_row_range(0..4), &buffer, &diff_base, &[(1..2, "", "two\n")], @@ -2478,7 +2481,7 @@ async fn test_git_diff_base_change( assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref())); git::diff::assert_hunks( - buffer.snapshot().git_diff_hunks_in_row_range(0..4, false), + buffer.snapshot().git_diff_hunks_in_row_range(0..4), &buffer, &diff_base, &[(2..3, "", "three\n")], @@ -2489,7 +2492,7 @@ async fn test_git_diff_base_change( buffer_remote_a.read_with(cx_b, |buffer, _| { assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref())); git::diff::assert_hunks( - buffer.snapshot().git_diff_hunks_in_row_range(0..4, false), + buffer.snapshot().git_diff_hunks_in_row_range(0..4), &buffer, &diff_base, &[(2..3, "", "three\n")], @@ -2532,7 +2535,7 @@ async fn test_git_diff_base_change( buffer_local_b.read_with(cx_a, |buffer, _| { assert_eq!(buffer.diff_base(), Some(diff_base.as_ref())); git::diff::assert_hunks( - buffer.snapshot().git_diff_hunks_in_row_range(0..4, false), + buffer.snapshot().git_diff_hunks_in_row_range(0..4), &buffer, &diff_base, &[(1..2, "", "two\n")], @@ -2552,7 +2555,7 @@ async fn test_git_diff_base_change( buffer_remote_b.read_with(cx_b, |buffer, _| { assert_eq!(buffer.diff_base(), Some(diff_base.as_ref())); git::diff::assert_hunks( - buffer.snapshot().git_diff_hunks_in_row_range(0..4, false), + buffer.snapshot().git_diff_hunks_in_row_range(0..4), &buffer, &diff_base, &[(1..2, "", "two\n")], @@ -2580,12 +2583,12 @@ async fn test_git_diff_base_change( "{:?}", buffer .snapshot() - .git_diff_hunks_in_row_range(0..4, false) + .git_diff_hunks_in_row_range(0..4) .collect::>() ); git::diff::assert_hunks( - buffer.snapshot().git_diff_hunks_in_row_range(0..4, false), + buffer.snapshot().git_diff_hunks_in_row_range(0..4), &buffer, &diff_base, &[(2..3, "", "three\n")], @@ -2596,7 +2599,7 @@ async fn test_git_diff_base_change( buffer_remote_b.read_with(cx_b, |buffer, _| { assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref())); git::diff::assert_hunks( - buffer.snapshot().git_diff_hunks_in_row_range(0..4, false), + buffer.snapshot().git_diff_hunks_in_row_range(0..4), &buffer, &diff_base, &[(2..3, "", "three\n")], @@ -2690,6 +2693,154 @@ async fn test_git_branch_name( }); } +#[gpui::test] +async fn test_git_status_sync( + 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; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + + client_a + .fs + .insert_tree( + "/dir", + json!({ + ".git": {}, + "a.txt": "a", + "b.txt": "b", + }), + ) + .await; + + const A_TXT: &'static str = "a.txt"; + const B_TXT: &'static str = "b.txt"; + + client_a + .fs + .as_fake() + .set_status_for_repo( + Path::new("/dir/.git"), + &[ + (&Path::new(A_TXT), GitFileStatus::Added), + (&Path::new(B_TXT), GitFileStatus::Added), + ], + ) + .await; + + let (project_local, _worktree_id) = client_a.build_local_project("/dir", cx_a).await; + let project_id = active_call_a + .update(cx_a, |call, cx| { + call.share_project(project_local.clone(), cx) + }) + .await + .unwrap(); + + let project_remote = client_b.build_remote_project(project_id, cx_b).await; + + // Wait for it to catch up to the new status + deterministic.run_until_parked(); + + #[track_caller] + fn assert_status( + file: &impl AsRef, + status: Option, + project: &Project, + cx: &AppContext, + ) { + let file = file.as_ref(); + let worktrees = project.visible_worktrees(cx).collect::>(); + assert_eq!(worktrees.len(), 1); + let worktree = worktrees[0].clone(); + let snapshot = worktree.read(cx).snapshot(); + let root_entry = snapshot.root_git_entry().unwrap(); + assert_eq!(root_entry.status_for_file(&snapshot, file), status); + } + + // Smoke test status reading + project_local.read_with(cx_a, |project, cx| { + assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx); + assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx); + }); + project_remote.read_with(cx_b, |project, cx| { + assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx); + assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx); + }); + + client_a + .fs + .as_fake() + .set_status_for_repo( + Path::new("/dir/.git"), + &[ + (&Path::new(A_TXT), GitFileStatus::Modified), + (&Path::new(B_TXT), GitFileStatus::Modified), + ], + ) + .await; + + // Wait for buffer_local_a to receive it + deterministic.run_until_parked(); + + // Smoke test status reading + project_local.read_with(cx_a, |project, cx| { + assert_status( + &Path::new(A_TXT), + Some(GitFileStatus::Modified), + project, + cx, + ); + assert_status( + &Path::new(B_TXT), + Some(GitFileStatus::Modified), + project, + cx, + ); + }); + project_remote.read_with(cx_b, |project, cx| { + assert_status( + &Path::new(A_TXT), + Some(GitFileStatus::Modified), + project, + cx, + ); + assert_status( + &Path::new(B_TXT), + Some(GitFileStatus::Modified), + project, + cx, + ); + }); + + // And synchronization while joining + let project_remote_c = client_c.build_remote_project(project_id, cx_c).await; + deterministic.run_until_parked(); + + project_remote_c.read_with(cx_c, |project, cx| { + assert_status( + &Path::new(A_TXT), + Some(GitFileStatus::Modified), + project, + cx, + ); + assert_status( + &Path::new(B_TXT), + Some(GitFileStatus::Modified), + project, + cx, + ); + }); +} + #[gpui::test(iterations = 10)] async fn test_fs_operations( deterministic: Arc, @@ -4219,10 +4370,12 @@ async fn test_formatting_buffer( // Ensure buffer can be formatted using an external command. Notice how the // host's configuration is honored as opposed to using the guest's settings. cx_a.update(|cx| { - cx.update_global(|settings: &mut Settings, _| { - settings.editor_defaults.formatter = Some(Formatter::External { - command: "awk".to_string(), - arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()], + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |file| { + file.defaults.formatter = Some(Formatter::External { + command: "awk".into(), + arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into(), + }); }); }); }); @@ -4989,7 +5142,6 @@ async fn test_collaborating_with_code_actions( cx_b: &mut TestAppContext, ) { deterministic.forbid_parking(); - cx_b.update(editor::init); 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; @@ -4998,6 +5150,8 @@ async fn test_collaborating_with_code_actions( .await; let active_call_a = cx_a.read(ActiveCall::global); + cx_b.update(editor::init); + // Set up a fake language server. let mut language = Language::new( LanguageConfig { @@ -5202,7 +5356,6 @@ async fn test_collaborating_with_renames( cx_b: &mut TestAppContext, ) { deterministic.forbid_parking(); - cx_b.update(editor::init); 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; @@ -5211,6 +5364,8 @@ async fn test_collaborating_with_renames( .await; let active_call_a = cx_a.read(ActiveCall::global); + cx_b.update(editor::init); + // Set up a fake language server. let mut language = Language::new( LanguageConfig { @@ -5392,8 +5547,6 @@ async fn test_language_server_statuses( cx_b: &mut TestAppContext, ) { deterministic.forbid_parking(); - - cx_b.update(editor::init); 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; @@ -5402,6 +5555,8 @@ async fn test_language_server_statuses( .await; let active_call_a = cx_a.read(ActiveCall::global); + cx_b.update(editor::init); + // Set up a fake language server. let mut language = Language::new( LanguageConfig { @@ -6109,8 +6264,6 @@ async fn test_basic_following( cx_d: &mut TestAppContext, ) { deterministic.forbid_parking(); - cx_a.update(editor::init); - cx_b.update(editor::init); let mut server = TestServer::start(&deterministic).await; let client_a = server.create_client(cx_a, "user_a").await; @@ -6128,6 +6281,9 @@ async fn test_basic_following( let active_call_a = cx_a.read(ActiveCall::global); let active_call_b = cx_b.read(ActiveCall::global); + cx_a.update(editor::init); + cx_b.update(editor::init); + client_a .fs .insert_tree( @@ -6706,9 +6862,6 @@ async fn test_following_tab_order( cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { - cx_a.update(editor::init); - cx_b.update(editor::init); - 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; @@ -6718,6 +6871,9 @@ async fn test_following_tab_order( let active_call_a = cx_a.read(ActiveCall::global); let active_call_b = cx_b.read(ActiveCall::global); + cx_a.update(editor::init); + cx_b.update(editor::init); + client_a .fs .insert_tree( @@ -6828,9 +6984,6 @@ async fn test_peers_following_each_other( cx_b: &mut TestAppContext, ) { deterministic.forbid_parking(); - cx_a.update(editor::init); - cx_b.update(editor::init); - 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; @@ -6840,6 +6993,9 @@ async fn test_peers_following_each_other( let active_call_a = cx_a.read(ActiveCall::global); let active_call_b = cx_b.read(ActiveCall::global); + cx_a.update(editor::init); + cx_b.update(editor::init); + // Client A shares a project. client_a .fs @@ -6999,8 +7155,6 @@ async fn test_auto_unfollowing( cx_b: &mut TestAppContext, ) { deterministic.forbid_parking(); - cx_a.update(editor::init); - cx_b.update(editor::init); // 2 clients connect to a server. let mut server = TestServer::start(&deterministic).await; @@ -7012,6 +7166,9 @@ async fn test_auto_unfollowing( let active_call_a = cx_a.read(ActiveCall::global); let active_call_b = cx_b.read(ActiveCall::global); + cx_a.update(editor::init); + cx_b.update(editor::init); + // Client A shares a project. client_a .fs @@ -7166,8 +7323,6 @@ async fn test_peers_simultaneously_following_each_other( cx_b: &mut TestAppContext, ) { deterministic.forbid_parking(); - cx_a.update(editor::init); - cx_b.update(editor::init); let mut server = TestServer::start(&deterministic).await; let client_a = server.create_client(cx_a, "user_a").await; @@ -7177,6 +7332,9 @@ async fn test_peers_simultaneously_following_each_other( .await; let active_call_a = cx_a.read(ActiveCall::global); + cx_a.update(editor::init); + cx_b.update(editor::init); + client_a.fs.insert_tree("/a", json!({})).await; let (project_a, _) = client_a.build_local_project("/a", cx_a).await; let workspace_a = client_a.build_workspace(&project_a, cx_a); diff --git a/crates/collab/src/tests/randomized_integration_tests.rs b/crates/collab/src/tests/randomized_integration_tests.rs index c4326be101..3beff6942a 100644 --- a/crates/collab/src/tests/randomized_integration_tests.rs +++ b/crates/collab/src/tests/randomized_integration_tests.rs @@ -8,19 +8,20 @@ use call::ActiveCall; use client::RECEIVE_TIMEOUT; use collections::BTreeMap; use editor::Bias; -use fs::{FakeFs, Fs as _}; +use fs::{repository::GitFileStatus, FakeFs, Fs as _}; use futures::StreamExt as _; use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext}; use language::{range_to_lsp, FakeLspAdapter, Language, LanguageConfig, PointUtf16}; use lsp::FakeLanguageServer; use parking_lot::Mutex; +use pretty_assertions::assert_eq; use project::{search::SearchQuery, Project, ProjectPath}; use rand::{ distributions::{Alphanumeric, DistString}, prelude::*, }; use serde::{Deserialize, Serialize}; -use settings::Settings; +use settings::SettingsStore; use std::{ env, ops::Range, @@ -148,8 +149,9 @@ async fn test_random_collaboration( for (client, mut cx) in clients { cx.update(|cx| { + let store = cx.remove_global::(); cx.clear_globals(); - cx.set_global(Settings::test(cx)); + cx.set_global(store); drop(client); }); } @@ -763,53 +765,85 @@ async fn apply_client_operation( } } - ClientOperation::WriteGitIndex { - repo_path, - contents, - } => { - if !client.fs.directories().contains(&repo_path) { - return Err(TestError::Inapplicable); - } - - log::info!( - "{}: writing git index for repo {:?}: {:?}", - client.username, + ClientOperation::GitOperation { operation } => match operation { + GitOperation::WriteGitIndex { repo_path, - contents - ); + contents, + } => { + if !client.fs.directories().contains(&repo_path) { + return Err(TestError::Inapplicable); + } - let dot_git_dir = repo_path.join(".git"); - let contents = contents - .iter() - .map(|(path, contents)| (path.as_path(), contents.clone())) - .collect::>(); - if client.fs.metadata(&dot_git_dir).await?.is_none() { - client.fs.create_dir(&dot_git_dir).await?; + log::info!( + "{}: writing git index for repo {:?}: {:?}", + client.username, + repo_path, + contents + ); + + let dot_git_dir = repo_path.join(".git"); + let contents = contents + .iter() + .map(|(path, contents)| (path.as_path(), contents.clone())) + .collect::>(); + if client.fs.metadata(&dot_git_dir).await?.is_none() { + client.fs.create_dir(&dot_git_dir).await?; + } + client.fs.set_index_for_repo(&dot_git_dir, &contents).await; } - client.fs.set_index_for_repo(&dot_git_dir, &contents).await; - } - - ClientOperation::WriteGitBranch { - repo_path, - new_branch, - } => { - if !client.fs.directories().contains(&repo_path) { - return Err(TestError::Inapplicable); - } - - log::info!( - "{}: writing git branch for repo {:?}: {:?}", - client.username, + GitOperation::WriteGitBranch { repo_path, - new_branch - ); + new_branch, + } => { + if !client.fs.directories().contains(&repo_path) { + return Err(TestError::Inapplicable); + } - let dot_git_dir = repo_path.join(".git"); - if client.fs.metadata(&dot_git_dir).await?.is_none() { - client.fs.create_dir(&dot_git_dir).await?; + log::info!( + "{}: writing git branch for repo {:?}: {:?}", + client.username, + repo_path, + new_branch + ); + + let dot_git_dir = repo_path.join(".git"); + if client.fs.metadata(&dot_git_dir).await?.is_none() { + client.fs.create_dir(&dot_git_dir).await?; + } + client.fs.set_branch_name(&dot_git_dir, new_branch).await; } - client.fs.set_branch_name(&dot_git_dir, new_branch).await; - } + GitOperation::WriteGitStatuses { + repo_path, + statuses, + } => { + if !client.fs.directories().contains(&repo_path) { + return Err(TestError::Inapplicable); + } + + log::info!( + "{}: writing git statuses for repo {:?}: {:?}", + client.username, + repo_path, + statuses + ); + + let dot_git_dir = repo_path.join(".git"); + + let statuses = statuses + .iter() + .map(|(path, val)| (path.as_path(), val.clone())) + .collect::>(); + + if client.fs.metadata(&dot_git_dir).await?.is_none() { + client.fs.create_dir(&dot_git_dir).await?; + } + + client + .fs + .set_status_for_repo(&dot_git_dir, statuses.as_slice()) + .await; + } + }, } Ok(()) } @@ -1178,6 +1212,13 @@ enum ClientOperation { is_dir: bool, content: String, }, + GitOperation { + operation: GitOperation, + }, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +enum GitOperation { WriteGitIndex { repo_path: PathBuf, contents: Vec<(PathBuf, String)>, @@ -1186,6 +1227,10 @@ enum ClientOperation { repo_path: PathBuf, new_branch: Option, }, + WriteGitStatuses { + repo_path: PathBuf, + statuses: Vec<(PathBuf, GitFileStatus)>, + }, } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -1698,57 +1743,10 @@ impl TestPlan { } } - // Update a git index - 91..=93 => { - let repo_path = client - .fs - .directories() - .into_iter() - .choose(&mut self.rng) - .unwrap() - .clone(); - - let mut file_paths = client - .fs - .files() - .into_iter() - .filter(|path| path.starts_with(&repo_path)) - .collect::>(); - let count = self.rng.gen_range(0..=file_paths.len()); - file_paths.shuffle(&mut self.rng); - file_paths.truncate(count); - - let mut contents = Vec::new(); - for abs_child_file_path in &file_paths { - let child_file_path = abs_child_file_path - .strip_prefix(&repo_path) - .unwrap() - .to_path_buf(); - let new_base = Alphanumeric.sample_string(&mut self.rng, 16); - contents.push((child_file_path, new_base)); - } - - break ClientOperation::WriteGitIndex { - repo_path, - contents, - }; - } - - // Update a git branch - 94..=95 => { - let repo_path = client - .fs - .directories() - .choose(&mut self.rng) - .unwrap() - .clone(); - - let new_branch = (self.rng.gen_range(0..10) > 3) - .then(|| Alphanumeric.sample_string(&mut self.rng, 8)); - - break ClientOperation::WriteGitBranch { - repo_path, - new_branch, + // Update a git related action + 91..=95 => { + break ClientOperation::GitOperation { + operation: self.generate_git_operation(client), }; } @@ -1786,6 +1784,86 @@ impl TestPlan { }) } + fn generate_git_operation(&mut self, client: &TestClient) -> GitOperation { + fn generate_file_paths( + repo_path: &Path, + rng: &mut StdRng, + client: &TestClient, + ) -> Vec { + let mut paths = client + .fs + .files() + .into_iter() + .filter(|path| path.starts_with(repo_path)) + .collect::>(); + + let count = rng.gen_range(0..=paths.len()); + paths.shuffle(rng); + paths.truncate(count); + + paths + .iter() + .map(|path| path.strip_prefix(repo_path).unwrap().to_path_buf()) + .collect::>() + } + + let repo_path = client + .fs + .directories() + .choose(&mut self.rng) + .unwrap() + .clone(); + + match self.rng.gen_range(0..100_u32) { + 0..=25 => { + let file_paths = generate_file_paths(&repo_path, &mut self.rng, client); + + let contents = file_paths + .into_iter() + .map(|path| (path, Alphanumeric.sample_string(&mut self.rng, 16))) + .collect(); + + GitOperation::WriteGitIndex { + repo_path, + contents, + } + } + 26..=63 => { + let new_branch = (self.rng.gen_range(0..10) > 3) + .then(|| Alphanumeric.sample_string(&mut self.rng, 8)); + + GitOperation::WriteGitBranch { + repo_path, + new_branch, + } + } + 64..=100 => { + let file_paths = generate_file_paths(&repo_path, &mut self.rng, client); + + let statuses = file_paths + .into_iter() + .map(|paths| { + ( + paths, + match self.rng.gen_range(0..3_u32) { + 0 => GitFileStatus::Added, + 1 => GitFileStatus::Modified, + 2 => GitFileStatus::Conflict, + _ => unreachable!(), + }, + ) + }) + .collect::>(); + + GitOperation::WriteGitStatuses { + repo_path, + statuses, + } + } + _ => unreachable!(), + } + } + fn next_root_dir_name(&mut self, user_id: UserId) -> String { let user_ix = self .users diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 7374b166ca..eb1755a9ff 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -18,7 +18,6 @@ use gpui::{ ViewContext, ViewHandle, WeakViewHandle, }; use project::Project; -use settings::Settings; use std::{ops::Range, sync::Arc}; use theme::{AvatarStyle, Theme}; use util::ResultExt; @@ -70,7 +69,7 @@ impl View for CollabTitlebarItem { }; let project = self.project.read(cx); - let theme = cx.global::().theme.clone(); + let theme = theme::current(cx).clone(); let mut left_container = Flex::row(); let mut right_container = Flex::row().align_children_center(); @@ -298,7 +297,7 @@ impl CollabTitlebarItem { } pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext) { - let theme = cx.global::().theme.clone(); + let theme = theme::current(cx).clone(); let avatar_style = theme.workspace.titlebar.leader_avatar.clone(); let item_style = theme.context_menu.item.disabled_style().clone(); self.user_menu.update(cx, |user_menu, cx| { @@ -866,7 +865,7 @@ impl CollabTitlebarItem { ) -> Option> { enum ConnectionStatusButton {} - let theme = &cx.global::().theme.clone(); + let theme = &theme::current(cx).clone(); match status { client::Status::ConnectionError | client::Status::ConnectionLost diff --git a/crates/collab_ui/src/contact_finder.rs b/crates/collab_ui/src/contact_finder.rs index 8530867f14..b5f2416a5b 100644 --- a/crates/collab_ui/src/contact_finder.rs +++ b/crates/collab_ui/src/contact_finder.rs @@ -1,7 +1,6 @@ use client::{ContactRequestStatus, User, UserStore}; use gpui::{elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext}; use picker::{Picker, PickerDelegate, PickerEvent}; -use settings::Settings; use std::sync::Arc; use util::TryFutureExt; @@ -98,7 +97,7 @@ impl PickerDelegate for ContactFinderDelegate { selected: bool, cx: &gpui::AppContext, ) -> AnyElement> { - let theme = &cx.global::().theme; + let theme = &theme::current(cx); let user = &self.potential_contacts[ix]; let request_status = self.user_store.read(cx).contact_request_status(user); diff --git a/crates/collab_ui/src/contact_list.rs b/crates/collab_ui/src/contact_list.rs index 452867b8c4..e8dae210c4 100644 --- a/crates/collab_ui/src/contact_list.rs +++ b/crates/collab_ui/src/contact_list.rs @@ -14,7 +14,6 @@ use gpui::{ use menu::{Confirm, SelectNext, SelectPrev}; use project::Project; use serde::Deserialize; -use settings::Settings; use std::{mem, sync::Arc}; use theme::IconButton; use workspace::Workspace; @@ -192,7 +191,7 @@ impl ContactList { .detach(); let list_state = ListState::::new(0, Orientation::Top, 1000., move |this, ix, cx| { - let theme = cx.global::().theme.clone(); + let theme = theme::current(cx).clone(); let is_selected = this.selection == Some(ix); let current_project_id = this.project.read(cx).remote_id(); @@ -1313,7 +1312,7 @@ impl View for ContactList { fn render(&mut self, cx: &mut ViewContext) -> AnyElement { enum AddContact {} - let theme = cx.global::().theme.clone(); + let theme = theme::current(cx).clone(); Flex::column() .with_child( diff --git a/crates/collab_ui/src/contacts_popover.rs b/crates/collab_ui/src/contacts_popover.rs index 35734d81f4..1d6d1c84c7 100644 --- a/crates/collab_ui/src/contacts_popover.rs +++ b/crates/collab_ui/src/contacts_popover.rs @@ -9,7 +9,6 @@ use gpui::{ }; use picker::PickerEvent; use project::Project; -use settings::Settings; use workspace::Workspace; actions!(contacts_popover, [ToggleContactFinder]); @@ -108,7 +107,7 @@ impl View for ContactsPopover { } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let theme = cx.global::().theme.clone(); + let theme = theme::current(cx).clone(); let child = match &self.child { Child::ContactList(child) => ChildView::new(child, cx), Child::ContactFinder(child) => ChildView::new(child, cx), diff --git a/crates/collab_ui/src/incoming_call_notification.rs b/crates/collab_ui/src/incoming_call_notification.rs index d8cf01d20c..12fad467e3 100644 --- a/crates/collab_ui/src/incoming_call_notification.rs +++ b/crates/collab_ui/src/incoming_call_notification.rs @@ -9,7 +9,6 @@ use gpui::{ platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions}, AnyElement, AppContext, Entity, View, ViewContext, }; -use settings::Settings; use util::ResultExt; use workspace::AppState; @@ -26,7 +25,7 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { if let Some(incoming_call) = incoming_call { const PADDING: f32 = 16.; let window_size = cx.read(|cx| { - let theme = &cx.global::().theme.incoming_call_notification; + let theme = &theme::current(cx).incoming_call_notification; vec2f(theme.window_width, theme.window_height) }); @@ -107,7 +106,7 @@ impl IncomingCallNotification { } fn render_caller(&self, cx: &mut ViewContext) -> AnyElement { - let theme = &cx.global::().theme.incoming_call_notification; + let theme = &theme::current(cx).incoming_call_notification; let default_project = proto::ParticipantProject::default(); let initial_project = self .call @@ -171,10 +170,11 @@ impl IncomingCallNotification { enum Accept {} enum Decline {} + let theme = theme::current(cx); Flex::column() .with_child( - MouseEventHandler::::new(0, cx, |_, cx| { - let theme = &cx.global::().theme.incoming_call_notification; + MouseEventHandler::::new(0, cx, |_, _| { + let theme = &theme.incoming_call_notification; Label::new("Accept", theme.accept_button.text.clone()) .aligned() .contained() @@ -187,8 +187,8 @@ impl IncomingCallNotification { .flex(1., true), ) .with_child( - MouseEventHandler::::new(0, cx, |_, cx| { - let theme = &cx.global::().theme.incoming_call_notification; + MouseEventHandler::::new(0, cx, |_, _| { + let theme = &theme.incoming_call_notification; Label::new("Decline", theme.decline_button.text.clone()) .aligned() .contained() @@ -201,12 +201,7 @@ impl IncomingCallNotification { .flex(1., true), ) .constrained() - .with_width( - cx.global::() - .theme - .incoming_call_notification - .button_width, - ) + .with_width(theme.incoming_call_notification.button_width) .into_any() } } @@ -221,12 +216,7 @@ impl View for IncomingCallNotification { } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let background = cx - .global::() - .theme - .incoming_call_notification - .background; - + let background = theme::current(cx).incoming_call_notification.background; Flex::row() .with_child(self.render_caller(cx)) .with_child(self.render_buttons(cx)) diff --git a/crates/collab_ui/src/notifications.rs b/crates/collab_ui/src/notifications.rs index 1dec5a8411..abeb65b1dc 100644 --- a/crates/collab_ui/src/notifications.rs +++ b/crates/collab_ui/src/notifications.rs @@ -4,7 +4,6 @@ use gpui::{ platform::{CursorStyle, MouseButton}, AnyElement, Element, View, ViewContext, }; -use settings::Settings; use std::sync::Arc; enum Dismiss {} @@ -22,7 +21,7 @@ where F: 'static + Fn(&mut V, &mut ViewContext), V: View, { - let theme = cx.global::().theme.clone(); + let theme = theme::current(cx).clone(); let theme = &theme.contact_notification; Flex::column() diff --git a/crates/collab_ui/src/project_shared_notification.rs b/crates/collab_ui/src/project_shared_notification.rs index fac2588e0b..fea6118bdf 100644 --- a/crates/collab_ui/src/project_shared_notification.rs +++ b/crates/collab_ui/src/project_shared_notification.rs @@ -7,7 +7,6 @@ use gpui::{ platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions}, AppContext, Entity, View, ViewContext, }; -use settings::Settings; use std::sync::{Arc, Weak}; use workspace::AppState; @@ -22,7 +21,7 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { worktree_root_names, } => { const PADDING: f32 = 16.; - let theme = &cx.global::().theme.project_shared_notification; + let theme = &theme::current(cx).project_shared_notification; let window_size = vec2f(theme.window_width, theme.window_height); for screen in cx.platform().screens() { @@ -110,7 +109,7 @@ impl ProjectSharedNotification { } fn render_owner(&self, cx: &mut ViewContext) -> AnyElement { - let theme = &cx.global::().theme.project_shared_notification; + let theme = &theme::current(cx).project_shared_notification; Flex::row() .with_children(self.owner.avatar.clone().map(|avatar| { Image::from_data(avatar) @@ -168,10 +167,11 @@ impl ProjectSharedNotification { enum Open {} enum Dismiss {} + let theme = theme::current(cx); Flex::column() .with_child( - MouseEventHandler::::new(0, cx, |_, cx| { - let theme = &cx.global::().theme.project_shared_notification; + MouseEventHandler::::new(0, cx, |_, _| { + let theme = &theme.project_shared_notification; Label::new("Open", theme.open_button.text.clone()) .aligned() .contained() @@ -182,8 +182,8 @@ impl ProjectSharedNotification { .flex(1., true), ) .with_child( - MouseEventHandler::::new(0, cx, |_, cx| { - let theme = &cx.global::().theme.project_shared_notification; + MouseEventHandler::::new(0, cx, |_, _| { + let theme = &theme.project_shared_notification; Label::new("Dismiss", theme.dismiss_button.text.clone()) .aligned() .contained() @@ -196,12 +196,7 @@ impl ProjectSharedNotification { .flex(1., true), ) .constrained() - .with_width( - cx.global::() - .theme - .project_shared_notification - .button_width, - ) + .with_width(theme.project_shared_notification.button_width) .into_any() } } @@ -216,11 +211,7 @@ impl View for ProjectSharedNotification { } fn render(&mut self, cx: &mut ViewContext) -> gpui::AnyElement { - let background = cx - .global::() - .theme - .project_shared_notification - .background; + let background = theme::current(cx).project_shared_notification.background; Flex::row() .with_child(self.render_owner(cx)) .with_child(self.render_buttons(cx)) diff --git a/crates/collab_ui/src/sharing_status_indicator.rs b/crates/collab_ui/src/sharing_status_indicator.rs index 9fbe57af65..3a1dde072f 100644 --- a/crates/collab_ui/src/sharing_status_indicator.rs +++ b/crates/collab_ui/src/sharing_status_indicator.rs @@ -6,7 +6,7 @@ use gpui::{ platform::{Appearance, MouseButton}, AnyElement, AppContext, Element, Entity, View, ViewContext, }; -use settings::Settings; +use workspace::WorkspaceSettings; pub fn init(cx: &mut AppContext) { let active_call = ActiveCall::global(cx); @@ -15,7 +15,9 @@ pub fn init(cx: &mut AppContext) { cx.observe(&active_call, move |call, cx| { if let Some(room) = call.read(cx).room() { if room.read(cx).is_screen_sharing() { - if status_indicator.is_none() && cx.global::().show_call_status_icon { + if status_indicator.is_none() + && settings::get::(cx).show_call_status_icon + { status_indicator = Some(cx.add_status_bar_item(|_| SharingStatusIndicator)); } } else if let Some((window_id, _)) = status_indicator.take() { diff --git a/crates/command_palette/Cargo.toml b/crates/command_palette/Cargo.toml index 8ad1843cb6..95ba452c14 100644 --- a/crates/command_palette/Cargo.toml +++ b/crates/command_palette/Cargo.toml @@ -23,6 +23,7 @@ workspace = { path = "../workspace" } [dev-dependencies] gpui = { path = "../gpui", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] } +language = { path = "../language", features = ["test-support"] } project = { path = "../project", features = ["test-support"] } serde_json.workspace = true workspace = { path = "../workspace", features = ["test-support"] } diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 4e0e776000..2ee93a0734 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -5,7 +5,6 @@ use gpui::{ ViewContext, }; use picker::{Picker, PickerDelegate, PickerEvent}; -use settings::Settings; use std::cmp; use util::ResultExt; use workspace::Workspace; @@ -185,8 +184,7 @@ impl PickerDelegate for CommandPaletteDelegate { ) -> AnyElement> { let mat = &self.matches[ix]; let command = &self.actions[mat.candidate_id]; - let settings = cx.global::(); - let theme = &settings.theme; + let theme = theme::current(cx); let style = theme.picker.item.style_for(mouse_state, selected); let key_style = &theme.command_palette.key.style_for(mouse_state, selected); let keystroke_spacing = theme.command_palette.keystroke_spacing; @@ -294,14 +292,7 @@ mod tests { #[gpui::test] async fn test_command_palette(deterministic: Arc, cx: &mut TestAppContext) { - deterministic.forbid_parking(); - let app_state = cx.update(AppState::test); - - cx.update(|cx| { - editor::init(cx); - workspace::init(app_state.clone(), cx); - init(cx); - }); + let app_state = init_test(cx); let project = Project::test(app_state.fs.clone(), [], cx).await; let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); @@ -369,4 +360,16 @@ mod tests { assert!(palette.delegate().matches.is_empty()) }); } + + fn init_test(cx: &mut TestAppContext) -> Arc { + cx.update(|cx| { + let app_state = AppState::test(cx); + theme::init((), cx); + language::init(cx); + editor::init(cx); + workspace::init(app_state.clone(), cx); + init(cx); + app_state + }) + } } diff --git a/crates/context_menu/src/context_menu.rs b/crates/context_menu/src/context_menu.rs index f0d477e42f..fb455fe1d0 100644 --- a/crates/context_menu/src/context_menu.rs +++ b/crates/context_menu/src/context_menu.rs @@ -8,7 +8,6 @@ use gpui::{ View, ViewContext, }; use menu::*; -use settings::Settings; use std::{any::TypeId, borrow::Cow, sync::Arc, time::Duration}; pub fn init(cx: &mut AppContext) { @@ -323,7 +322,7 @@ impl ContextMenu { } fn render_menu_for_measurement(&self, cx: &mut ViewContext) -> impl Element { - let style = cx.global::().theme.context_menu.clone(); + let style = theme::current(cx).context_menu.clone(); Flex::row() .with_child( Flex::column().with_children(self.items.iter().enumerate().map(|(ix, item)| { @@ -403,7 +402,7 @@ impl ContextMenu { enum Menu {} enum MenuItem {} - let style = cx.global::().theme.context_menu.clone(); + let style = theme::current(cx).context_menu.clone(); MouseEventHandler::::new(0, cx, |_, cx| { Flex::column() diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 9ccd9c445d..de9104a684 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -10,6 +10,7 @@ use gpui::{ actions, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle, }; use language::{ + language_settings::{all_language_settings, language_settings}, point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, Language, PointUtf16, ToPointUtf16, }; @@ -17,7 +18,7 @@ use log::{debug, error}; use lsp::{LanguageServer, LanguageServerId}; use node_runtime::NodeRuntime; use request::{LogMessage, StatusNotification}; -use settings::Settings; +use settings::SettingsStore; use smol::{fs, io::BufReader, stream::StreamExt}; use std::{ ffi::OsString, @@ -258,7 +259,7 @@ impl RegisteredBuffer { #[derive(Debug)] pub struct Completion { - uuid: String, + pub uuid: String, pub range: Range, pub text: String, } @@ -302,56 +303,34 @@ impl Copilot { node_runtime: Arc, cx: &mut ModelContext, ) -> Self { - cx.observe_global::({ - let http = http.clone(); - let node_runtime = node_runtime.clone(); - move |this, cx| { - if cx.global::().features.copilot { - if matches!(this.server, CopilotServer::Disabled) { - let start_task = cx - .spawn({ - let http = http.clone(); - let node_runtime = node_runtime.clone(); - move |this, cx| { - Self::start_language_server(http, node_runtime, this, cx) - } - }) - .shared(); - this.server = CopilotServer::Starting { task: start_task }; - cx.notify(); - } - } else { - this.server = CopilotServer::Disabled; - cx.notify(); - } - } - }) - .detach(); + let mut this = Self { + http, + node_runtime, + server: CopilotServer::Disabled, + buffers: Default::default(), + }; + this.enable_or_disable_copilot(cx); + cx.observe_global::(move |this, cx| this.enable_or_disable_copilot(cx)) + .detach(); + this + } - if cx.global::().features.copilot { - let start_task = cx - .spawn({ - let http = http.clone(); - let node_runtime = node_runtime.clone(); - move |this, cx| async { - Self::start_language_server(http, node_runtime, this, cx).await - } - }) - .shared(); - - Self { - http, - node_runtime, - server: CopilotServer::Starting { task: start_task }, - buffers: Default::default(), + fn enable_or_disable_copilot(&mut self, cx: &mut ModelContext) { + let http = self.http.clone(); + let node_runtime = self.node_runtime.clone(); + if all_language_settings(cx).copilot_enabled(None, None) { + if matches!(self.server, CopilotServer::Disabled) { + let start_task = cx + .spawn({ + move |this, cx| Self::start_language_server(http, node_runtime, this, cx) + }) + .shared(); + self.server = CopilotServer::Starting { task: start_task }; + cx.notify(); } } else { - Self { - http, - node_runtime, - server: CopilotServer::Disabled, - buffers: Default::default(), - } + self.server = CopilotServer::Disabled; + cx.notify(); } } @@ -805,13 +784,13 @@ impl Copilot { let snapshot = registered_buffer.report_changes(buffer, cx); let buffer = buffer.read(cx); let uri = registered_buffer.uri.clone(); - let settings = cx.global::(); let position = position.to_point_utf16(buffer); - let language = buffer.language_at(position); - let language_name = language.map(|language| language.name()); - let language_name = language_name.as_deref(); - let tab_size = settings.tab_size(language_name); - let hard_tabs = settings.hard_tabs(language_name); + let settings = language_settings( + buffer.language_at(position).map(|l| l.name()).as_deref(), + cx, + ); + let tab_size = settings.tab_size; + let hard_tabs = settings.hard_tabs; let relative_path = buffer .file() .map(|file| file.path().to_path_buf()) diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index 18211e4276..0993a33e6c 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -6,7 +6,6 @@ use gpui::{ AnyElement, AnyViewHandle, AppContext, ClipboardItem, Element, Entity, View, ViewContext, ViewHandle, }; -use settings::Settings; use theme::ui::modal; #[derive(PartialEq, Eq, Debug, Clone)] @@ -68,7 +67,7 @@ fn create_copilot_auth_window( cx: &mut AppContext, status: &Status, ) -> ViewHandle { - let window_size = cx.global::().theme.copilot.modal.dimensions(); + let window_size = theme::current(cx).copilot.modal.dimensions(); let window_options = WindowOptions { bounds: WindowBounds::Fixed(RectF::new(Default::default(), window_size)), titlebar: None, @@ -339,7 +338,7 @@ impl View for CopilotCodeVerification { fn render(&mut self, cx: &mut ViewContext) -> AnyElement { enum ConnectModal {} - let style = cx.global::().theme.clone(); + let style = theme::current(cx).clone(); modal::( "Connect Copilot to Zed", diff --git a/crates/copilot_button/Cargo.toml b/crates/copilot_button/Cargo.toml index 2d42b192d9..50fbaa64ee 100644 --- a/crates/copilot_button/Cargo.toml +++ b/crates/copilot_button/Cargo.toml @@ -12,8 +12,10 @@ doctest = false assets = { path = "../assets" } copilot = { path = "../copilot" } editor = { path = "../editor" } +fs = { path = "../fs" } context_menu = { path = "../context_menu" } gpui = { path = "../gpui" } +language = { path = "../language" } settings = { path = "../settings" } theme = { path = "../theme" } util = { path = "../util" } @@ -21,3 +23,6 @@ workspace = { path = "../workspace" } anyhow.workspace = true smol.workspace = true futures.workspace = true + +[dev-dependencies] +editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/copilot_button/src/copilot_button.rs b/crates/copilot_button/src/copilot_button.rs index 884d1358f7..17d27ca41f 100644 --- a/crates/copilot_button/src/copilot_button.rs +++ b/crates/copilot_button/src/copilot_button.rs @@ -2,13 +2,15 @@ use anyhow::Result; use context_menu::{ContextMenu, ContextMenuItem}; use copilot::{Copilot, SignOut, Status}; use editor::{scroll::autoscroll::Autoscroll, Editor}; +use fs::Fs; use gpui::{ elements::*, platform::{CursorStyle, MouseButton}, AnyElement, AppContext, AsyncAppContext, Element, Entity, MouseState, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, }; -use settings::{settings_file::SettingsFile, Settings}; +use language::language_settings::{self, all_language_settings, AllLanguageSettings}; +use settings::{update_settings_file, SettingsStore}; use std::{path::Path, sync::Arc}; use util::{paths, ResultExt}; use workspace::{ @@ -26,6 +28,7 @@ pub struct CopilotButton { editor_enabled: Option, language: Option>, path: Option>, + fs: Arc, } impl Entity for CopilotButton { @@ -38,13 +41,12 @@ impl View for CopilotButton { } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let settings = cx.global::(); - - if !settings.features.copilot { + let all_language_settings = &all_language_settings(cx); + if !all_language_settings.copilot.feature_enabled { return Empty::new().into_any(); } - let theme = settings.theme.clone(); + let theme = theme::current(cx).clone(); let active = self.popup_menu.read(cx).visible(); let Some(copilot) = Copilot::global(cx) else { return Empty::new().into_any(); @@ -53,7 +55,7 @@ impl View for CopilotButton { let enabled = self .editor_enabled - .unwrap_or(settings.show_copilot_suggestions(None, None)); + .unwrap_or_else(|| all_language_settings.copilot_enabled(None, None)); Stack::new() .with_child( @@ -143,7 +145,7 @@ impl View for CopilotButton { } impl CopilotButton { - pub fn new(cx: &mut ViewContext) -> Self { + pub fn new(fs: Arc, cx: &mut ViewContext) -> Self { let button_view_id = cx.view_id(); let menu = cx.add_view(|cx| { let mut menu = ContextMenu::new(button_view_id, cx); @@ -155,7 +157,7 @@ impl CopilotButton { Copilot::global(cx).map(|copilot| cx.observe(&copilot, |_, _, cx| cx.notify()).detach()); - cx.observe_global::(move |_, cx| cx.notify()) + cx.observe_global::(move |_, cx| cx.notify()) .detach(); Self { @@ -164,17 +166,19 @@ impl CopilotButton { editor_enabled: None, language: None, path: None, + fs, } } pub fn deploy_copilot_start_menu(&mut self, cx: &mut ViewContext) { let mut menu_options = Vec::with_capacity(2); + let fs = self.fs.clone(); menu_options.push(ContextMenuItem::handler("Sign In", |cx| { initiate_sign_in(cx) })); - menu_options.push(ContextMenuItem::handler("Disable Copilot", |cx| { - hide_copilot(cx) + menu_options.push(ContextMenuItem::handler("Disable Copilot", move |cx| { + hide_copilot(fs.clone(), cx) })); self.popup_menu.update(cx, |menu, cx| { @@ -188,22 +192,26 @@ impl CopilotButton { } pub fn deploy_copilot_menu(&mut self, cx: &mut ViewContext) { - let settings = cx.global::(); - + let fs = self.fs.clone(); let mut menu_options = Vec::with_capacity(8); if let Some(language) = self.language.clone() { - let language_enabled = settings.copilot_enabled_for_language(Some(language.as_ref())); + let fs = fs.clone(); + let language_enabled = + language_settings::language_settings(Some(language.as_ref()), cx) + .show_copilot_suggestions; menu_options.push(ContextMenuItem::handler( format!( "{} Suggestions for {}", if language_enabled { "Hide" } else { "Show" }, language ), - move |cx| toggle_copilot_for_language(language.clone(), cx), + move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx), )); } + let settings = settings::get::(cx); + if let Some(path) = self.path.as_ref() { let path_enabled = settings.copilot_enabled_for_path(path); let path = path.clone(); @@ -228,19 +236,19 @@ impl CopilotButton { )); } - let globally_enabled = cx.global::().features.copilot; + let globally_enabled = settings.copilot_enabled(None, None); menu_options.push(ContextMenuItem::handler( if globally_enabled { "Hide Suggestions for All Files" } else { "Show Suggestions for All Files" }, - |cx| toggle_copilot_globally(cx), + move |cx| toggle_copilot_globally(fs.clone(), cx), )); menu_options.push(ContextMenuItem::Separator); - let icon_style = settings.theme.copilot.out_link_icon.clone(); + let icon_style = theme::current(cx).copilot.out_link_icon.clone(); menu_options.push(ContextMenuItem::action( move |state: &mut MouseState, style: &theme::ContextMenuItem| { Flex::row() @@ -266,22 +274,19 @@ impl CopilotButton { pub fn update_enabled(&mut self, editor: ViewHandle, cx: &mut ViewContext) { let editor = editor.read(cx); - let snapshot = editor.buffer().read(cx).snapshot(cx); - let settings = cx.global::(); let suggestion_anchor = editor.selections.newest_anchor().start; - let language_name = snapshot .language_at(suggestion_anchor) .map(|language| language.name()); - let path = snapshot - .file_at(suggestion_anchor) - .map(|file| file.path().clone()); + let path = snapshot.file_at(suggestion_anchor).map(|file| file.path()); - self.editor_enabled = - Some(settings.show_copilot_suggestions(language_name.as_deref(), path.as_deref())); + self.editor_enabled = Some( + all_language_settings(cx) + .copilot_enabled(language_name.as_deref(), path.map(|p| p.as_ref())), + ); self.language = language_name; - self.path = path; + self.path = path.cloned(); cx.notify() } @@ -310,7 +315,7 @@ async fn configure_disabled_globs( let settings_editor = workspace .update(&mut cx, |_, cx| { create_and_open_local_file(&paths::SETTINGS, cx, || { - Settings::initial_user_settings_content(&assets::Assets) + settings::initial_user_settings_content(&assets::Assets) .as_ref() .into() }) @@ -322,16 +327,17 @@ async fn configure_disabled_globs( settings_editor.downgrade().update(&mut cx, |item, cx| { let text = item.buffer().read(cx).snapshot(cx).text(); - let edits = SettingsFile::update_unsaved(&text, cx, |file| { + let settings = cx.global::(); + let edits = settings.edits_for_update::(&text, |file| { let copilot = file.copilot.get_or_insert_with(Default::default); let globs = copilot.disabled_globs.get_or_insert_with(|| { - cx.global::() + settings + .get::(None) .copilot .disabled_globs - .clone() .iter() - .map(|glob| glob.as_str().to_string()) - .collect::>() + .map(|glob| glob.glob().to_string()) + .collect() }); if let Some(path_to_disable) = &path_to_disable { @@ -356,32 +362,26 @@ async fn configure_disabled_globs( anyhow::Ok(()) } -fn toggle_copilot_globally(cx: &mut AppContext) { - let show_copilot_suggestions = cx.global::().show_copilot_suggestions(None, None); - SettingsFile::update(cx, move |file_contents| { - file_contents.editor.show_copilot_suggestions = Some((!show_copilot_suggestions).into()) +fn toggle_copilot_globally(fs: Arc, cx: &mut AppContext) { + let show_copilot_suggestions = all_language_settings(cx).copilot_enabled(None, None); + update_settings_file::(fs, cx, move |file| { + file.defaults.show_copilot_suggestions = Some((!show_copilot_suggestions).into()) }); } -fn toggle_copilot_for_language(language: Arc, cx: &mut AppContext) { - let show_copilot_suggestions = cx - .global::() - .show_copilot_suggestions(Some(&language), None); - - SettingsFile::update(cx, move |file_contents| { - file_contents.languages.insert( - language, - settings::EditorSettings { - show_copilot_suggestions: Some((!show_copilot_suggestions).into()), - ..Default::default() - }, - ); +fn toggle_copilot_for_language(language: Arc, fs: Arc, cx: &mut AppContext) { + let show_copilot_suggestions = all_language_settings(cx).copilot_enabled(Some(&language), None); + update_settings_file::(fs, cx, move |file| { + file.languages + .entry(language) + .or_default() + .show_copilot_suggestions = Some(!show_copilot_suggestions); }); } -fn hide_copilot(cx: &mut AppContext) { - SettingsFile::update(cx, move |file_contents| { - file_contents.features.copilot = Some(false) +fn hide_copilot(fs: Arc, cx: &mut AppContext) { + update_settings_file::(fs, cx, move |file| { + file.features.get_or_insert(Default::default()).copilot = Some(false); }); } diff --git a/crates/diagnostics/Cargo.toml b/crates/diagnostics/Cargo.toml index 9d455f520e..4e898cca0a 100644 --- a/crates/diagnostics/Cargo.toml +++ b/crates/diagnostics/Cargo.toml @@ -31,6 +31,7 @@ language = { path = "../language", features = ["test-support"] } lsp = { path = "../lsp", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] } +theme = { path = "../theme", features = ["test-support"] } serde_json.workspace = true unindent.workspace = true diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index d82c653a09..a202a6082c 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -20,7 +20,6 @@ use language::{ use lsp::LanguageServerId; use project::{DiagnosticSummary, Project, ProjectPath}; use serde_json::json; -use settings::Settings; use smallvec::SmallVec; use std::{ any::{Any, TypeId}, @@ -30,6 +29,7 @@ use std::{ path::PathBuf, sync::Arc, }; +use theme::ThemeSettings; use util::TryFutureExt; use workspace::{ item::{BreadcrumbText, Item, ItemEvent, ItemHandle}, @@ -89,7 +89,7 @@ impl View for ProjectDiagnosticsEditor { fn render(&mut self, cx: &mut ViewContext) -> AnyElement { if self.path_states.is_empty() { - let theme = &cx.global::().theme.project_diagnostics; + let theme = &theme::current(cx).project_diagnostics; Label::new("No problems in workspace", theme.empty_message.clone()) .aligned() .contained() @@ -537,7 +537,7 @@ impl Item for ProjectDiagnosticsEditor { render_summary( &self.summary, &style.label.text, - &cx.global::().theme.project_diagnostics, + &theme::current(cx).project_diagnostics, ) } @@ -679,10 +679,10 @@ impl Item for ProjectDiagnosticsEditor { fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock { let (message, highlights) = highlight_diagnostic_message(Vec::new(), &diagnostic.message); Arc::new(move |cx| { - let settings = cx.global::(); + let settings = settings::get::(cx); let theme = &settings.theme.editor; let style = theme.diagnostic_header.clone(); - let font_size = (style.text_scale_factor * settings.buffer_font_size).round(); + let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round(); let icon_width = cx.em_width * style.icon_width_factor; let icon = if diagnostic.severity == DiagnosticSeverity::ERROR { Svg::new("icons/circle_x_mark_12.svg") @@ -818,33 +818,35 @@ mod tests { use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped}; use project::FakeFs; use serde_json::json; + use settings::SettingsStore; use unindent::Unindent as _; #[gpui::test] async fn test_diagnostics(cx: &mut TestAppContext) { - Settings::test_async(cx); + init_test(cx); + let fs = FakeFs::new(cx.background()); fs.insert_tree( "/test", json!({ "consts.rs": " - const a: i32 = 'a'; - const b: i32 = c; - " + const a: i32 = 'a'; + const b: i32 = c; + " .unindent(), "main.rs": " - fn main() { - let x = vec![]; - let y = vec![]; - a(x); - b(y); - // comment 1 - // comment 2 - c(y); - d(x); - } - " + fn main() { + let x = vec![]; + let y = vec![]; + a(x); + b(y); + // comment 1 + // comment 2 + c(y); + d(x); + } + " .unindent(), }), ) @@ -1225,7 +1227,8 @@ mod tests { #[gpui::test] async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) { - Settings::test_async(cx); + init_test(cx); + let fs = FakeFs::new(cx.background()); fs.insert_tree( "/test", @@ -1489,6 +1492,16 @@ mod tests { }); } + fn init_test(cx: &mut TestAppContext) { + cx.update(|cx| { + cx.set_global(SettingsStore::test(cx)); + theme::init((), cx); + language::init(cx); + client::init_settings(cx); + workspace::init_settings(cx); + }); + } + fn editor_blocks(editor: &ViewHandle, cx: &mut WindowContext) -> Vec<(u32, String)> { editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index f0ceacc619..f84846eae1 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -7,7 +7,6 @@ use gpui::{ }; use language::Diagnostic; use lsp::LanguageServerId; -use settings::Settings; use workspace::{item::ItemHandle, StatusItemView, Workspace}; use crate::ProjectDiagnosticsEditor; @@ -92,13 +91,12 @@ impl View for DiagnosticIndicator { enum Summary {} enum Message {} - let tooltip_style = cx.global::().theme.tooltip.clone(); + let tooltip_style = theme::current(cx).tooltip.clone(); let in_progress = !self.in_progress_checks.is_empty(); let mut element = Flex::row().with_child( MouseEventHandler::::new(0, cx, |state, cx| { - let style = cx - .global::() - .theme + let theme = theme::current(cx); + let style = theme .workspace .status_bar .diagnostic_summary @@ -184,7 +182,7 @@ impl View for DiagnosticIndicator { .into_any(), ); - let style = &cx.global::().theme.workspace.status_bar; + let style = &theme::current(cx).workspace.status_bar; let item_spacing = style.item_spacing; if in_progress { diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index e4767a12e2..325883b7c0 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -58,6 +58,7 @@ parking_lot.workspace = true postage.workspace = true pulldown-cmark = { version = "0.9.2", default-features = false } rand = { workspace = true, optional = true } +schemars.workspace = true serde.workspace = true serde_derive.workspace = true smallvec.workspace = true @@ -80,7 +81,6 @@ workspace = { path = "../workspace", features = ["test-support"] } ctor.workspace = true env_logger.workspace = true -glob.workspace = true rand.workspace = true unindent.workspace = true tree-sitter = "0.20" diff --git a/crates/editor/src/blink_manager.rs b/crates/editor/src/blink_manager.rs index 409b6f9b03..24ea4774aa 100644 --- a/crates/editor/src/blink_manager.rs +++ b/crates/editor/src/blink_manager.rs @@ -1,8 +1,8 @@ -use std::time::Duration; - +use crate::EditorSettings; use gpui::{Entity, ModelContext}; -use settings::Settings; +use settings::SettingsStore; use smol::Timer; +use std::time::Duration; pub struct BlinkManager { blink_interval: Duration, @@ -15,8 +15,8 @@ pub struct BlinkManager { impl BlinkManager { pub fn new(blink_interval: Duration, cx: &mut ModelContext) -> Self { - cx.observe_global::(move |this, cx| { - // Make sure we blink the cursors if the setting is re-enabled + // Make sure we blink the cursors if the setting is re-enabled + cx.observe_global::(move |this, cx| { this.blink_cursors(this.blink_epoch, cx) }) .detach(); @@ -64,7 +64,7 @@ impl BlinkManager { } fn blink_cursors(&mut self, epoch: usize, cx: &mut ModelContext) { - if cx.global::().cursor_blink { + if settings::get::(cx).cursor_blink { if epoch == self.blink_epoch && self.enabled && !self.blinking_paused { self.visible = !self.visible; cx.notify(); diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index e190ec7717..366e47ddc6 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -13,8 +13,9 @@ use gpui::{ fonts::{FontId, HighlightStyle}, Entity, ModelContext, ModelHandle, }; -use language::{OffsetUtf16, Point, Subscription as BufferSubscription}; -use settings::Settings; +use language::{ + language_settings::language_settings, OffsetUtf16, Point, Subscription as BufferSubscription, +}; use std::{any::TypeId, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc}; pub use suggestion_map::Suggestion; use suggestion_map::SuggestionMap; @@ -276,8 +277,7 @@ impl DisplayMap { .as_singleton() .and_then(|buffer| buffer.read(cx).language()) .map(|language| language.name()); - - cx.global::().tab_size(language_name.as_deref()) + language_settings(language_name.as_deref(), cx).tab_size } #[cfg(test)] @@ -844,8 +844,12 @@ pub mod tests { use super::*; use crate::{movement, test::marked_display_snapshot}; use gpui::{color::Color, elements::*, test::observe, AppContext}; - use language::{Buffer, Language, LanguageConfig, SelectionGoal}; + use language::{ + language_settings::{AllLanguageSettings, AllLanguageSettingsContent}, + Buffer, Language, LanguageConfig, SelectionGoal, + }; use rand::{prelude::*, Rng}; + use settings::SettingsStore; use smol::stream::StreamExt; use std::{env, sync::Arc}; use theme::SyntaxTheme; @@ -882,9 +886,7 @@ pub mod tests { log::info!("wrap width: {:?}", wrap_width); cx.update(|cx| { - let mut settings = Settings::test(cx); - settings.editor_overrides.tab_size = NonZeroU32::new(tab_size); - cx.set_global(settings) + init_test(cx, |s| s.defaults.tab_size = NonZeroU32::new(tab_size)); }); let buffer = cx.update(|cx| { @@ -939,9 +941,11 @@ pub mod tests { tab_size = *tab_sizes.choose(&mut rng).unwrap(); log::info!("setting tab size to {:?}", tab_size); cx.update(|cx| { - let mut settings = Settings::test(cx); - settings.editor_overrides.tab_size = NonZeroU32::new(tab_size); - cx.set_global(settings) + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, |s| { + s.defaults.tab_size = NonZeroU32::new(tab_size); + }); + }); }); } 30..=44 => { @@ -1119,7 +1123,7 @@ pub mod tests { #[gpui::test(retries = 5)] fn test_soft_wraps(cx: &mut AppContext) { cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX); - cx.foreground().forbid_parking(); + init_test(cx, |_| {}); let font_cache = cx.font_cache(); @@ -1131,7 +1135,6 @@ pub mod tests { .unwrap(); let font_size = 12.0; let wrap_width = Some(64.); - cx.set_global(Settings::test(cx)); let text = "one two three four five\nsix seven eight"; let buffer = MultiBuffer::build_simple(text, cx); @@ -1211,7 +1214,8 @@ pub mod tests { #[gpui::test] fn test_text_chunks(cx: &mut gpui::AppContext) { - cx.set_global(Settings::test(cx)); + init_test(cx, |_| {}); + let text = sample_text(6, 6, 'a'); let buffer = MultiBuffer::build_simple(&text, cx); let family_id = cx @@ -1225,6 +1229,7 @@ pub mod tests { let font_size = 14.0; let map = cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx)); + buffer.update(cx, |buffer, cx| { buffer.edit( vec![ @@ -1289,11 +1294,8 @@ pub mod tests { .unwrap(), ); language.set_theme(&theme); - cx.update(|cx| { - let mut settings = Settings::test(cx); - settings.editor_defaults.tab_size = Some(2.try_into().unwrap()); - cx.set_global(settings); - }); + + 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)); buffer.condition(cx, |buf, _| !buf.is_parsing()).await; @@ -1382,7 +1384,7 @@ pub mod tests { ); language.set_theme(&theme); - cx.update(|cx| cx.set_global(Settings::test(cx))); + cx.update(|cx| init_test(cx, |_| {})); let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); buffer.condition(cx, |buf, _| !buf.is_parsing()).await; @@ -1429,9 +1431,8 @@ pub mod tests { #[gpui::test] async fn test_chunks_with_text_highlights(cx: &mut gpui::TestAppContext) { - cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX); + cx.update(|cx| init_test(cx, |_| {})); - cx.update(|cx| cx.set_global(Settings::test(cx))); let theme = SyntaxTheme::new(vec![ ("operator".to_string(), Color::red().into()), ("string".to_string(), Color::green().into()), @@ -1510,7 +1511,8 @@ pub mod tests { #[gpui::test] fn test_clip_point(cx: &mut gpui::AppContext) { - cx.set_global(Settings::test(cx)); + init_test(cx, |_| {}); + fn assert(text: &str, shift_right: bool, bias: Bias, cx: &mut gpui::AppContext) { let (unmarked_snapshot, mut markers) = marked_display_snapshot(text, cx); @@ -1559,7 +1561,7 @@ pub mod tests { #[gpui::test] fn test_clip_at_line_ends(cx: &mut gpui::AppContext) { - cx.set_global(Settings::test(cx)); + init_test(cx, |_| {}); fn assert(text: &str, cx: &mut gpui::AppContext) { let (mut unmarked_snapshot, markers) = marked_display_snapshot(text, cx); @@ -1578,7 +1580,8 @@ pub mod tests { #[gpui::test] fn test_tabs_with_multibyte_chars(cx: &mut gpui::AppContext) { - cx.set_global(Settings::test(cx)); + init_test(cx, |_| {}); + let text = "✅\t\tα\nβ\t\n🏀β\t\tγ"; let buffer = MultiBuffer::build_simple(text, cx); let font_cache = cx.font_cache(); @@ -1639,7 +1642,8 @@ pub mod tests { #[gpui::test] fn test_max_point(cx: &mut gpui::AppContext) { - cx.set_global(Settings::test(cx)); + init_test(cx, |_| {}); + let buffer = MultiBuffer::build_simple("aaa\n\t\tbbb", cx); let font_cache = cx.font_cache(); let family_id = font_cache @@ -1718,4 +1722,13 @@ pub mod tests { } chunks } + + fn init_test(cx: &mut AppContext, f: impl Fn(&mut AllLanguageSettingsContent)) { + cx.foreground().forbid_parking(); + cx.set_global(SettingsStore::test(cx)); + language::init(cx); + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, f); + }); + } } diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 93e43f876c..05ff9886f1 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -993,7 +993,7 @@ mod tests { use crate::multi_buffer::MultiBuffer; use gpui::{elements::Empty, Element}; use rand::prelude::*; - use settings::Settings; + use settings::SettingsStore; use std::env; use util::RandomCharIter; @@ -1013,7 +1013,7 @@ mod tests { #[gpui::test] fn test_basic_blocks(cx: &mut gpui::AppContext) { - cx.set_global(Settings::test(cx)); + init_test(cx); let family_id = cx .font_cache() @@ -1189,7 +1189,7 @@ mod tests { #[gpui::test] fn test_blocks_on_wrapped_lines(cx: &mut gpui::AppContext) { - cx.set_global(Settings::test(cx)); + init_test(cx); let family_id = cx .font_cache() @@ -1239,7 +1239,7 @@ mod tests { #[gpui::test(iterations = 100)] fn test_random_blocks(cx: &mut gpui::AppContext, mut rng: StdRng) { - cx.set_global(Settings::test(cx)); + init_test(cx); let operations = env::var("OPERATIONS") .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) @@ -1647,6 +1647,11 @@ mod tests { } } + fn init_test(cx: &mut gpui::AppContext) { + cx.set_global(SettingsStore::test(cx)); + theme::init((), cx); + } + impl TransformBlock { fn as_custom(&self) -> Option<&Block> { match self { diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index bd3cd1a620..6ef1ebce1d 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -1204,7 +1204,7 @@ mod tests { use crate::{MultiBuffer, ToPoint}; use collections::HashSet; use rand::prelude::*; - use settings::Settings; + use settings::SettingsStore; use std::{cmp::Reverse, env, mem, sync::Arc}; use sum_tree::TreeMap; use util::test::sample_text; @@ -1213,7 +1213,7 @@ mod tests { #[gpui::test] fn test_basic_folds(cx: &mut gpui::AppContext) { - cx.set_global(Settings::test(cx)); + init_test(cx); let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx); let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); let buffer_snapshot = buffer.read(cx).snapshot(cx); @@ -1286,7 +1286,7 @@ mod tests { #[gpui::test] fn test_adjacent_folds(cx: &mut gpui::AppContext) { - cx.set_global(Settings::test(cx)); + init_test(cx); let buffer = MultiBuffer::build_simple("abcdefghijkl", cx); let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); let buffer_snapshot = buffer.read(cx).snapshot(cx); @@ -1349,7 +1349,7 @@ mod tests { #[gpui::test] fn test_merging_folds_via_edit(cx: &mut gpui::AppContext) { - cx.set_global(Settings::test(cx)); + init_test(cx); let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx); let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); let buffer_snapshot = buffer.read(cx).snapshot(cx); @@ -1400,7 +1400,7 @@ mod tests { #[gpui::test(iterations = 100)] fn test_random_folds(cx: &mut gpui::AppContext, mut rng: StdRng) { - cx.set_global(Settings::test(cx)); + init_test(cx); let operations = env::var("OPERATIONS") .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) .unwrap_or(10); @@ -1676,6 +1676,10 @@ mod tests { assert_eq!(snapshot.buffer_rows(3).collect::>(), [Some(6)]); } + fn init_test(cx: &mut gpui::AppContext) { + cx.set_global(SettingsStore::test(cx)); + } + impl FoldMap { fn merged_fold_ranges(&self) -> Vec> { let buffer = self.buffer.lock().clone(); diff --git a/crates/editor/src/display_map/suggestion_map.rs b/crates/editor/src/display_map/suggestion_map.rs index f48efc76f4..eac903d0af 100644 --- a/crates/editor/src/display_map/suggestion_map.rs +++ b/crates/editor/src/display_map/suggestion_map.rs @@ -578,7 +578,7 @@ mod tests { use crate::{display_map::fold_map::FoldMap, MultiBuffer}; use gpui::AppContext; use rand::{prelude::StdRng, Rng}; - use settings::Settings; + use settings::SettingsStore; use std::{ env, ops::{Bound, RangeBounds}, @@ -631,7 +631,8 @@ mod tests { #[gpui::test(iterations = 100)] fn test_random_suggestions(cx: &mut AppContext, mut rng: StdRng) { - cx.set_global(Settings::test(cx)); + init_test(cx); + let operations = env::var("OPERATIONS") .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) .unwrap_or(10); @@ -834,6 +835,11 @@ mod tests { } } + fn init_test(cx: &mut AppContext) { + cx.set_global(SettingsStore::test(cx)); + theme::init((), cx); + } + impl SuggestionMap { pub fn randomly_mutate( &self, diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index e1de4fce77..478eaf4c7e 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -1043,16 +1043,16 @@ mod tests { }; use gpui::test::observe; use rand::prelude::*; - use settings::Settings; + use settings::SettingsStore; use smol::stream::StreamExt; use std::{cmp, env, num::NonZeroU32}; use text::Rope; #[gpui::test(iterations = 100)] async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx); + cx.foreground().set_block_on_ticks(0..=50); - cx.foreground().forbid_parking(); let operations = env::var("OPERATIONS") .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) .unwrap_or(10); @@ -1287,6 +1287,14 @@ mod tests { wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty())); } + fn init_test(cx: &mut gpui::TestAppContext) { + cx.foreground().forbid_parking(); + cx.update(|cx| { + cx.set_global(SettingsStore::test(cx)); + theme::init((), cx); + }); + } + fn wrap_text( unwrapped_text: &str, wrap_width: Option, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2bb9869e6d..7207e3c91c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1,5 +1,6 @@ mod blink_manager; pub mod display_map; +mod editor_settings; mod element; mod git; @@ -19,15 +20,17 @@ mod editor_tests; #[cfg(any(test, feature = "test-support"))] pub mod test; +use ::git::diff::DiffHunk; use aho_corasick::AhoCorasick; use anyhow::{anyhow, Result}; use blink_manager::BlinkManager; -use client::ClickhouseEvent; +use client::{ClickhouseEvent, TelemetrySettings}; use clock::ReplicaId; use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque}; use copilot::Copilot; pub use display_map::DisplayPoint; use display_map::*; +pub use editor_settings::EditorSettings; pub use element::*; use futures::FutureExt; use fuzzy::{StringMatch, StringMatchCandidate}; @@ -51,6 +54,7 @@ pub use items::MAX_TAB_TITLE_LEN; use itertools::Itertools; pub use language::{char_kind, CharKind}; use language::{ + language_settings::{self, all_language_settings}, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape, Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Point, Selection, SelectionGoal, TransactionId, @@ -70,7 +74,7 @@ use scroll::{ }; use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection}; use serde::{Deserialize, Serialize}; -use settings::Settings; +use settings::SettingsStore; use smallvec::SmallVec; use snippet::Snippet; use std::{ @@ -85,7 +89,7 @@ use std::{ time::{Duration, Instant}, }; pub use sum_tree::Bias; -use theme::{DiagnosticStyle, Theme}; +use theme::{DiagnosticStyle, Theme, ThemeSettings}; use util::{post_inc, RangeExt, ResultExt, TryFutureExt}; use workspace::{ItemNavHistory, ViewId, Workspace}; @@ -286,7 +290,12 @@ pub enum Direction { Next, } +pub fn init_settings(cx: &mut AppContext) { + settings::register::(cx); +} + pub fn init(cx: &mut AppContext) { + init_settings(cx); cx.add_action(Editor::new_file); cx.add_action(Editor::cancel); cx.add_action(Editor::newline); @@ -436,7 +445,7 @@ pub enum EditorMode { Full, } -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum SoftWrap { None, EditorWidth, @@ -471,7 +480,7 @@ pub struct Editor { select_larger_syntax_node_stack: Vec]>>, ime_transaction: Option, active_diagnostics: Option, - soft_wrap_mode_override: Option, + soft_wrap_mode_override: Option, get_field_editor_theme: Option>, override_text_style: Option>, project: Option>, @@ -516,6 +525,15 @@ pub struct EditorSnapshot { ongoing_scroll: OngoingScroll, } +impl EditorSnapshot { + fn has_scrollbar_info(&self) -> bool { + self.buffer_snapshot + .git_diff_hunks_in_range(0..self.max_point().row()) + .next() + .is_some() + } +} + #[derive(Clone, Debug)] struct SelectionHistoryEntry { selections: Arc<[Selection]>, @@ -1229,8 +1247,8 @@ impl Editor { ) -> Self { let editor_view_id = cx.view_id(); let display_map = cx.add_model(|cx| { - let settings = cx.global::(); - let style = build_style(&*settings, get_field_editor_theme.as_deref(), None, cx); + let settings = settings::get::(cx); + let style = build_style(settings, get_field_editor_theme.as_deref(), None, cx); DisplayMap::new( buffer.clone(), style.text.font_id, @@ -1247,7 +1265,17 @@ impl Editor { let blink_manager = cx.add_model(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx)); let soft_wrap_mode_override = - (mode == EditorMode::SingleLine).then(|| settings::SoftWrap::None); + (mode == EditorMode::SingleLine).then(|| language_settings::SoftWrap::None); + + let mut project_subscription = None; + if mode == EditorMode::Full && buffer.read(cx).is_singleton() { + if let Some(project) = project.as_ref() { + project_subscription = Some(cx.observe(project, |_, _, cx| { + cx.emit(Event::TitleChanged); + })) + } + } + let mut this = Self { handle: cx.weak_handle(), buffer: buffer.clone(), @@ -1301,9 +1329,14 @@ impl Editor { cx.subscribe(&buffer, Self::on_buffer_event), cx.observe(&display_map, Self::on_display_map_changed), cx.observe(&blink_manager, |_, _, cx| cx.notify()), - cx.observe_global::(Self::settings_changed), + cx.observe_global::(Self::settings_changed), ], }; + + if let Some(project_subscription) = project_subscription { + this._subscriptions.push(project_subscription); + } + this.end_selection(cx); this.scroll_manager.show_scrollbar(cx); @@ -1315,7 +1348,7 @@ impl Editor { cx.set_global(ScrollbarAutoHide(should_auto_hide_scrollbars)); } - this.report_editor_event("open", cx); + this.report_editor_event("open", None, cx); this } @@ -1395,7 +1428,7 @@ impl Editor { fn style(&self, cx: &AppContext) -> EditorStyle { build_style( - cx.global::(), + settings::get::(cx), self.get_field_editor_theme.as_deref(), self.override_text_style.as_deref(), cx, @@ -2353,7 +2386,7 @@ impl Editor { } fn trigger_completion_on_input(&mut self, text: &str, cx: &mut ViewContext) { - if !cx.global::().show_completions_on_input { + if !settings::get::(cx).show_completions_on_input { return; } @@ -3082,6 +3115,8 @@ impl Editor { copilot .update(cx, |copilot, cx| copilot.accept_completion(completion, cx)) .detach_and_log_err(cx); + + self.report_copilot_event(Some(completion.uuid.clone()), true, cx) } self.insert_with_autoindent_mode(&suggestion.text.to_string(), None, cx); cx.notify(); @@ -3099,6 +3134,8 @@ impl Editor { copilot.discard_completions(&self.copilot_state.completions, cx) }) .detach_and_log_err(cx); + + self.report_copilot_event(None, false, cx) } self.display_map @@ -3116,17 +3153,12 @@ impl Editor { snapshot: &MultiBufferSnapshot, cx: &mut ViewContext, ) -> bool { - let settings = cx.global::(); - - let path = snapshot.file_at(location).map(|file| file.path()); + let path = snapshot.file_at(location).map(|file| file.path().as_ref()); let language_name = snapshot .language_at(location) .map(|language| language.name()); - if !settings.show_copilot_suggestions(language_name.as_deref(), path.map(|p| p.as_ref())) { - return false; - } - - true + let settings = all_language_settings(cx); + settings.copilot_enabled(language_name.as_deref(), path) } fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool { @@ -3427,12 +3459,9 @@ impl Editor { { let indent_size = buffer.indent_size_for_line(line_buffer_range.start.row); - let language_name = buffer - .language_at(line_buffer_range.start) - .map(|language| language.name()); let indent_len = match indent_size.kind { IndentKind::Space => { - cx.global::().tab_size(language_name.as_deref()) + buffer.settings_at(line_buffer_range.start, cx).tab_size } IndentKind::Tab => NonZeroU32::new(1).unwrap(), }; @@ -3544,12 +3573,11 @@ impl Editor { } // Otherwise, insert a hard or soft tab. - let settings = cx.global::(); - let language_name = buffer.language_at(cursor, cx).map(|l| l.name()); - let tab_size = if settings.hard_tabs(language_name.as_deref()) { + let settings = buffer.settings_at(cursor, cx); + let tab_size = if settings.hard_tabs { IndentSize::tab() } else { - let tab_size = settings.tab_size(language_name.as_deref()).get(); + let tab_size = settings.tab_size.get(); let char_column = snapshot .text_for_range(Point::new(cursor.row, 0)..cursor) .flat_map(str::chars) @@ -3602,10 +3630,9 @@ impl Editor { delta_for_start_row: u32, cx: &AppContext, ) -> u32 { - let language_name = buffer.language_at(selection.start, cx).map(|l| l.name()); - let settings = cx.global::(); - let tab_size = settings.tab_size(language_name.as_deref()).get(); - let indent_kind = if settings.hard_tabs(language_name.as_deref()) { + let settings = buffer.settings_at(selection.start, cx); + let tab_size = settings.tab_size.get(); + let indent_kind = if settings.hard_tabs { IndentKind::Tab } else { IndentKind::Space @@ -3674,11 +3701,8 @@ impl Editor { let buffer = self.buffer.read(cx); let snapshot = buffer.snapshot(cx); for selection in &selections { - let language_name = buffer.language_at(selection.start, cx).map(|l| l.name()); - let tab_size = cx - .global::() - .tab_size(language_name.as_deref()) - .get(); + let settings = buffer.settings_at(selection.start, cx); + let tab_size = settings.tab_size.get(); let mut rows = selection.spanned_rows(false, &display_map); // Avoid re-outdenting a row that has already been outdented by a @@ -5546,68 +5570,91 @@ impl Editor { } fn go_to_hunk(&mut self, _: &GoToHunk, cx: &mut ViewContext) { - self.go_to_hunk_impl(Direction::Next, cx) - } - - fn go_to_prev_hunk(&mut self, _: &GoToPrevHunk, cx: &mut ViewContext) { - self.go_to_hunk_impl(Direction::Prev, cx) - } - - pub fn go_to_hunk_impl(&mut self, direction: Direction, cx: &mut ViewContext) { let snapshot = self .display_map .update(cx, |display_map, cx| display_map.snapshot(cx)); let selection = self.selections.newest::(cx); - fn seek_in_direction( - this: &mut Editor, - snapshot: &DisplaySnapshot, - initial_point: Point, - is_wrapped: bool, - direction: Direction, - cx: &mut ViewContext, - ) -> bool { - let hunks = if direction == Direction::Next { + if !self.seek_in_direction( + &snapshot, + selection.head(), + false, + snapshot + .buffer_snapshot + .git_diff_hunks_in_range((selection.head().row + 1)..u32::MAX), + cx, + ) { + let wrapped_point = Point::zero(); + self.seek_in_direction( + &snapshot, + wrapped_point, + true, snapshot .buffer_snapshot - .git_diff_hunks_in_range(initial_point.row..u32::MAX, false) - } else { - snapshot - .buffer_snapshot - .git_diff_hunks_in_range(0..initial_point.row, true) - }; - - let display_point = initial_point.to_display_point(snapshot); - let mut hunks = hunks - .map(|hunk| diff_hunk_to_display(hunk, &snapshot)) - .skip_while(|hunk| { - if is_wrapped { - false - } else { - hunk.contains_display_row(display_point.row()) - } - }) - .dedup(); - - if let Some(hunk) = hunks.next() { - this.change_selections(Some(Autoscroll::fit()), cx, |s| { - let row = hunk.start_display_row(); - let point = DisplayPoint::new(row, 0); - s.select_display_ranges([point..point]); - }); - - true - } else { - false - } + .git_diff_hunks_in_range((wrapped_point.row + 1)..u32::MAX), + cx, + ); } + } - if !seek_in_direction(self, &snapshot, selection.head(), false, direction, cx) { - let wrapped_point = match direction { - Direction::Next => Point::zero(), - Direction::Prev => snapshot.buffer_snapshot.max_point(), - }; - seek_in_direction(self, &snapshot, wrapped_point, true, direction, cx); + fn go_to_prev_hunk(&mut self, _: &GoToPrevHunk, cx: &mut ViewContext) { + let snapshot = self + .display_map + .update(cx, |display_map, cx| display_map.snapshot(cx)); + let selection = self.selections.newest::(cx); + + if !self.seek_in_direction( + &snapshot, + selection.head(), + false, + snapshot + .buffer_snapshot + .git_diff_hunks_in_range_rev(0..selection.head().row), + cx, + ) { + let wrapped_point = snapshot.buffer_snapshot.max_point(); + self.seek_in_direction( + &snapshot, + wrapped_point, + true, + snapshot + .buffer_snapshot + .git_diff_hunks_in_range_rev(0..wrapped_point.row), + cx, + ); + } + } + + fn seek_in_direction( + &mut self, + snapshot: &DisplaySnapshot, + initial_point: Point, + is_wrapped: bool, + hunks: impl Iterator>, + cx: &mut ViewContext, + ) -> bool { + let display_point = initial_point.to_display_point(snapshot); + let mut hunks = hunks + .map(|hunk| diff_hunk_to_display(hunk, &snapshot)) + .skip_while(|hunk| { + if is_wrapped { + false + } else { + hunk.contains_display_row(display_point.row()) + } + }) + .dedup(); + + if let Some(hunk) = hunks.next() { + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + let row = hunk.start_display_row(); + let point = DisplayPoint::new(row, 0); + s.select_display_ranges([point..point]); + }); + + true + } else { + false } } @@ -6439,27 +6486,24 @@ impl Editor { } pub fn soft_wrap_mode(&self, cx: &AppContext) -> SoftWrap { - let language_name = self - .buffer - .read(cx) - .as_singleton() - .and_then(|singleton_buffer| singleton_buffer.read(cx).language()) - .map(|l| l.name()); - - let settings = cx.global::(); + let settings = self.buffer.read(cx).settings_at(0, cx); let mode = self .soft_wrap_mode_override - .unwrap_or_else(|| settings.soft_wrap(language_name.as_deref())); + .unwrap_or_else(|| settings.soft_wrap); match mode { - settings::SoftWrap::None => SoftWrap::None, - settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth, - settings::SoftWrap::PreferredLineLength => { - SoftWrap::Column(settings.preferred_line_length(language_name.as_deref())) + language_settings::SoftWrap::None => SoftWrap::None, + language_settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth, + language_settings::SoftWrap::PreferredLineLength => { + SoftWrap::Column(settings.preferred_line_length) } } } - pub fn set_soft_wrap_mode(&mut self, mode: settings::SoftWrap, cx: &mut ViewContext) { + pub fn set_soft_wrap_mode( + &mut self, + mode: language_settings::SoftWrap, + cx: &mut ViewContext, + ) { self.soft_wrap_mode_override = Some(mode); cx.notify(); } @@ -6474,8 +6518,8 @@ impl Editor { self.soft_wrap_mode_override.take(); } else { let soft_wrap = match self.soft_wrap_mode(cx) { - SoftWrap::None => settings::SoftWrap::EditorWidth, - SoftWrap::EditorWidth | SoftWrap::Column(_) => settings::SoftWrap::None, + SoftWrap::None => language_settings::SoftWrap::EditorWidth, + SoftWrap::EditorWidth | SoftWrap::Column(_) => language_settings::SoftWrap::None, }; self.soft_wrap_mode_override = Some(soft_wrap); } @@ -6550,8 +6594,8 @@ impl Editor { let buffer = &snapshot.buffer_snapshot; let start = buffer.anchor_before(0); let end = buffer.anchor_after(buffer.len()); - let theme = cx.global::().theme.as_ref(); - self.background_highlights_in_range(start..end, &snapshot, theme) + let theme = theme::current(cx); + self.background_highlights_in_range(start..end, &snapshot, theme.as_ref()) } fn document_highlights_for_position<'a>( @@ -6861,44 +6905,88 @@ impl Editor { .collect() } - fn report_editor_event(&self, name: &'static str, cx: &AppContext) { - if let Some((project, file)) = self.project.as_ref().zip( - self.buffer - .read(cx) - .as_singleton() - .and_then(|b| b.read(cx).file()), - ) { - let settings = cx.global::(); + fn report_copilot_event( + &self, + suggestion_id: Option, + suggestion_accepted: bool, + cx: &AppContext, + ) { + let Some(project) = &self.project else { + return + }; - let extension = Path::new(file.file_name(cx)) - .extension() - .and_then(|e| e.to_str()); - let telemetry = project.read(cx).client().telemetry().clone(); - telemetry.report_mixpanel_event( - match name { - "open" => "open editor", - "save" => "save editor", - _ => name, - }, - json!({ "File Extension": extension, "Vim Mode": settings.vim_mode, "In Clickhouse": true }), - settings.telemetry(), - ); - let event = ClickhouseEvent::Editor { - file_extension: extension.map(ToString::to_string), - vim_mode: settings.vim_mode, - operation: name, - copilot_enabled: settings.features.copilot, - copilot_enabled_for_language: settings.show_copilot_suggestions( - self.language_at(0, cx) - .map(|language| language.name()) - .as_deref(), - self.file_at(0, cx) - .map(|file| file.path().clone()) - .as_deref(), - ), - }; - telemetry.report_clickhouse_event(event, settings.telemetry()) - } + // If None, we are either getting suggestions in a new, unsaved file, or in a file without an extension + let file_extension = self + .buffer + .read(cx) + .as_singleton() + .and_then(|b| b.read(cx).file()) + .and_then(|file| Path::new(file.file_name(cx)).extension()) + .and_then(|e| e.to_str()) + .map(|a| a.to_string()); + + let telemetry = project.read(cx).client().telemetry().clone(); + let telemetry_settings = *settings::get::(cx); + + let event = ClickhouseEvent::Copilot { + suggestion_id, + suggestion_accepted, + file_extension, + }; + telemetry.report_clickhouse_event(event, telemetry_settings); + } + + fn report_editor_event( + &self, + name: &'static str, + file_extension: Option, + cx: &AppContext, + ) { + let Some(project) = &self.project else { + return + }; + + // If None, we are in a file without an extension + let file_extension = file_extension.or(self + .buffer + .read(cx) + .as_singleton() + .and_then(|b| b.read(cx).file()) + .and_then(|file| Path::new(file.file_name(cx)).extension()) + .and_then(|e| e.to_str()) + .map(|a| a.to_string())); + + let vim_mode = cx + .global::() + .untyped_user_settings() + .get("vim_mode") + == Some(&serde_json::Value::Bool(true)); + let telemetry_settings = *settings::get::(cx); + let copilot_enabled = all_language_settings(cx).copilot_enabled(None, None); + let copilot_enabled_for_language = self + .buffer + .read(cx) + .settings_at(0, cx) + .show_copilot_suggestions; + + let telemetry = project.read(cx).client().telemetry().clone(); + telemetry.report_mixpanel_event( + match name { + "open" => "open editor", + "save" => "save editor", + _ => name, + }, + json!({ "File Extension": file_extension, "Vim Mode": vim_mode, "In Clickhouse": true }), + telemetry_settings, + ); + let event = ClickhouseEvent::Editor { + file_extension, + vim_mode, + operation: name, + copilot_enabled, + copilot_enabled_for_language, + }; + telemetry.report_clickhouse_event(event, telemetry_settings) } /// Copy the highlighted chunks to the clipboard as JSON. The format is an array of lines, @@ -6930,7 +7018,7 @@ impl Editor { let mut lines = Vec::new(); let mut line: VecDeque = VecDeque::new(); - let theme = &cx.global::().theme.editor.syntax; + let theme = &theme::current(cx).editor.syntax; for chunk in chunks { let highlight = chunk.syntax_highlight_id.and_then(|id| id.name(theme)); @@ -7352,7 +7440,7 @@ impl View for Editor { } fn build_style( - settings: &Settings, + settings: &ThemeSettings, get_field_editor_theme: Option<&GetFieldEditorTheme>, override_text_style: Option<&OverrideTextStyle>, cx: &AppContext, @@ -7382,7 +7470,7 @@ fn build_style( let font_id = font_cache .select_font(font_family_id, &font_properties) .unwrap(); - let font_size = settings.buffer_font_size; + let font_size = settings.buffer_font_size(cx); EditorStyle { text: TextStyle { color: settings.theme.editor.text_color, @@ -7552,10 +7640,10 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend } Arc::new(move |cx: &mut BlockContext| { - let settings = cx.global::(); + let settings = settings::get::(cx); let theme = &settings.theme.editor; let style = diagnostic_style(diagnostic.severity, is_valid, theme); - let font_size = (style.text_scale_factor * settings.buffer_font_size).round(); + let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round(); Flex::column() .with_children(highlighted_lines.iter().map(|(line, highlights)| { Label::new( diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs new file mode 100644 index 0000000000..5108d27408 --- /dev/null +++ b/crates/editor/src/editor_settings.rs @@ -0,0 +1,43 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::Setting; + +#[derive(Deserialize)] +pub struct EditorSettings { + pub cursor_blink: bool, + pub hover_popover_enabled: bool, + pub show_completions_on_input: bool, + pub show_scrollbars: ShowScrollbars, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)] +#[serde(rename_all = "snake_case")] +pub enum ShowScrollbars { + #[default] + Auto, + System, + Always, + Never, +} + +#[derive(Clone, Serialize, Deserialize, JsonSchema)] +pub struct EditorSettingsContent { + pub cursor_blink: Option, + pub hover_popover_enabled: Option, + pub show_completions_on_input: Option, + pub show_scrollbars: Option, +} + +impl Setting for EditorSettings { + const KEY: Option<&'static str> = None; + + type FileContent = EditorSettingsContent; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &gpui::AppContext, + ) -> anyhow::Result { + Self::load_via_json_merge(default_value, user_values) + } +} diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 0f749bde48..9a21429301 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -12,10 +12,12 @@ use gpui::{ serde_json, TestAppContext, }; use indoc::indoc; -use language::{BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageRegistry, Point}; +use language::{ + language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent}, + BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageRegistry, Point, +}; use parking_lot::Mutex; use project::FakeFs; -use settings::EditorSettings; use std::{cell::RefCell, future::Future, rc::Rc, time::Instant}; use unindent::Unindent; use util::{ @@ -29,7 +31,8 @@ use workspace::{ #[gpui::test] fn test_edit_events(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let buffer = cx.add_model(|cx| { let mut buffer = language::Buffer::new(0, "123456", cx); buffer.set_group_interval(Duration::from_secs(1)); @@ -156,7 +159,8 @@ fn test_edit_events(cx: &mut TestAppContext) { #[gpui::test] fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let mut now = Instant::now(); let buffer = cx.add_model(|cx| language::Buffer::new(0, "123456", cx)); let group_interval = buffer.read_with(cx, |buffer, _| buffer.transaction_group_interval()); @@ -226,7 +230,8 @@ fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) { #[gpui::test] fn test_ime_composition(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let buffer = cx.add_model(|cx| { let mut buffer = language::Buffer::new(0, "abcde", cx); // Ensure automatic grouping doesn't occur. @@ -328,7 +333,7 @@ fn test_ime_composition(cx: &mut TestAppContext) { #[gpui::test] fn test_selection_with_mouse(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); let (_, editor) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx); @@ -395,7 +400,8 @@ fn test_selection_with_mouse(cx: &mut TestAppContext) { #[gpui::test] fn test_canceling_pending_selection(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, view) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); build_editor(buffer, cx) @@ -429,6 +435,8 @@ fn test_canceling_pending_selection(cx: &mut TestAppContext) { #[gpui::test] fn test_clone(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let (text, selection_ranges) = marked_text_ranges( indoc! {" one @@ -439,7 +447,6 @@ fn test_clone(cx: &mut TestAppContext) { "}, true, ); - cx.update(|cx| cx.set_global(Settings::test(cx))); let (_, editor) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple(&text, cx); @@ -487,7 +494,8 @@ fn test_clone(cx: &mut TestAppContext) { #[gpui::test] async fn test_navigation_history(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + cx.set_global(DragAndDrop::::default()); use workspace::item::Item; @@ -600,7 +608,8 @@ async fn test_navigation_history(cx: &mut TestAppContext) { #[gpui::test] fn test_cancel(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, view) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); build_editor(buffer, cx) @@ -642,7 +651,8 @@ fn test_cancel(cx: &mut TestAppContext) { #[gpui::test] fn test_fold_action(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, view) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple( &" @@ -731,7 +741,8 @@ fn test_fold_action(cx: &mut TestAppContext) { #[gpui::test] fn test_move_cursor(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx)); let (_, view) = cx.add_window(|cx| build_editor(buffer.clone(), cx)); @@ -806,7 +817,8 @@ fn test_move_cursor(cx: &mut TestAppContext) { #[gpui::test] fn test_move_cursor_multibyte(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, view) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε\n", cx); build_editor(buffer.clone(), cx) @@ -910,7 +922,8 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) { #[gpui::test] fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, view) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx); build_editor(buffer.clone(), cx) @@ -959,7 +972,8 @@ fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) { #[gpui::test] fn test_beginning_end_of_line(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, view) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple("abc\n def", cx); build_editor(buffer, cx) @@ -1121,7 +1135,8 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) { #[gpui::test] fn test_prev_next_word_boundary(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, view) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx); build_editor(buffer, cx) @@ -1172,7 +1187,8 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) { #[gpui::test] fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, view) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx); build_editor(buffer, cx) @@ -1229,6 +1245,7 @@ fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) { #[gpui::test] async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx); let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); @@ -1343,6 +1360,7 @@ async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx); cx.set_state("one «two threeˇ» four"); cx.update_editor(|editor, cx| { @@ -1353,7 +1371,8 @@ async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) { #[gpui::test] fn test_delete_to_word_boundary(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, view) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple("one two three four", cx); build_editor(buffer.clone(), cx) @@ -1388,7 +1407,8 @@ fn test_delete_to_word_boundary(cx: &mut TestAppContext) { #[gpui::test] fn test_newline(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, view) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx); build_editor(buffer.clone(), cx) @@ -1410,7 +1430,8 @@ fn test_newline(cx: &mut TestAppContext) { #[gpui::test] fn test_newline_with_old_selections(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, editor) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple( " @@ -1491,11 +1512,8 @@ fn test_newline_with_old_selections(cx: &mut TestAppContext) { #[gpui::test] async fn test_newline_above(cx: &mut gpui::TestAppContext) { - let mut cx = EditorTestContext::new(cx); - cx.update(|cx| { - cx.update_global::(|settings, _| { - settings.editor_overrides.tab_size = Some(NonZeroU32::new(4).unwrap()); - }); + init_test(cx, |settings| { + settings.defaults.tab_size = NonZeroU32::new(4) }); let language = Arc::new( @@ -1506,8 +1524,9 @@ async fn test_newline_above(cx: &mut gpui::TestAppContext) { .with_indents_query(r#"(_ "(" ")" @end) @indent"#) .unwrap(), ); - cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + let mut cx = EditorTestContext::new(cx); + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); cx.set_state(indoc! {" const a: ˇA = ( (ˇ @@ -1516,6 +1535,7 @@ async fn test_newline_above(cx: &mut gpui::TestAppContext) { )ˇ ˇ);ˇ "}); + cx.update_editor(|e, cx| e.newline_above(&NewlineAbove, cx)); cx.assert_editor_state(indoc! {" ˇ @@ -1540,11 +1560,8 @@ async fn test_newline_above(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_newline_below(cx: &mut gpui::TestAppContext) { - let mut cx = EditorTestContext::new(cx); - cx.update(|cx| { - cx.update_global::(|settings, _| { - settings.editor_overrides.tab_size = Some(NonZeroU32::new(4).unwrap()); - }); + init_test(cx, |settings| { + settings.defaults.tab_size = NonZeroU32::new(4) }); let language = Arc::new( @@ -1555,8 +1572,9 @@ async fn test_newline_below(cx: &mut gpui::TestAppContext) { .with_indents_query(r#"(_ "(" ")" @end) @indent"#) .unwrap(), ); - cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + let mut cx = EditorTestContext::new(cx); + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); cx.set_state(indoc! {" const a: ˇA = ( (ˇ @@ -1565,6 +1583,7 @@ async fn test_newline_below(cx: &mut gpui::TestAppContext) { )ˇ ˇ);ˇ "}); + cx.update_editor(|e, cx| e.newline_below(&NewlineBelow, cx)); cx.assert_editor_state(indoc! {" const a: A = ( @@ -1589,7 +1608,8 @@ async fn test_newline_below(cx: &mut gpui::TestAppContext) { #[gpui::test] fn test_insert_with_old_selections(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, editor) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx); let mut editor = build_editor(buffer.clone(), cx); @@ -1615,12 +1635,11 @@ fn test_insert_with_old_selections(cx: &mut TestAppContext) { #[gpui::test] async fn test_tab(cx: &mut gpui::TestAppContext) { - let mut cx = EditorTestContext::new(cx); - cx.update(|cx| { - cx.update_global::(|settings, _| { - settings.editor_overrides.tab_size = Some(NonZeroU32::new(3).unwrap()); - }); + init_test(cx, |settings| { + settings.defaults.tab_size = NonZeroU32::new(3) }); + + let mut cx = EditorTestContext::new(cx); cx.set_state(indoc! {" ˇabˇc ˇ🏀ˇ🏀ˇefg @@ -1646,6 +1665,8 @@ async fn test_tab(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx); let language = Arc::new( Language::new( @@ -1704,7 +1725,10 @@ async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut gpui::TestAp #[gpui::test] async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) { - let mut cx = EditorTestContext::new(cx); + init_test(cx, |settings| { + settings.defaults.tab_size = NonZeroU32::new(4) + }); + let language = Arc::new( Language::new( LanguageConfig::default(), @@ -1713,14 +1737,9 @@ async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) { .with_indents_query(r#"(_ "{" "}" @end) @indent"#) .unwrap(), ); + + let mut cx = EditorTestContext::new(cx); cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); - - cx.update(|cx| { - cx.update_global::(|settings, _| { - settings.editor_overrides.tab_size = Some(4.try_into().unwrap()); - }); - }); - cx.set_state(indoc! {" fn a() { if b { @@ -1741,6 +1760,10 @@ async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_indent_outdent(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.tab_size = NonZeroU32::new(4); + }); + let mut cx = EditorTestContext::new(cx); cx.set_state(indoc! {" @@ -1810,13 +1833,12 @@ async fn test_indent_outdent(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) { - let mut cx = EditorTestContext::new(cx); - cx.update(|cx| { - cx.update_global::(|settings, _| { - settings.editor_overrides.hard_tabs = Some(true); - }); + init_test(cx, |settings| { + settings.defaults.hard_tabs = Some(true); }); + let mut cx = EditorTestContext::new(cx); + // select two ranges on one line cx.set_state(indoc! {" «oneˇ» «twoˇ» @@ -1907,25 +1929,25 @@ async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) { #[gpui::test] fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) { - cx.update(|cx| { - cx.set_global( - Settings::test(cx) - .with_language_defaults( - "TOML", - EditorSettings { - tab_size: Some(2.try_into().unwrap()), - ..Default::default() - }, - ) - .with_language_defaults( - "Rust", - EditorSettings { - tab_size: Some(4.try_into().unwrap()), - ..Default::default() - }, - ), - ); + init_test(cx, |settings| { + settings.languages.extend([ + ( + "TOML".into(), + LanguageSettingsContent { + tab_size: NonZeroU32::new(2), + ..Default::default() + }, + ), + ( + "Rust".into(), + LanguageSettingsContent { + tab_size: NonZeroU32::new(4), + ..Default::default() + }, + ), + ]); }); + let toml_language = Arc::new(Language::new( LanguageConfig { name: "TOML".into(), @@ -2020,6 +2042,8 @@ fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) { #[gpui::test] async fn test_backspace(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx); // Basic backspace @@ -2067,8 +2091,9 @@ async fn test_backspace(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_delete(cx: &mut gpui::TestAppContext) { - let mut cx = EditorTestContext::new(cx); + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx); cx.set_state(indoc! {" onˇe two three fou«rˇ» five six @@ -2095,7 +2120,8 @@ async fn test_delete(cx: &mut gpui::TestAppContext) { #[gpui::test] fn test_delete_line(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, view) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); build_editor(buffer, cx) @@ -2119,7 +2145,6 @@ fn test_delete_line(cx: &mut TestAppContext) { ); }); - cx.update(|cx| cx.set_global(Settings::test(cx))); let (_, view) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); build_editor(buffer, cx) @@ -2139,7 +2164,8 @@ fn test_delete_line(cx: &mut TestAppContext) { #[gpui::test] fn test_duplicate_line(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, view) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); build_editor(buffer, cx) @@ -2191,7 +2217,8 @@ fn test_duplicate_line(cx: &mut TestAppContext) { #[gpui::test] fn test_move_line_up_down(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, view) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); build_editor(buffer, cx) @@ -2289,7 +2316,8 @@ fn test_move_line_up_down(cx: &mut TestAppContext) { #[gpui::test] fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, editor) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); build_editor(buffer, cx) @@ -2315,7 +2343,7 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) { #[gpui::test] fn test_transpose(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); _ = cx .add_window(|cx| { @@ -2417,6 +2445,8 @@ fn test_transpose(cx: &mut TestAppContext) { #[gpui::test] async fn test_clipboard(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx); cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six "); @@ -2497,6 +2527,8 @@ async fn test_clipboard(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_paste_multiline(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx); let language = Arc::new(Language::new( LanguageConfig::default(), @@ -2609,7 +2641,8 @@ async fn test_paste_multiline(cx: &mut gpui::TestAppContext) { #[gpui::test] fn test_select_all(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, view) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx); build_editor(buffer, cx) @@ -2625,7 +2658,8 @@ fn test_select_all(cx: &mut TestAppContext) { #[gpui::test] fn test_select_line(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, view) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx); build_editor(buffer, cx) @@ -2671,7 +2705,8 @@ fn test_select_line(cx: &mut TestAppContext) { #[gpui::test] fn test_split_selection_into_lines(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, view) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx); build_editor(buffer, cx) @@ -2741,7 +2776,8 @@ fn test_split_selection_into_lines(cx: &mut TestAppContext) { #[gpui::test] fn test_add_selection_above_below(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, view) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx); build_editor(buffer, cx) @@ -2935,6 +2971,8 @@ fn test_add_selection_above_below(cx: &mut TestAppContext) { #[gpui::test] async fn test_select_next(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx); cx.set_state("abc\nˇabc abc\ndefabc\nabc"); @@ -2959,7 +2997,8 @@ async fn test_select_next(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let language = Arc::new(Language::new( LanguageConfig::default(), Some(tree_sitter_rust::language()), @@ -3100,7 +3139,8 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let language = Arc::new( Language::new( LanguageConfig { @@ -3160,6 +3200,8 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx); let language = Arc::new(Language::new( @@ -3329,6 +3371,8 @@ async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx); let html_language = Arc::new( @@ -3563,6 +3607,8 @@ async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx); let rust_language = Arc::new( @@ -3660,7 +3706,8 @@ async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let language = Arc::new(Language::new( LanguageConfig { brackets: BracketPairConfig { @@ -3814,7 +3861,8 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let language = Arc::new(Language::new( LanguageConfig { brackets: BracketPairConfig { @@ -3919,7 +3967,7 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_snippets(cx: &mut gpui::TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); let (text, insertion_ranges) = marked_text_ranges( indoc! {" @@ -4027,7 +4075,7 @@ async fn test_snippets(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); + init_test(cx, |_| {}); let mut language = Language::new( LanguageConfig { @@ -4111,16 +4159,14 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { assert!(!cx.read(|cx| editor.is_dirty(cx))); // Set rust language override and assert overriden tabsize is sent to language server - cx.update(|cx| { - cx.update_global::(|settings, _| { - settings.language_overrides.insert( - "Rust".into(), - EditorSettings { - tab_size: Some(8.try_into().unwrap()), - ..Default::default() - }, - ); - }) + update_test_settings(cx, |settings| { + settings.languages.insert( + "Rust".into(), + LanguageSettingsContent { + tab_size: NonZeroU32::new(8), + ..Default::default() + }, + ); }); let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); @@ -4141,7 +4187,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); + init_test(cx, |_| {}); let mut language = Language::new( LanguageConfig { @@ -4227,16 +4273,14 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { assert!(!cx.read(|cx| editor.is_dirty(cx))); // Set rust language override and assert overriden tabsize is sent to language server - cx.update(|cx| { - cx.update_global::(|settings, _| { - settings.language_overrides.insert( - "Rust".into(), - EditorSettings { - tab_size: Some(8.try_into().unwrap()), - ..Default::default() - }, - ); - }) + update_test_settings(cx, |settings| { + settings.languages.insert( + "Rust".into(), + LanguageSettingsContent { + tab_size: NonZeroU32::new(8), + ..Default::default() + }, + ); }); let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); @@ -4257,7 +4301,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); + init_test(cx, |_| {}); let mut language = Language::new( LanguageConfig { @@ -4342,7 +4386,7 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); + init_test(cx, |_| {}); let mut cx = EditorLspTestContext::new_rust( lsp::ServerCapabilities { @@ -4399,7 +4443,7 @@ async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); + init_test(cx, |_| {}); let mut cx = EditorLspTestContext::new_rust( lsp::ServerCapabilities { @@ -4514,6 +4558,8 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) #[gpui::test] async fn test_completion(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorLspTestContext::new_rust( lsp::ServerCapabilities { completion_provider: Some(lsp::CompletionOptions { @@ -4651,8 +4697,10 @@ async fn test_completion(cx: &mut gpui::TestAppContext) { apply_additional_edits.await.unwrap(); cx.update(|cx| { - cx.update_global::(|settings, _| { - settings.show_completions_on_input = false; + cx.update_global::(|settings, cx| { + settings.update_user_settings::(cx, |settings| { + settings.show_completions_on_input = Some(false); + }); }) }); cx.set_state("editorˇ"); @@ -4681,7 +4729,8 @@ async fn test_completion(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_toggle_comment(cx: &mut gpui::TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let language = Arc::new(Language::new( LanguageConfig { line_comment: Some("// ".into()), @@ -4764,8 +4813,7 @@ async fn test_toggle_comment(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) { - let mut cx = EditorTestContext::new(cx); - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); let language = Arc::new(Language::new( LanguageConfig { @@ -4778,6 +4826,7 @@ async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) let registry = Arc::new(LanguageRegistry::test()); registry.add(language.clone()); + let mut cx = EditorTestContext::new(cx); cx.update_buffer(|buffer, cx| { buffer.set_language_registry(registry); buffer.set_language(Some(language), cx); @@ -4897,6 +4946,8 @@ async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) #[gpui::test] async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx); let html_language = Arc::new( @@ -5021,7 +5072,8 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) { #[gpui::test] fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); @@ -5067,7 +5119,8 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) { #[gpui::test] fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let markers = vec![('[', ']').into(), ('(', ')').into()]; let (initial_text, mut excerpt_ranges) = marked_text_ranges_by( indoc! {" @@ -5140,7 +5193,8 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { #[gpui::test] fn test_refresh_selections(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); let mut excerpt1_id = None; let multibuffer = cx.add_model(|cx| { @@ -5224,7 +5278,8 @@ fn test_refresh_selections(cx: &mut TestAppContext) { #[gpui::test] fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); let mut excerpt1_id = None; let multibuffer = cx.add_model(|cx| { @@ -5282,7 +5337,8 @@ fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) { #[gpui::test] async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let language = Arc::new( Language::new( LanguageConfig { @@ -5355,7 +5411,8 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { #[gpui::test] fn test_highlighted_ranges(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, editor) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); build_editor(buffer.clone(), cx) @@ -5395,7 +5452,7 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) { let mut highlighted_ranges = editor.background_highlights_in_range( anchor_range(Point::new(3, 4)..Point::new(7, 4)), &snapshot, - cx.global::().theme.as_ref(), + theme::current(cx).as_ref(), ); // Enforce a consistent ordering based on color without relying on the ordering of the // highlight's `TypeId` which is non-deterministic. @@ -5425,7 +5482,7 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) { editor.background_highlights_in_range( anchor_range(Point::new(5, 6)..Point::new(6, 4)), &snapshot, - cx.global::().theme.as_ref(), + theme::current(cx).as_ref(), ), &[( DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), @@ -5437,7 +5494,8 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) { #[gpui::test] async fn test_following(cx: &mut gpui::TestAppContext) { - Settings::test_async(cx); + init_test(cx, |_| {}); + let fs = FakeFs::new(cx.background()); let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; @@ -5459,10 +5517,12 @@ async fn test_following(cx: &mut gpui::TestAppContext) { }); let is_still_following = Rc::new(RefCell::new(true)); + let follower_edit_event_count = Rc::new(RefCell::new(0)); let pending_update = Rc::new(RefCell::new(None)); follower.update(cx, { let update = pending_update.clone(); let is_still_following = is_still_following.clone(); + let follower_edit_event_count = follower_edit_event_count.clone(); |_, cx| { cx.subscribe(&leader, move |_, leader, event, cx| { leader @@ -5475,6 +5535,9 @@ async fn test_following(cx: &mut gpui::TestAppContext) { if Editor::should_unfollow_on_event(event, cx) { *is_still_following.borrow_mut() = false; } + if let Event::BufferEdited = event { + *follower_edit_event_count.borrow_mut() += 1; + } }) .detach(); } @@ -5494,6 +5557,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) { assert_eq!(follower.selections.ranges(cx), vec![1..1]); }); assert_eq!(*is_still_following.borrow(), true); + assert_eq!(*follower_edit_event_count.borrow(), 0); // Update the scroll position only leader.update(cx, |leader, cx| { @@ -5510,6 +5574,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) { vec2f(1.5, 3.5) ); assert_eq!(*is_still_following.borrow(), true); + assert_eq!(*follower_edit_event_count.borrow(), 0); // Update the selections and scroll position. The follower's scroll position is updated // via autoscroll, not via the leader's exact scroll position. @@ -5576,7 +5641,8 @@ async fn test_following(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { - Settings::test_async(cx); + init_test(cx, |_| {}); + let fs = FakeFs::new(cx.background()); let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); @@ -5805,6 +5871,8 @@ fn test_combine_syntax_and_fuzzy_match_highlights() { #[gpui::test] async fn go_to_hunk(deterministic: Arc, cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx); let diff_base = r#" @@ -5924,6 +5992,8 @@ fn test_split_words() { #[gpui::test] async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await; let mut assert = |before, after| { let _state_context = cx.set_state(before); @@ -5972,6 +6042,8 @@ async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) { #[gpui::test(iterations = 10)] async fn test_copilot(deterministic: Arc, cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let (copilot, copilot_lsp) = Copilot::fake(cx); cx.update(|cx| cx.set_global(copilot)); let mut cx = EditorLspTestContext::new_rust( @@ -6223,6 +6295,8 @@ async fn test_copilot_completion_invalidation( deterministic: Arc, cx: &mut gpui::TestAppContext, ) { + init_test(cx, |_| {}); + let (copilot, copilot_lsp) = Copilot::fake(cx); cx.update(|cx| cx.set_global(copilot)); let mut cx = EditorLspTestContext::new_rust( @@ -6288,11 +6362,10 @@ async fn test_copilot_multibuffer( deterministic: Arc, cx: &mut gpui::TestAppContext, ) { + init_test(cx, |_| {}); + let (copilot, copilot_lsp) = Copilot::fake(cx); - cx.update(|cx| { - cx.set_global(Settings::test(cx)); - cx.set_global(copilot) - }); + 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)); @@ -6392,14 +6465,16 @@ async fn test_copilot_disabled_globs( deterministic: Arc, cx: &mut gpui::TestAppContext, ) { - let (copilot, copilot_lsp) = Copilot::fake(cx); - cx.update(|cx| { - let mut settings = Settings::test(cx); - settings.copilot.disabled_globs = vec![glob::Pattern::new(".env*").unwrap()]; - cx.set_global(settings); - cx.set_global(copilot) + init_test(cx, |settings| { + settings + .copilot + .get_or_insert(Default::default()) + .disabled_globs = Some(vec![".env*".to_string()]); }); + let (copilot, copilot_lsp) = Copilot::fake(cx); + cx.update(|cx| cx.set_global(copilot)); + let fs = FakeFs::new(cx.background()); fs.insert_tree( "/test", @@ -6596,3 +6671,30 @@ fn handle_copilot_completion_request( } }); } + +pub(crate) fn update_test_settings( + cx: &mut TestAppContext, + f: impl Fn(&mut AllLanguageSettingsContent), +) { + cx.update(|cx| { + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, f); + }); + }); +} + +pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) { + cx.foreground().forbid_parking(); + + cx.update(|cx| { + cx.set_global(SettingsStore::test(cx)); + theme::init((), cx); + client::init_settings(cx); + language::init(cx); + Project::init_settings(cx); + workspace::init_settings(cx); + crate::init(cx); + }); + + update_test_settings(cx, f); +} diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index c1710b7337..57dc3293f6 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -5,6 +5,7 @@ use super::{ }; use crate::{ display_map::{BlockStyle, DisplaySnapshot, FoldStatus, TransformBlock}, + editor_settings::ShowScrollbars, git::{diff_hunk_to_display, DisplayDiffHunk}, hover_popover::{ hide_hover, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, @@ -13,7 +14,7 @@ use crate::{ link_go_to_definition::{ go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link, }, - mouse_context_menu, EditorStyle, GutterHover, UnfoldAt, + mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt, }; use clock::ReplicaId; use collections::{BTreeMap, HashMap}; @@ -35,9 +36,11 @@ use gpui::{ }; use itertools::Itertools; use json::json; -use language::{Bias, CursorShape, DiagnosticSeverity, OffsetUtf16, Selection}; +use language::{ + language_settings::ShowWhitespaceSetting, Bias, CursorShape, DiagnosticSeverity, OffsetUtf16, + Selection, +}; use project::ProjectPath; -use settings::{GitGutter, Settings, ShowWhitespaces}; use smallvec::SmallVec; use std::{ borrow::Cow, @@ -47,7 +50,8 @@ use std::{ ops::Range, sync::Arc, }; -use workspace::item::Item; +use text::Point; +use workspace::{item::Item, GitGutterSetting, WorkspaceSettings}; enum FoldMarkers {} @@ -547,11 +551,11 @@ impl EditorElement { let scroll_top = scroll_position.y() * line_height; let show_gutter = matches!( - &cx.global::() - .git_overrides + settings::get::(cx) + .git .git_gutter .unwrap_or_default(), - GitGutter::TrackedFiles + GitGutterSetting::TrackedFiles ); if show_gutter { @@ -608,7 +612,7 @@ impl EditorElement { layout: &mut LayoutState, cx: &mut ViewContext, ) { - let diff_style = &cx.global::().theme.editor.diff.clone(); + let diff_style = &theme::current(cx).editor.diff.clone(); let line_height = layout.position_map.line_height; let scroll_position = layout.position_map.snapshot.scroll_position(); @@ -648,7 +652,7 @@ impl EditorElement { //TODO: This rendering is entirely a horrible hack DiffHunkStatus::Removed => { - let row = *display_row_range.start(); + let row = display_row_range.start; let offset = line_height / 2.; let start_y = row as f32 * line_height - offset - scroll_top; @@ -670,11 +674,11 @@ impl EditorElement { } }; - let start_row = *display_row_range.start(); - let end_row = *display_row_range.end(); + let start_row = display_row_range.start; + let end_row = display_row_range.end; let start_y = start_row as f32 * line_height - scroll_top; - let end_y = end_row as f32 * line_height - scroll_top + line_height; + let end_y = end_row as f32 * line_height - scroll_top; let width = diff_style.width_em * line_height; let highlight_origin = bounds.origin() + vec2f(-width, start_y); @@ -708,6 +712,7 @@ impl EditorElement { let scroll_left = scroll_position.x() * max_glyph_width; let content_origin = bounds.origin() + vec2f(layout.gutter_margin, 0.); let line_end_overshoot = 0.15 * layout.position_map.line_height; + let whitespace_setting = editor.buffer.read(cx).settings_at(0, cx).show_whitespaces; scene.push_layer(Some(bounds)); @@ -882,9 +887,10 @@ impl EditorElement { content_origin, scroll_left, visible_text_bounds, - cx, + whitespace_setting, &invisible_display_ranges, visible_bounds, + cx, ) } } @@ -1022,15 +1028,16 @@ impl EditorElement { let mut first_row_y_offset = 0.0; // Impose a minimum height on the scrollbar thumb + let row_height = height / max_row; let min_thumb_height = style.min_height_factor * cx.font_cache.line_height(self.style.text.font_size); - let thumb_height = (row_range.end - row_range.start) * height / max_row; + let thumb_height = (row_range.end - row_range.start) * row_height; if thumb_height < min_thumb_height { first_row_y_offset = (min_thumb_height - thumb_height) / 2.0; height -= min_thumb_height - thumb_height; } - let y_for_row = |row: f32| -> f32 { top + first_row_y_offset + row * height / max_row }; + let y_for_row = |row: f32| -> f32 { top + first_row_y_offset + row * row_height }; let thumb_top = y_for_row(row_range.start) - first_row_y_offset; let thumb_bottom = y_for_row(row_range.end) + first_row_y_offset; @@ -1044,6 +1051,54 @@ impl EditorElement { background: style.track.background_color, ..Default::default() }); + + let diff_style = theme::current(cx).editor.diff.clone(); + for hunk in layout + .position_map + .snapshot + .buffer_snapshot + .git_diff_hunks_in_range(0..(max_row.floor() as u32)) + { + let start_display = Point::new(hunk.buffer_range.start, 0) + .to_display_point(&layout.position_map.snapshot.display_snapshot); + let end_display = Point::new(hunk.buffer_range.end, 0) + .to_display_point(&layout.position_map.snapshot.display_snapshot); + let start_y = y_for_row(start_display.row() as f32); + let mut end_y = if hunk.buffer_range.start == hunk.buffer_range.end { + y_for_row((end_display.row() + 1) as f32) + } else { + y_for_row((end_display.row()) as f32) + }; + + if end_y - start_y < 1. { + end_y = start_y + 1.; + } + let bounds = RectF::from_points(vec2f(left, start_y), vec2f(right, end_y)); + + let color = match hunk.status() { + DiffHunkStatus::Added => diff_style.inserted, + DiffHunkStatus::Modified => diff_style.modified, + DiffHunkStatus::Removed => diff_style.deleted, + }; + + let border = Border { + width: 1., + color: style.thumb.border.color, + overlay: false, + top: false, + right: true, + bottom: false, + left: true, + }; + + scene.push_quad(Quad { + bounds, + background: Some(color), + border, + corner_radius: style.thumb.corner_radius, + }) + } + scene.push_quad(Quad { bounds: thumb_bounds, border: style.thumb.border, @@ -1219,7 +1274,7 @@ impl EditorElement { .row; buffer_snapshot - .git_diff_hunks_in_range(buffer_start_row..buffer_end_row, false) + .git_diff_hunks_in_range(buffer_start_row..buffer_end_row) .map(|hunk| diff_hunk_to_display(hunk, snapshot)) .dedup() .collect() @@ -1412,7 +1467,7 @@ impl EditorElement { editor: &mut Editor, cx: &mut LayoutContext, ) -> (f32, Vec) { - let tooltip_style = cx.global::().theme.tooltip.clone(); + let tooltip_style = theme::current(cx).tooltip.clone(); let scroll_x = snapshot.scroll_anchor.offset.x(); let (fixed_blocks, non_fixed_blocks) = snapshot .blocks_in_range(rows.clone()) @@ -1738,9 +1793,10 @@ impl LineWithInvisibles { content_origin: Vector2F, scroll_left: f32, visible_text_bounds: RectF, - cx: &mut ViewContext, + whitespace_setting: ShowWhitespaceSetting, selection_ranges: &[Range], visible_bounds: RectF, + cx: &mut ViewContext, ) { let line_height = layout.position_map.line_height; let line_y = row as f32 * line_height - scroll_top; @@ -1754,7 +1810,6 @@ impl LineWithInvisibles { ); self.draw_invisibles( - cx, &selection_ranges, layout, content_origin, @@ -1764,12 +1819,13 @@ impl LineWithInvisibles { scene, visible_bounds, line_height, + whitespace_setting, + cx, ); } fn draw_invisibles( &self, - cx: &mut ViewContext, selection_ranges: &[Range], layout: &LayoutState, content_origin: Vector2F, @@ -1779,17 +1835,13 @@ impl LineWithInvisibles { scene: &mut SceneBuilder, visible_bounds: RectF, line_height: f32, + whitespace_setting: ShowWhitespaceSetting, + cx: &mut ViewContext, ) { - let settings = cx.global::(); - let allowed_invisibles_regions = match settings - .editor_overrides - .show_whitespaces - .or(settings.editor_defaults.show_whitespaces) - .unwrap_or_default() - { - ShowWhitespaces::None => return, - ShowWhitespaces::Selection => Some(selection_ranges), - ShowWhitespaces::All => None, + let allowed_invisibles_regions = match whitespace_setting { + ShowWhitespaceSetting::None => return, + ShowWhitespaceSetting::Selection => Some(selection_ranges), + ShowWhitespaceSetting::All => None, }; for invisible in &self.invisibles { @@ -1934,11 +1986,11 @@ impl Element for EditorElement { let is_singleton = editor.is_singleton(cx); let highlighted_rows = editor.highlighted_rows(); - let theme = cx.global::().theme.as_ref(); + let theme = theme::current(cx); let highlighted_ranges = editor.background_highlights_in_range( start_anchor..end_anchor, &snapshot.display_snapshot, - theme, + theme.as_ref(), ); fold_ranges.extend( @@ -2013,7 +2065,15 @@ impl Element for EditorElement { )); } - let show_scrollbars = editor.scroll_manager.scrollbars_visible(); + let show_scrollbars = match settings::get::(cx).show_scrollbars { + ShowScrollbars::Auto => { + snapshot.has_scrollbar_info() || editor.scroll_manager.scrollbars_visible() + } + ShowScrollbars::System => editor.scroll_manager.scrollbars_visible(), + ShowScrollbars::Always => true, + ShowScrollbars::Never => false, + }; + let include_root = editor .project .as_ref() @@ -2773,17 +2833,19 @@ mod tests { use super::*; use crate::{ display_map::{BlockDisposition, BlockProperties}, + editor_tests::{init_test, update_test_settings}, Editor, MultiBuffer, }; use gpui::TestAppContext; + use language::language_settings; use log::info; - use settings::Settings; use std::{num::NonZeroU32, sync::Arc}; use util::test::sample_text; #[gpui::test] fn test_layout_line_numbers(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, editor) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx); Editor::new(EditorMode::Full, buffer, None, None, cx) @@ -2801,7 +2863,8 @@ mod tests { #[gpui::test] fn test_layout_with_placeholder_text_and_blocks(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, editor) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple("", cx); Editor::new(EditorMode::Full, buffer, None, None, cx) @@ -2861,26 +2924,27 @@ mod tests { #[gpui::test] fn test_all_invisibles_drawing(cx: &mut TestAppContext) { - let tab_size = 4; + const TAB_SIZE: u32 = 4; + let input_text = "\t \t|\t| a b"; let expected_invisibles = vec![ Invisible::Tab { line_start_offset: 0, }, Invisible::Whitespace { - line_offset: tab_size as usize, + line_offset: TAB_SIZE as usize, }, Invisible::Tab { - line_start_offset: tab_size as usize + 1, + line_start_offset: TAB_SIZE as usize + 1, }, Invisible::Tab { - line_start_offset: tab_size as usize * 2 + 1, + line_start_offset: TAB_SIZE as usize * 2 + 1, }, Invisible::Whitespace { - line_offset: tab_size as usize * 3 + 1, + line_offset: TAB_SIZE as usize * 3 + 1, }, Invisible::Whitespace { - line_offset: tab_size as usize * 3 + 3, + line_offset: TAB_SIZE as usize * 3 + 3, }, ]; assert_eq!( @@ -2892,12 +2956,11 @@ mod tests { "Hardcoded expected invisibles differ from the actual ones in '{input_text}'" ); - cx.update(|cx| { - let mut test_settings = Settings::test(cx); - test_settings.editor_defaults.show_whitespaces = Some(ShowWhitespaces::All); - test_settings.editor_defaults.tab_size = Some(NonZeroU32::new(tab_size).unwrap()); - cx.set_global(test_settings); + init_test(cx, |s| { + s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All); + s.defaults.tab_size = NonZeroU32::new(TAB_SIZE); }); + let actual_invisibles = collect_invisibles_from_new_editor(cx, EditorMode::Full, &input_text, 500.0); @@ -2906,11 +2969,9 @@ mod tests { #[gpui::test] fn test_invisibles_dont_appear_in_certain_editors(cx: &mut TestAppContext) { - cx.update(|cx| { - let mut test_settings = Settings::test(cx); - test_settings.editor_defaults.show_whitespaces = Some(ShowWhitespaces::All); - test_settings.editor_defaults.tab_size = Some(NonZeroU32::new(4).unwrap()); - cx.set_global(test_settings); + init_test(cx, |s| { + s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All); + s.defaults.tab_size = NonZeroU32::new(4); }); for editor_mode_without_invisibles in [ @@ -2961,19 +3022,18 @@ mod tests { ); info!("Expected invisibles: {expected_invisibles:?}"); + init_test(cx, |_| {}); + // Put the same string with repeating whitespace pattern into editors of various size, // take deliberately small steps during resizing, to put all whitespace kinds near the wrap point. let resize_step = 10.0; let mut editor_width = 200.0; while editor_width <= 1000.0 { - cx.update(|cx| { - let mut test_settings = Settings::test(cx); - test_settings.editor_defaults.tab_size = Some(NonZeroU32::new(tab_size).unwrap()); - test_settings.editor_defaults.show_whitespaces = Some(ShowWhitespaces::All); - test_settings.editor_defaults.preferred_line_length = Some(editor_width as u32); - test_settings.editor_defaults.soft_wrap = - Some(settings::SoftWrap::PreferredLineLength); - cx.set_global(test_settings); + update_test_settings(cx, |s| { + s.defaults.tab_size = NonZeroU32::new(tab_size); + s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All); + s.defaults.preferred_line_length = Some(editor_width as u32); + s.defaults.soft_wrap = Some(language_settings::SoftWrap::PreferredLineLength); }); let actual_invisibles = @@ -3021,7 +3081,7 @@ mod tests { let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx))); let (_, layout_state) = editor.update(cx, |editor, cx| { - editor.set_soft_wrap_mode(settings::SoftWrap::EditorWidth, cx); + editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx); editor.set_wrap_width(Some(editor_width), cx); let mut new_parents = Default::default(); diff --git a/crates/editor/src/git.rs b/crates/editor/src/git.rs index 549d74a0b5..3452138126 100644 --- a/crates/editor/src/git.rs +++ b/crates/editor/src/git.rs @@ -1,4 +1,4 @@ -use std::ops::RangeInclusive; +use std::ops::Range; use git::diff::{DiffHunk, DiffHunkStatus}; use language::Point; @@ -15,7 +15,7 @@ pub enum DisplayDiffHunk { }, Unfolded { - display_row_range: RangeInclusive, + display_row_range: Range, status: DiffHunkStatus, }, } @@ -26,7 +26,7 @@ impl DisplayDiffHunk { &DisplayDiffHunk::Folded { display_row } => display_row, DisplayDiffHunk::Unfolded { display_row_range, .. - } => *display_row_range.start(), + } => display_row_range.start, } } @@ -36,7 +36,7 @@ impl DisplayDiffHunk { DisplayDiffHunk::Unfolded { display_row_range, .. - } => display_row_range.clone(), + } => display_row_range.start..=display_row_range.end - 1, }; range.contains(&display_row) @@ -77,16 +77,12 @@ pub fn diff_hunk_to_display(hunk: DiffHunk, snapshot: &DisplaySnapshot) -> } else { let start = hunk_start_point.to_display_point(snapshot).row(); - let hunk_end_row_inclusive = hunk - .buffer_range - .end - .saturating_sub(1) - .max(hunk.buffer_range.start); + let hunk_end_row_inclusive = hunk.buffer_range.end.max(hunk.buffer_range.start); let hunk_end_point = Point::new(hunk_end_row_inclusive, 0); let end = hunk_end_point.to_display_point(snapshot).row(); DisplayDiffHunk::Unfolded { - display_row_range: start..=end, + display_row_range: start..end, status: hunk.status(), } } diff --git a/crates/editor/src/highlight_matching_bracket.rs b/crates/editor/src/highlight_matching_bracket.rs index ce3864f56a..a0baf6882f 100644 --- a/crates/editor/src/highlight_matching_bracket.rs +++ b/crates/editor/src/highlight_matching_bracket.rs @@ -33,12 +33,14 @@ pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewCon #[cfg(test)] mod tests { use super::*; - use crate::test::editor_lsp_test_context::EditorLspTestContext; + use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext}; use indoc::indoc; use language::{BracketPair, BracketPairConfig, Language, LanguageConfig}; #[gpui::test] async fn test_matching_bracket_highlights(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorLspTestContext::new( Language::new( LanguageConfig { diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 438c662ed1..9192dc75e1 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -1,6 +1,6 @@ use crate::{ - display_map::ToDisplayPoint, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSnapshot, - EditorStyle, RangeToAnchorExt, + display_map::ToDisplayPoint, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, + EditorSnapshot, EditorStyle, RangeToAnchorExt, }; use futures::FutureExt; use gpui::{ @@ -12,7 +12,6 @@ use gpui::{ }; use language::{Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry}; use project::{HoverBlock, HoverBlockKind, Project}; -use settings::Settings; use std::{ops::Range, sync::Arc, time::Duration}; use util::TryFutureExt; @@ -38,7 +37,7 @@ pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext) { /// The internal hover action dispatches between `show_hover` or `hide_hover` /// depending on whether a point to hover over is provided. pub fn hover_at(editor: &mut Editor, point: Option, cx: &mut ViewContext) { - if cx.global::().hover_popover_enabled { + if settings::get::(cx).hover_popover_enabled { if let Some(point) = point { show_hover(editor, point, false, cx); } else { @@ -654,7 +653,7 @@ impl DiagnosticPopover { _ => style.hover_popover.container, }; - let tooltip_style = cx.global::().theme.tooltip.clone(); + let tooltip_style = theme::current(cx).tooltip.clone(); MouseEventHandler::::new(0, cx, |_, _| { text.with_soft_wrap(true) @@ -694,7 +693,7 @@ impl DiagnosticPopover { #[cfg(test)] mod tests { use super::*; - use crate::test::editor_lsp_test_context::EditorLspTestContext; + use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext}; use gpui::fonts::Weight; use indoc::indoc; use language::{Diagnostic, DiagnosticSet}; @@ -706,6 +705,8 @@ mod tests { #[gpui::test] async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorLspTestContext::new_rust( lsp::ServerCapabilities { hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), @@ -773,6 +774,8 @@ mod tests { #[gpui::test] async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorLspTestContext::new_rust( lsp::ServerCapabilities { hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), @@ -816,6 +819,8 @@ mod tests { #[gpui::test] async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorLspTestContext::new_rust( lsp::ServerCapabilities { hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), @@ -882,7 +887,8 @@ mod tests { #[gpui::test] fn test_render_blocks(cx: &mut gpui::TestAppContext) { - Settings::test_async(cx); + init_test(cx, |_| {}); + cx.add_window(|cx| { let editor = Editor::single_line(None, cx); let style = editor.style(cx); @@ -1006,8 +1012,7 @@ mod tests { .zip(expected_styles.iter().cloned()) .collect::>(); assert_eq!( - rendered.text, - dbg!(expected_text), + rendered.text, expected_text, "wrong text for input {blocks:?}" ); assert_eq!( diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index fc7c1cd968..483fd56cc5 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -16,7 +16,6 @@ use language::{ }; use project::{FormatTrigger, Item as _, Project, ProjectPath}; use rpc::proto::{self, update_view}; -use settings::Settings; use smallvec::SmallVec; use std::{ borrow::Cow, @@ -27,7 +26,7 @@ use std::{ path::{Path, PathBuf}, }; use text::Selection; -use util::{ResultExt, TryFutureExt}; +use util::{paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt}; use workspace::item::{BreadcrumbText, FollowableItemHandle}; use workspace::{ item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem}, @@ -566,7 +565,7 @@ impl Item for Editor { cx: &AppContext, ) -> AnyElement { Flex::row() - .with_child(Label::new(self.title(cx).to_string(), style.label.clone()).aligned()) + .with_child(Label::new(self.title(cx).to_string(), style.label.clone()).into_any()) .with_children(detail.and_then(|detail| { let path = path_for_buffer(&self.buffer, detail, false, cx)?; let description = path.to_string_lossy(); @@ -580,6 +579,7 @@ impl Item for Editor { .aligned(), ) })) + .align_children_center() .into_any() } @@ -636,7 +636,7 @@ impl Item for Editor { project: ModelHandle, cx: &mut ViewContext, ) -> Task> { - self.report_editor_event("save", cx); + self.report_editor_event("save", None, cx); let format = self.perform_format(project.clone(), FormatTrigger::Save, cx); let buffers = self.buffer().clone().read(cx).all_buffers(); cx.spawn(|_, mut cx| async move { @@ -685,6 +685,11 @@ impl Item for Editor { .as_singleton() .expect("cannot call save_as on an excerpt list"); + let file_extension = abs_path + .extension() + .map(|a| a.to_string_lossy().to_string()); + self.report_editor_event("save", file_extension, cx); + project.update(cx, |project, cx| { project.save_buffer_as(buffer, abs_path, cx) }) @@ -1110,8 +1115,12 @@ impl View for CursorPosition { fn render(&mut self, cx: &mut ViewContext) -> AnyElement { if let Some(position) = self.position { - let theme = &cx.global::().theme.workspace.status_bar; - let mut text = format!("{},{}", position.row + 1, position.column + 1); + let theme = &theme::current(cx).workspace.status_bar; + let mut text = format!( + "{}{FILE_ROW_COLUMN_DELIMITER}{}", + position.row + 1, + position.column + 1 + ); if self.selected_count > 0 { write!(text, " ({} selected)", self.selected_count).unwrap(); } diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index b2105c1c81..a52647fb55 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -1,10 +1,8 @@ -use std::ops::Range; - use crate::{Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase}; use gpui::{Task, ViewContext}; use language::{Bias, ToOffset}; use project::LocationLink; -use settings::Settings; +use std::ops::Range; use util::TryFutureExt; #[derive(Debug, Default)] @@ -211,7 +209,7 @@ pub fn show_link_definition( }); // Highlight symbol using theme link definition highlight style - let style = cx.global::().theme.editor.link_definition; + let style = theme::current(cx).editor.link_definition; this.highlight_text::( vec![highlight_range], style, @@ -297,6 +295,8 @@ 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 futures::StreamExt; use gpui::{ platform::{self, Modifiers, ModifiersChangedEvent}, @@ -305,12 +305,10 @@ mod tests { use indoc::indoc; use lsp::request::{GotoDefinition, GotoTypeDefinition}; - use crate::test::editor_lsp_test_context::EditorLspTestContext; - - use super::*; - #[gpui::test] async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorLspTestContext::new_rust( lsp::ServerCapabilities { hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), @@ -417,6 +415,8 @@ mod tests { #[gpui::test] async fn test_link_go_to_definition(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorLspTestContext::new_rust( lsp::ServerCapabilities { hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), diff --git a/crates/editor/src/mouse_context_menu.rs b/crates/editor/src/mouse_context_menu.rs index b892fffbca..8dfdcdff53 100644 --- a/crates/editor/src/mouse_context_menu.rs +++ b/crates/editor/src/mouse_context_menu.rs @@ -57,13 +57,14 @@ pub fn deploy_context_menu( #[cfg(test)] mod tests { - use crate::test::editor_lsp_test_context::EditorLspTestContext; - use super::*; + use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext}; use indoc::indoc; #[gpui::test] async fn test_mouse_context_menu(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorLspTestContext::new_rust( lsp::ServerCapabilities { hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 284f0c94bc..6c9bd6cb4f 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -369,11 +369,12 @@ pub fn split_display_range_by_lines( mod tests { use super::*; use crate::{test::marked_display_snapshot, Buffer, DisplayMap, ExcerptRange, MultiBuffer}; - use settings::Settings; + use settings::SettingsStore; #[gpui::test] fn test_previous_word_start(cx: &mut gpui::AppContext) { - cx.set_global(Settings::test(cx)); + init_test(cx); + fn assert(marked_text: &str, cx: &mut gpui::AppContext) { let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); assert_eq!( @@ -400,7 +401,8 @@ mod tests { #[gpui::test] fn test_previous_subword_start(cx: &mut gpui::AppContext) { - cx.set_global(Settings::test(cx)); + init_test(cx); + fn assert(marked_text: &str, cx: &mut gpui::AppContext) { let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); assert_eq!( @@ -434,7 +436,8 @@ mod tests { #[gpui::test] fn test_find_preceding_boundary(cx: &mut gpui::AppContext) { - cx.set_global(Settings::test(cx)); + init_test(cx); + fn assert( marked_text: &str, cx: &mut gpui::AppContext, @@ -466,7 +469,8 @@ mod tests { #[gpui::test] fn test_next_word_end(cx: &mut gpui::AppContext) { - cx.set_global(Settings::test(cx)); + init_test(cx); + fn assert(marked_text: &str, cx: &mut gpui::AppContext) { let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); assert_eq!( @@ -490,7 +494,8 @@ mod tests { #[gpui::test] fn test_next_subword_end(cx: &mut gpui::AppContext) { - cx.set_global(Settings::test(cx)); + init_test(cx); + fn assert(marked_text: &str, cx: &mut gpui::AppContext) { let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); assert_eq!( @@ -523,7 +528,8 @@ mod tests { #[gpui::test] fn test_find_boundary(cx: &mut gpui::AppContext) { - cx.set_global(Settings::test(cx)); + init_test(cx); + fn assert( marked_text: &str, cx: &mut gpui::AppContext, @@ -555,7 +561,8 @@ mod tests { #[gpui::test] fn test_surrounding_word(cx: &mut gpui::AppContext) { - cx.set_global(Settings::test(cx)); + init_test(cx); + fn assert(marked_text: &str, cx: &mut gpui::AppContext) { let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); assert_eq!( @@ -576,7 +583,8 @@ mod tests { #[gpui::test] fn test_move_up_and_down_with_excerpts(cx: &mut gpui::AppContext) { - cx.set_global(Settings::test(cx)); + init_test(cx); + let family_id = cx .font_cache() .load_family(&["Helvetica"], &Default::default()) @@ -691,4 +699,11 @@ mod tests { (DisplayPoint::new(7, 2), SelectionGoal::Column(2)), ); } + + fn init_test(cx: &mut gpui::AppContext) { + cx.set_global(SettingsStore::test(cx)); + theme::init((), cx); + language::init(cx); + crate::init(cx); + } } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index a2160b47e5..1423473e1a 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -9,7 +9,9 @@ use git::diff::DiffHunk; use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task}; pub use language::Completion; use language::{ - char_kind, AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, CursorShape, + char_kind, + language_settings::{language_settings, LanguageSettings}, + AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, CursorShape, DiagnosticEntry, File, IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16, Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _, ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped, @@ -1165,6 +1167,9 @@ impl MultiBuffer { ) { self.sync(cx); let ids = excerpt_ids.into_iter().collect::>(); + if ids.is_empty() { + return; + } let mut buffers = self.buffers.borrow_mut(); let mut snapshot = self.snapshot.borrow_mut(); @@ -1372,6 +1377,15 @@ impl MultiBuffer { .and_then(|(buffer, offset)| buffer.read(cx).language_at(offset)) } + pub fn settings_at<'a, T: ToOffset>( + &self, + point: T, + cx: &'a AppContext, + ) -> &'a LanguageSettings { + let language = self.language_at(point, cx); + language_settings(language.map(|l| l.name()).as_deref(), cx) + } + pub fn for_each_buffer(&self, mut f: impl FnMut(&ModelHandle)) { self.buffers .borrow() @@ -2764,6 +2778,16 @@ impl MultiBufferSnapshot { .and_then(|(buffer, offset)| buffer.language_at(offset)) } + pub fn settings_at<'a, T: ToOffset>( + &'a self, + point: T, + cx: &'a AppContext, + ) -> &'a LanguageSettings { + self.point_to_buffer_offset(point) + .map(|(buffer, offset)| buffer.settings_at(offset, cx)) + .unwrap_or_else(|| language_settings(None, cx)) + } + pub fn language_scope_at<'a, T: ToOffset>(&'a self, point: T) -> Option { self.point_to_buffer_offset(point) .and_then(|(buffer, offset)| buffer.language_scope_at(offset)) @@ -2817,20 +2841,15 @@ impl MultiBufferSnapshot { }) } - pub fn git_diff_hunks_in_range<'a>( + pub fn git_diff_hunks_in_range_rev<'a>( &'a self, row_range: Range, - reversed: bool, ) -> impl 'a + Iterator> { let mut cursor = self.excerpts.cursor::(); - if reversed { - cursor.seek(&Point::new(row_range.end, 0), Bias::Left, &()); - if cursor.item().is_none() { - cursor.prev(&()); - } - } else { - cursor.seek(&Point::new(row_range.start, 0), Bias::Right, &()); + cursor.seek(&Point::new(row_range.end, 0), Bias::Left, &()); + if cursor.item().is_none() { + cursor.prev(&()); } std::iter::from_fn(move || { @@ -2860,7 +2879,7 @@ impl MultiBufferSnapshot { let buffer_hunks = excerpt .buffer - .git_diff_hunks_intersecting_range(buffer_start..buffer_end, reversed) + .git_diff_hunks_intersecting_range_rev(buffer_start..buffer_end) .filter_map(move |hunk| { let start = multibuffer_start.row + hunk @@ -2880,12 +2899,70 @@ impl MultiBufferSnapshot { }) }); - if reversed { - cursor.prev(&()); - } else { - cursor.next(&()); + cursor.prev(&()); + + Some(buffer_hunks) + }) + .flatten() + } + + pub fn git_diff_hunks_in_range<'a>( + &'a self, + row_range: Range, + ) -> impl 'a + Iterator> { + let mut cursor = self.excerpts.cursor::(); + + cursor.seek(&Point::new(row_range.start, 0), Bias::Right, &()); + + std::iter::from_fn(move || { + let excerpt = cursor.item()?; + let multibuffer_start = *cursor.start(); + let multibuffer_end = multibuffer_start + excerpt.text_summary.lines; + if multibuffer_start.row >= row_range.end { + return None; } + let mut buffer_start = excerpt.range.context.start; + let mut buffer_end = excerpt.range.context.end; + let excerpt_start_point = buffer_start.to_point(&excerpt.buffer); + let excerpt_end_point = excerpt_start_point + excerpt.text_summary.lines; + + if row_range.start > multibuffer_start.row { + let buffer_start_point = + excerpt_start_point + Point::new(row_range.start - multibuffer_start.row, 0); + buffer_start = excerpt.buffer.anchor_before(buffer_start_point); + } + + if row_range.end < multibuffer_end.row { + let buffer_end_point = + excerpt_start_point + Point::new(row_range.end - multibuffer_start.row, 0); + buffer_end = excerpt.buffer.anchor_before(buffer_end_point); + } + + let buffer_hunks = excerpt + .buffer + .git_diff_hunks_intersecting_range(buffer_start..buffer_end) + .filter_map(move |hunk| { + let start = multibuffer_start.row + + hunk + .buffer_range + .start + .saturating_sub(excerpt_start_point.row); + let end = multibuffer_start.row + + hunk + .buffer_range + .end + .min(excerpt_end_point.row + 1) + .saturating_sub(excerpt_start_point.row); + + Some(DiffHunk { + buffer_range: start..end, + diff_base_byte_range: hunk.diff_base_byte_range.clone(), + }) + }); + + cursor.next(&()); + Some(buffer_hunks) }) .flatten() @@ -3785,10 +3862,9 @@ mod tests { use gpui::{AppContext, TestAppContext}; use language::{Buffer, Rope}; use rand::prelude::*; - use settings::Settings; + use settings::SettingsStore; use std::{env, rc::Rc}; use unindent::Unindent; - use util::test::sample_text; #[gpui::test] @@ -4080,19 +4156,25 @@ mod tests { let leader_multibuffer = cx.add_model(|_| MultiBuffer::new(0)); let follower_multibuffer = cx.add_model(|_| MultiBuffer::new(0)); + let follower_edit_event_count = Rc::new(RefCell::new(0)); follower_multibuffer.update(cx, |_, cx| { - cx.subscribe(&leader_multibuffer, |follower, _, event, cx| { - match event.clone() { + let follower_edit_event_count = follower_edit_event_count.clone(); + cx.subscribe( + &leader_multibuffer, + move |follower, _, event, cx| match event.clone() { Event::ExcerptsAdded { buffer, predecessor, excerpts, } => follower.insert_excerpts_with_ids_after(predecessor, buffer, excerpts, cx), Event::ExcerptsRemoved { ids } => follower.remove_excerpts(ids, cx), + Event::Edited => { + *follower_edit_event_count.borrow_mut() += 1; + } _ => {} - } - }) + }, + ) .detach(); }); @@ -4131,6 +4213,7 @@ mod tests { leader_multibuffer.read(cx).snapshot(cx).text(), follower_multibuffer.read(cx).snapshot(cx).text(), ); + assert_eq!(*follower_edit_event_count.borrow(), 2); leader_multibuffer.update(cx, |leader, cx| { let excerpt_ids = leader.excerpt_ids(); @@ -4140,6 +4223,27 @@ mod tests { leader_multibuffer.read(cx).snapshot(cx).text(), follower_multibuffer.read(cx).snapshot(cx).text(), ); + assert_eq!(*follower_edit_event_count.borrow(), 3); + + // Removing an empty set of excerpts is a noop. + leader_multibuffer.update(cx, |leader, cx| { + leader.remove_excerpts([], cx); + }); + assert_eq!( + leader_multibuffer.read(cx).snapshot(cx).text(), + follower_multibuffer.read(cx).snapshot(cx).text(), + ); + assert_eq!(*follower_edit_event_count.borrow(), 3); + + // Adding an empty set of excerpts is a noop. + leader_multibuffer.update(cx, |leader, cx| { + leader.push_excerpts::(buffer_2.clone(), [], cx); + }); + assert_eq!( + leader_multibuffer.read(cx).snapshot(cx).text(), + follower_multibuffer.read(cx).snapshot(cx).text(), + ); + assert_eq!(*follower_edit_event_count.borrow(), 3); leader_multibuffer.update(cx, |leader, cx| { leader.clear(cx); @@ -4148,6 +4252,7 @@ mod tests { leader_multibuffer.read(cx).snapshot(cx).text(), follower_multibuffer.read(cx).snapshot(cx).text(), ); + assert_eq!(*follower_edit_event_count.borrow(), 4); } #[gpui::test] @@ -4595,7 +4700,7 @@ mod tests { assert_eq!( snapshot - .git_diff_hunks_in_range(0..12, false) + .git_diff_hunks_in_range(0..12) .map(|hunk| (hunk.status(), hunk.buffer_range)) .collect::>(), &expected, @@ -4603,7 +4708,7 @@ mod tests { assert_eq!( snapshot - .git_diff_hunks_in_range(0..12, true) + .git_diff_hunks_in_range_rev(0..12) .map(|hunk| (hunk.status(), hunk.buffer_range)) .collect::>(), expected @@ -5034,7 +5139,8 @@ mod tests { #[gpui::test] fn test_history(cx: &mut AppContext) { - cx.set_global(Settings::test(cx)); + 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 multibuffer = cx.add_model(|_| MultiBuffer::new(0)); diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index fe9a7909b8..0fe49d4d04 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -34,13 +34,17 @@ impl<'a> EditorLspTestContext<'a> { ) -> EditorLspTestContext<'a> { use json::json; + let app_state = cx.update(AppState::test); + cx.update(|cx| { + theme::init((), cx); + language::init(cx); crate::init(cx); pane::init(cx); + Project::init_settings(cx); + workspace::init_settings(cx); }); - let app_state = cx.update(AppState::test); - let file_name = format!( "file.{}", language diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index 269a2f4f3c..e520562ebb 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -1,19 +1,16 @@ +use crate::{ + display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer, +}; +use futures::Future; +use gpui::{ + keymap_matcher::Keystroke, AppContext, ContextHandle, ModelContext, ViewContext, ViewHandle, +}; +use indoc::indoc; +use language::{Buffer, BufferSnapshot}; use std::{ any::TypeId, ops::{Deref, DerefMut, Range}, }; - -use futures::Future; -use indoc::indoc; - -use crate::{ - display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer, -}; -use gpui::{ - keymap_matcher::Keystroke, AppContext, ContextHandle, ModelContext, ViewContext, ViewHandle, -}; -use language::{Buffer, BufferSnapshot}; -use settings::Settings; use util::{ assert_set_eq, test::{generate_marked_text, marked_text_ranges}, @@ -30,15 +27,10 @@ pub struct EditorTestContext<'a> { impl<'a> EditorTestContext<'a> { pub fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> { let (window_id, editor) = cx.update(|cx| { - cx.set_global(Settings::test(cx)); - crate::init(cx); - - let (window_id, editor) = cx.add_window(Default::default(), |cx| { + cx.add_window(Default::default(), |cx| { cx.focus_self(); build_editor(MultiBuffer::build_simple("", cx), cx) - }); - - (window_id, editor) + }) }); Self { @@ -212,6 +204,7 @@ impl<'a> EditorTestContext<'a> { self.assert_selections(expected_selections, marked_text.to_string()) } + #[track_caller] pub fn assert_editor_background_highlights(&mut self, marked_text: &str) { let expected_ranges = self.ranges(marked_text); let actual_ranges: Vec> = self.update_editor(|editor, cx| { @@ -228,6 +221,7 @@ impl<'a> EditorTestContext<'a> { assert_set_eq!(actual_ranges, expected_ranges); } + #[track_caller] pub fn assert_editor_text_highlights(&mut self, marked_text: &str) { let expected_ranges = self.ranges(marked_text); let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx)); @@ -241,12 +235,14 @@ impl<'a> EditorTestContext<'a> { assert_set_eq!(actual_ranges, expected_ranges); } + #[track_caller] pub fn assert_editor_selections(&mut self, expected_selections: Vec>) { let expected_marked_text = generate_marked_text(&self.buffer_text(), &expected_selections, true); self.assert_selections(expected_selections, expected_marked_text) } + #[track_caller] fn assert_selections( &mut self, expected_selections: Vec>, diff --git a/crates/feedback/Cargo.toml b/crates/feedback/Cargo.toml index e74e14ff4c..ddd6ab0009 100644 --- a/crates/feedback/Cargo.toml +++ b/crates/feedback/Cargo.toml @@ -35,3 +35,6 @@ serde_derive.workspace = true sysinfo = "0.27.1" tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" } urlencoding = "2.1.2" + +[dev-dependencies] +editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/feedback/src/deploy_feedback_button.rs b/crates/feedback/src/deploy_feedback_button.rs index fc90b38750..d32a3e5b4c 100644 --- a/crates/feedback/src/deploy_feedback_button.rs +++ b/crates/feedback/src/deploy_feedback_button.rs @@ -3,7 +3,6 @@ use gpui::{ platform::{CursorStyle, MouseButton}, Entity, View, ViewContext, WeakViewHandle, }; -use settings::Settings; use workspace::{item::ItemHandle, StatusItemView, Workspace}; use crate::feedback_editor::{FeedbackEditor, GiveFeedback}; @@ -33,7 +32,7 @@ impl View for DeployFeedbackButton { fn render(&mut self, cx: &mut ViewContext) -> AnyElement { let active = self.active; - let theme = cx.global::().theme.clone(); + let theme = theme::current(cx).clone(); Stack::new() .with_child( MouseEventHandler::::new(0, cx, |state, _| { diff --git a/crates/feedback/src/feedback_info_text.rs b/crates/feedback/src/feedback_info_text.rs index 9aee4e0e68..5852cd9a61 100644 --- a/crates/feedback/src/feedback_info_text.rs +++ b/crates/feedback/src/feedback_info_text.rs @@ -3,7 +3,6 @@ use gpui::{ platform::{CursorStyle, MouseButton}, AnyElement, Element, Entity, View, ViewContext, ViewHandle, }; -use settings::Settings; use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView}; use crate::{feedback_editor::FeedbackEditor, open_zed_community_repo, OpenZedCommunityRepo}; @@ -30,7 +29,7 @@ impl View for FeedbackInfoText { } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let theme = cx.global::().theme.clone(); + let theme = theme::current(cx).clone(); Flex::row() .with_child( diff --git a/crates/feedback/src/submit_feedback_button.rs b/crates/feedback/src/submit_feedback_button.rs index ccd58c3dc9..56bc235570 100644 --- a/crates/feedback/src/submit_feedback_button.rs +++ b/crates/feedback/src/submit_feedback_button.rs @@ -5,7 +5,6 @@ use gpui::{ platform::{CursorStyle, MouseButton}, AnyElement, AppContext, Element, Entity, Task, View, ViewContext, ViewHandle, }; -use settings::Settings; use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView}; pub fn init(cx: &mut AppContext) { @@ -46,7 +45,7 @@ impl View for SubmitFeedbackButton { } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let theme = cx.global::().theme.clone(); + let theme = theme::current(cx).clone(); enum SubmitFeedbackButton {} MouseEventHandler::::new(0, cx, |state, _| { let style = theme.feedback.submit_button.style_for(state, false); diff --git a/crates/file_finder/Cargo.toml b/crates/file_finder/Cargo.toml index 30a4650ad7..6f6be7427b 100644 --- a/crates/file_finder/Cargo.toml +++ b/crates/file_finder/Cargo.toml @@ -16,14 +16,19 @@ menu = { path = "../menu" } picker = { path = "../picker" } project = { path = "../project" } settings = { path = "../settings" } +text = { path = "../text" } util = { path = "../util" } theme = { path = "../theme" } workspace = { path = "../workspace" } postage.workspace = true [dev-dependencies] +editor = { path = "../editor", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } -serde_json.workspace = true +language = { path = "../language", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] } +theme = { path = "../theme", features = ["test-support"] } + +serde_json.workspace = true ctor.workspace = true env_logger.workspace = true diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index f00430feb7..6d2ba115b7 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -1,10 +1,10 @@ +use editor::{scroll::autoscroll::Autoscroll, Bias, Editor}; use fuzzy::PathMatch; use gpui::{ actions, elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext, WeakViewHandle, }; use picker::{Picker, PickerDelegate}; use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId}; -use settings::Settings; use std::{ path::Path, sync::{ @@ -12,7 +12,8 @@ use std::{ Arc, }, }; -use util::{post_inc, ResultExt}; +use text::Point; +use util::{paths::PathLikeWithPosition, post_inc, ResultExt}; use workspace::Workspace; pub type FileFinder = Picker; @@ -23,11 +24,12 @@ pub struct FileFinderDelegate { search_count: usize, latest_search_id: usize, latest_search_did_cancel: bool, - latest_search_query: String, - relative_to: Option>, + latest_search_query: Option>, + currently_opened_path: Option, matches: Vec, selected: Option<(usize, Arc)>, cancel_flag: Arc, + history_items: Vec, } actions!(file_finder, [Toggle]); @@ -37,17 +39,26 @@ pub fn init(cx: &mut AppContext) { FileFinder::init(cx); } +const MAX_RECENT_SELECTIONS: usize = 20; + fn toggle_file_finder(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { workspace.toggle_modal(cx, |workspace, cx| { - let relative_to = workspace + let history_items = workspace.recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx); + let currently_opened_path = workspace .active_item(cx) - .and_then(|item| item.project_path(cx)) - .map(|project_path| project_path.path.clone()); + .and_then(|item| item.project_path(cx)); + let project = workspace.project().clone(); let workspace = cx.handle().downgrade(); let finder = cx.add_view(|cx| { Picker::new( - FileFinderDelegate::new(workspace, project, relative_to, cx), + FileFinderDelegate::new( + workspace, + project, + currently_opened_path, + history_items, + cx, + ), cx, ) }); @@ -60,6 +71,21 @@ pub enum Event { Dismissed, } +#[derive(Debug, Clone)] +struct FileSearchQuery { + raw_query: String, + file_query_end: Option, +} + +impl FileSearchQuery { + fn path_query(&self) -> &str { + match self.file_query_end { + Some(file_path_end) => &self.raw_query[..file_path_end], + None => &self.raw_query, + } + } +} + impl FileFinderDelegate { fn labels_for_match(&self, path_match: &PathMatch) -> (String, Vec, String, Vec) { let path = &path_match.path; @@ -90,7 +116,8 @@ impl FileFinderDelegate { pub fn new( workspace: WeakViewHandle, project: ModelHandle, - relative_to: Option>, + currently_opened_path: Option, + history_items: Vec, cx: &mut ViewContext, ) -> Self { cx.observe(&project, |picker, _, cx| { @@ -103,16 +130,24 @@ impl FileFinderDelegate { search_count: 0, latest_search_id: 0, latest_search_did_cancel: false, - latest_search_query: String::new(), - relative_to, + latest_search_query: None, + currently_opened_path, matches: Vec::new(), selected: None, cancel_flag: Arc::new(AtomicBool::new(false)), + history_items, } } - fn spawn_search(&mut self, query: String, cx: &mut ViewContext) -> Task<()> { - let relative_to = self.relative_to.clone(); + fn spawn_search( + &mut self, + query: PathLikeWithPosition, + cx: &mut ViewContext, + ) -> Task<()> { + let relative_to = self + .currently_opened_path + .as_ref() + .map(|project_path| Arc::clone(&project_path.path)); let worktrees = self .project .read(cx) @@ -140,7 +175,7 @@ impl FileFinderDelegate { cx.spawn(|picker, mut cx| async move { let matches = fuzzy::match_path_sets( candidate_sets.as_slice(), - &query, + query.path_like.path_query(), relative_to, false, 100, @@ -163,18 +198,24 @@ impl FileFinderDelegate { &mut self, search_id: usize, did_cancel: bool, - query: String, + query: PathLikeWithPosition, matches: Vec, cx: &mut ViewContext, ) { if search_id >= self.latest_search_id { self.latest_search_id = search_id; - if self.latest_search_did_cancel && query == self.latest_search_query { + if self.latest_search_did_cancel + && Some(query.path_like.path_query()) + == self + .latest_search_query + .as_ref() + .map(|query| query.path_like.path_query()) + { util::extend_sorted(&mut self.matches, matches.into_iter(), 100, |a, b| b.cmp(a)); } else { self.matches = matches; } - self.latest_search_query = query; + self.latest_search_query = Some(query); self.latest_search_did_cancel = did_cancel; cx.notify(); } @@ -209,13 +250,42 @@ impl PickerDelegate for FileFinderDelegate { cx.notify(); } - fn update_matches(&mut self, query: String, cx: &mut ViewContext) -> Task<()> { - if query.is_empty() { + fn update_matches(&mut self, raw_query: String, cx: &mut ViewContext) -> Task<()> { + if raw_query.is_empty() { self.latest_search_id = post_inc(&mut self.search_count); self.matches.clear(); + + self.matches = self + .currently_opened_path + .iter() // if exists, bubble the currently opened path to the top + .chain(self.history_items.iter().filter(|history_item| { + Some(*history_item) != self.currently_opened_path.as_ref() + })) + .enumerate() + .map(|(i, history_item)| PathMatch { + score: i as f64, + positions: Vec::new(), + worktree_id: history_item.worktree_id.to_usize(), + path: Arc::clone(&history_item.path), + path_prefix: "".into(), + distance_to_relative_ancestor: usize::MAX, + }) + .collect(); cx.notify(); Task::ready(()) } else { + let raw_query = &raw_query; + let query = PathLikeWithPosition::parse_str(raw_query, |path_like_str| { + Ok::<_, std::convert::Infallible>(FileSearchQuery { + raw_query: raw_query.to_owned(), + file_query_end: if path_like_str == raw_query { + None + } else { + Some(path_like_str.len()) + }, + }) + }) + .expect("infallible"); self.spawn_search(query, cx) } } @@ -227,13 +297,48 @@ impl PickerDelegate for FileFinderDelegate { worktree_id: WorktreeId::from_usize(m.worktree_id), path: m.path.clone(), }; + let open_task = workspace.update(cx, |workspace, cx| { + workspace.open_path(project_path.clone(), None, true, cx) + }); - workspace.update(cx, |workspace, cx| { + let workspace = workspace.downgrade(); + + let row = self + .latest_search_query + .as_ref() + .and_then(|query| query.row) + .map(|row| row.saturating_sub(1)); + let col = self + .latest_search_query + .as_ref() + .and_then(|query| query.column) + .unwrap_or(0) + .saturating_sub(1); + cx.spawn(|_, mut cx| async move { + let item = open_task.await.log_err()?; + if let Some(row) = row { + if let Some(active_editor) = item.downcast::() { + active_editor + .downgrade() + .update(&mut cx, |editor, cx| { + let snapshot = editor.snapshot(cx).display_snapshot; + let point = snapshot + .buffer_snapshot + .clip_point(Point::new(row, col), Bias::Left); + editor.change_selections(Some(Autoscroll::center()), cx, |s| { + s.select_ranges([point..point]) + }); + }) + .log_err(); + } + } workspace - .open_path(project_path.clone(), None, true, cx) - .detach_and_log_err(cx); - workspace.dismiss_modal(cx); + .update(&mut cx, |workspace, cx| workspace.dismiss_modal(cx)) + .log_err(); + + Some(()) }) + .detach(); } } } @@ -248,8 +353,8 @@ impl PickerDelegate for FileFinderDelegate { cx: &AppContext, ) -> AnyElement> { let path_match = &self.matches[ix]; - let settings = cx.global::(); - let style = settings.theme.picker.item.style_for(mouse_state, selected); + let theme = theme::current(cx); + let style = theme.picker.item.style_for(mouse_state, selected); let (file_name, file_name_positions, full_path, full_path_positions) = self.labels_for_match(path_match); Flex::column() @@ -268,8 +373,11 @@ impl PickerDelegate for FileFinderDelegate { #[cfg(test)] mod tests { + use std::{assert_eq, collections::HashMap, time::Duration}; + use super::*; use editor::Editor; + use gpui::{TestAppContext, ViewHandle}; use menu::{Confirm, SelectNext}; use serde_json::json; use workspace::{AppState, Workspace}; @@ -282,13 +390,8 @@ mod tests { } #[gpui::test] - async fn test_matching_paths(cx: &mut gpui::TestAppContext) { - let app_state = cx.update(|cx| { - super::init(cx); - editor::init(cx); - AppState::test(cx) - }); - + async fn test_matching_paths(cx: &mut TestAppContext) { + let app_state = init_test(cx); app_state .fs .as_fake() @@ -338,8 +441,174 @@ mod tests { } #[gpui::test] - async fn test_matching_cancellation(cx: &mut gpui::TestAppContext) { - let app_state = cx.update(AppState::test); + async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) { + let app_state = init_test(cx); + + let first_file_name = "first.rs"; + let first_file_contents = "// First Rust file"; + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + "test": { + first_file_name: first_file_contents, + "second.rs": "// Second Rust file", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); + cx.dispatch_action(window_id, Toggle); + let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); + + let file_query = &first_file_name[..3]; + let file_row = 1; + let file_column = 3; + assert!(file_column <= first_file_contents.len()); + let query_inside_file = format!("{file_query}:{file_row}:{file_column}"); + finder + .update(cx, |finder, cx| { + finder + .delegate_mut() + .update_matches(query_inside_file.to_string(), cx) + }) + .await; + finder.read_with(cx, |finder, _| { + let finder = finder.delegate(); + assert_eq!(finder.matches.len(), 1); + let latest_search_query = finder + .latest_search_query + .as_ref() + .expect("Finder should have a query after the update_matches call"); + assert_eq!(latest_search_query.path_like.raw_query, query_inside_file); + assert_eq!( + latest_search_query.path_like.file_query_end, + Some(file_query.len()) + ); + assert_eq!(latest_search_query.row, Some(file_row)); + assert_eq!(latest_search_query.column, Some(file_column as u32)); + }); + + let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); + cx.dispatch_action(window_id, SelectNext); + cx.dispatch_action(window_id, Confirm); + active_pane + .condition(cx, |pane, _| pane.active_item().is_some()) + .await; + let editor = cx.update(|cx| { + let active_item = active_pane.read(cx).active_item().unwrap(); + active_item.downcast::().unwrap() + }); + cx.foreground().advance_clock(Duration::from_secs(2)); + cx.foreground().start_waiting(); + cx.foreground().finish_waiting(); + editor.update(cx, |editor, cx| { + let all_selections = editor.selections.all_adjusted(cx); + assert_eq!( + all_selections.len(), + 1, + "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}" + ); + let caret_selection = all_selections.into_iter().next().unwrap(); + assert_eq!(caret_selection.start, caret_selection.end, + "Caret selection should have its start and end at the same position"); + assert_eq!(file_row, caret_selection.start.row + 1, + "Query inside file should get caret with the same focus row"); + assert_eq!(file_column, caret_selection.start.column as usize + 1, + "Query inside file should get caret with the same focus column"); + }); + } + + #[gpui::test] + async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) { + let app_state = init_test(cx); + + let first_file_name = "first.rs"; + let first_file_contents = "// First Rust file"; + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + "test": { + first_file_name: first_file_contents, + "second.rs": "// Second Rust file", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); + cx.dispatch_action(window_id, Toggle); + let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); + + let file_query = &first_file_name[..3]; + let file_row = 200; + let file_column = 300; + assert!(file_column > first_file_contents.len()); + let query_outside_file = format!("{file_query}:{file_row}:{file_column}"); + finder + .update(cx, |finder, cx| { + finder + .delegate_mut() + .update_matches(query_outside_file.to_string(), cx) + }) + .await; + finder.read_with(cx, |finder, _| { + let finder = finder.delegate(); + assert_eq!(finder.matches.len(), 1); + let latest_search_query = finder + .latest_search_query + .as_ref() + .expect("Finder should have a query after the update_matches call"); + assert_eq!(latest_search_query.path_like.raw_query, query_outside_file); + assert_eq!( + latest_search_query.path_like.file_query_end, + Some(file_query.len()) + ); + assert_eq!(latest_search_query.row, Some(file_row)); + assert_eq!(latest_search_query.column, Some(file_column as u32)); + }); + + let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); + cx.dispatch_action(window_id, SelectNext); + cx.dispatch_action(window_id, Confirm); + active_pane + .condition(cx, |pane, _| pane.active_item().is_some()) + .await; + let editor = cx.update(|cx| { + let active_item = active_pane.read(cx).active_item().unwrap(); + active_item.downcast::().unwrap() + }); + cx.foreground().advance_clock(Duration::from_secs(2)); + cx.foreground().start_waiting(); + cx.foreground().finish_waiting(); + editor.update(cx, |editor, cx| { + let all_selections = editor.selections.all_adjusted(cx); + assert_eq!( + all_selections.len(), + 1, + "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}" + ); + let caret_selection = all_selections.into_iter().next().unwrap(); + assert_eq!(caret_selection.start, caret_selection.end, + "Caret selection should have its start and end at the same position"); + assert_eq!(0, caret_selection.start.row, + "Excessive rows (as in query outside file borders) should get trimmed to last file row"); + assert_eq!(first_file_contents.len(), caret_selection.start.column as usize, + "Excessive columns (as in query outside file borders) should get trimmed to selected row's last column"); + }); + } + + #[gpui::test] + async fn test_matching_cancellation(cx: &mut TestAppContext) { + let app_state = init_test(cx); app_state .fs .as_fake() @@ -365,13 +634,14 @@ mod tests { workspace.downgrade(), workspace.read(cx).project().clone(), None, + Vec::new(), cx, ), cx, ) }); - let query = "hi".to_string(); + let query = test_path_like("hi"); finder .update(cx, |f, cx| f.delegate_mut().spawn_search(query.clone(), cx)) .await; @@ -407,8 +677,8 @@ mod tests { } #[gpui::test] - async fn test_ignored_files(cx: &mut gpui::TestAppContext) { - let app_state = cx.update(AppState::test); + async fn test_ignored_files(cx: &mut TestAppContext) { + let app_state = init_test(cx); app_state .fs .as_fake() @@ -449,20 +719,23 @@ mod tests { workspace.downgrade(), workspace.read(cx).project().clone(), None, + Vec::new(), cx, ), cx, ) }); finder - .update(cx, |f, cx| f.delegate_mut().spawn_search("hi".into(), cx)) + .update(cx, |f, cx| { + f.delegate_mut().spawn_search(test_path_like("hi"), cx) + }) .await; finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 7)); } #[gpui::test] - async fn test_single_file_worktrees(cx: &mut gpui::TestAppContext) { - let app_state = cx.update(AppState::test); + async fn test_single_file_worktrees(cx: &mut TestAppContext) { + let app_state = init_test(cx); app_state .fs .as_fake() @@ -482,6 +755,7 @@ mod tests { workspace.downgrade(), workspace.read(cx).project().clone(), None, + Vec::new(), cx, ), cx, @@ -491,7 +765,9 @@ mod tests { // Even though there is only one worktree, that worktree's filename // is included in the matching, because the worktree is a single file. finder - .update(cx, |f, cx| f.delegate_mut().spawn_search("thf".into(), cx)) + .update(cx, |f, cx| { + f.delegate_mut().spawn_search(test_path_like("thf"), cx) + }) .await; cx.read(|cx| { let finder = finder.read(cx); @@ -509,16 +785,16 @@ mod tests { // Since the worktree root is a file, searching for its name followed by a slash does // not match anything. finder - .update(cx, |f, cx| f.delegate_mut().spawn_search("thf/".into(), cx)) + .update(cx, |f, cx| { + f.delegate_mut().spawn_search(test_path_like("thf/"), cx) + }) .await; finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 0)); } #[gpui::test] - async fn test_multiple_matches_with_same_relative_path(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); - - let app_state = cx.update(AppState::test); + async fn test_multiple_matches_with_same_relative_path(cx: &mut TestAppContext) { + let app_state = init_test(cx); app_state .fs .as_fake() @@ -545,6 +821,7 @@ mod tests { workspace.downgrade(), workspace.read(cx).project().clone(), None, + Vec::new(), cx, ), cx, @@ -553,7 +830,9 @@ mod tests { // Run a search that matches two files with the same relative path. finder - .update(cx, |f, cx| f.delegate_mut().spawn_search("a.t".into(), cx)) + .update(cx, |f, cx| { + f.delegate_mut().spawn_search(test_path_like("a.t"), cx) + }) .await; // Can switch between different matches with the same relative path. @@ -569,10 +848,8 @@ mod tests { } #[gpui::test] - async fn test_path_distance_ordering(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); - - let app_state = cx.update(AppState::test); + async fn test_path_distance_ordering(cx: &mut TestAppContext) { + let app_state = init_test(cx); app_state .fs .as_fake() @@ -590,17 +867,26 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); + let worktree_id = cx.read(|cx| { + let worktrees = workspace.read(cx).worktrees(cx).collect::>(); + assert_eq!(worktrees.len(), 1); + WorktreeId::from_usize(worktrees[0].id()) + }); // When workspace has an active item, sort items which are closer to that item // first when they have the same name. In this case, b.txt is closer to dir2's a.txt // so that one should be sorted earlier - let b_path = Some(Arc::from(Path::new("/root/dir2/b.txt"))); + let b_path = Some(ProjectPath { + worktree_id, + path: Arc::from(Path::new("/root/dir2/b.txt")), + }); let (_, finder) = cx.add_window(|cx| { Picker::new( FileFinderDelegate::new( workspace.downgrade(), workspace.read(cx).project().clone(), b_path, + Vec::new(), cx, ), cx, @@ -609,7 +895,7 @@ mod tests { finder .update(cx, |f, cx| { - f.delegate_mut().spawn_search("a.txt".into(), cx) + f.delegate_mut().spawn_search(test_path_like("a.txt"), cx) }) .await; @@ -621,8 +907,8 @@ mod tests { } #[gpui::test] - async fn test_search_worktree_without_files(cx: &mut gpui::TestAppContext) { - let app_state = cx.update(AppState::test); + async fn test_search_worktree_without_files(cx: &mut TestAppContext) { + let app_state = init_test(cx); app_state .fs .as_fake() @@ -645,17 +931,288 @@ mod tests { workspace.downgrade(), workspace.read(cx).project().clone(), None, + Vec::new(), cx, ), cx, ) }); finder - .update(cx, |f, cx| f.delegate_mut().spawn_search("dir".into(), cx)) + .update(cx, |f, cx| { + f.delegate_mut().spawn_search(test_path_like("dir"), cx) + }) .await; cx.read(|cx| { let finder = finder.read(cx); assert_eq!(finder.delegate().matches.len(), 0); }); } + + #[gpui::test] + async fn test_query_history( + deterministic: Arc, + cx: &mut gpui::TestAppContext, + ) { + let app_state = init_test(cx); + + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + "test": { + "first.rs": "// First Rust file", + "second.rs": "// Second Rust file", + "third.rs": "// Third Rust file", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); + let worktree_id = cx.read(|cx| { + let worktrees = workspace.read(cx).worktrees(cx).collect::>(); + assert_eq!(worktrees.len(), 1); + WorktreeId::from_usize(worktrees[0].id()) + }); + + // Open and close panels, getting their history items afterwards. + // Ensure history items get populated with opened items, and items are kept in a certain order. + // The history lags one opened buffer behind, since it's updated in the search panel only on its reopen. + // + // TODO: without closing, the opened items do not propagate their history changes for some reason + // it does work in real app though, only tests do not propagate. + + let initial_history = open_close_queried_buffer( + "fir", + 1, + "first.rs", + window_id, + &workspace, + &deterministic, + cx, + ) + .await; + assert!( + initial_history.is_empty(), + "Should have no history before opening any files" + ); + + let history_after_first = open_close_queried_buffer( + "sec", + 1, + "second.rs", + window_id, + &workspace, + &deterministic, + cx, + ) + .await; + assert_eq!( + history_after_first, + vec![ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/first.rs")), + }], + "Should show 1st opened item in the history when opening the 2nd item" + ); + + let history_after_second = open_close_queried_buffer( + "thi", + 1, + "third.rs", + window_id, + &workspace, + &deterministic, + cx, + ) + .await; + assert_eq!( + history_after_second, + vec![ + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/second.rs")), + }, + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/first.rs")), + }, + ], + "Should show 1st and 2nd opened items in the history when opening the 3rd item. \ +2nd item should be the first in the history, as the last opened." + ); + + let history_after_third = open_close_queried_buffer( + "sec", + 1, + "second.rs", + window_id, + &workspace, + &deterministic, + cx, + ) + .await; + assert_eq!( + history_after_third, + vec![ + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/third.rs")), + }, + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/second.rs")), + }, + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/first.rs")), + }, + ], + "Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \ +3rd item should be the first in the history, as the last opened." + ); + + let history_after_second_again = open_close_queried_buffer( + "thi", + 1, + "third.rs", + window_id, + &workspace, + &deterministic, + cx, + ) + .await; + assert_eq!( + history_after_second_again, + vec![ + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/second.rs")), + }, + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/third.rs")), + }, + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/first.rs")), + }, + ], + "Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \ +2nd item, as the last opened, 3rd item should go next as it was opened right before." + ); + } + + async fn open_close_queried_buffer( + input: &str, + expected_matches: usize, + expected_editor_title: &str, + window_id: usize, + workspace: &ViewHandle, + deterministic: &gpui::executor::Deterministic, + cx: &mut gpui::TestAppContext, + ) -> Vec { + cx.dispatch_action(window_id, Toggle); + let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); + finder + .update(cx, |finder, cx| { + finder.delegate_mut().update_matches(input.to_string(), cx) + }) + .await; + let history_items = finder.read_with(cx, |finder, _| { + assert_eq!( + finder.delegate().matches.len(), + expected_matches, + "Unexpected number of matches found for query {input}" + ); + finder.delegate().history_items.clone() + }); + + let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); + cx.dispatch_action(window_id, SelectNext); + cx.dispatch_action(window_id, Confirm); + deterministic.run_until_parked(); + active_pane + .condition(cx, |pane, _| pane.active_item().is_some()) + .await; + cx.read(|cx| { + let active_item = active_pane.read(cx).active_item().unwrap(); + let active_editor_title = active_item + .as_any() + .downcast_ref::() + .unwrap() + .read(cx) + .title(cx); + assert_eq!( + expected_editor_title, active_editor_title, + "Unexpected editor title for query {input}" + ); + }); + + let mut original_items = HashMap::new(); + cx.read(|cx| { + for pane in workspace.read(cx).panes() { + let pane_id = pane.id(); + let pane = pane.read(cx); + let insertion_result = original_items.insert(pane_id, pane.items().count()); + assert!(insertion_result.is_none(), "Pane id {pane_id} collision"); + } + }); + active_pane + .update(cx, |pane, cx| { + pane.close_active_item(&workspace::CloseActiveItem, cx) + .unwrap() + }) + .await + .unwrap(); + deterministic.run_until_parked(); + cx.read(|cx| { + for pane in workspace.read(cx).panes() { + let pane_id = pane.id(); + let pane = pane.read(cx); + match original_items.remove(&pane_id) { + Some(original_items) => { + assert_eq!( + pane.items().count(), + original_items.saturating_sub(1), + "Pane id {pane_id} should have item closed" + ); + } + None => panic!("Pane id {pane_id} not found in original items"), + } + } + }); + + history_items + } + + fn init_test(cx: &mut TestAppContext) -> Arc { + cx.foreground().forbid_parking(); + cx.update(|cx| { + let state = AppState::test(cx); + theme::init((), cx); + language::init(cx); + super::init(cx); + editor::init(cx); + workspace::init_settings(cx); + state + }) + } + + fn test_path_like(test_str: &str) -> PathLikeWithPosition { + PathLikeWithPosition::parse_str(test_str, |path_like_str| { + Ok::<_, std::convert::Infallible>(FileSearchQuery { + raw_query: test_str.to_owned(), + file_query_end: if path_like_str == test_str { + None + } else { + Some(path_like_str.len()) + }, + }) + }) + .unwrap() + } } diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index d080fe3cd1..54c6ce362a 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -13,6 +13,7 @@ gpui = { path = "../gpui" } lsp = { path = "../lsp" } rope = { path = "../rope" } util = { path = "../util" } +sum_tree = { path = "../sum_tree" } anyhow.workspace = true async-trait.workspace = true futures.workspace = true diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 945ffaea16..99562405b5 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -27,7 +27,7 @@ use util::ResultExt; #[cfg(any(test, feature = "test-support"))] use collections::{btree_map, BTreeMap}; #[cfg(any(test, feature = "test-support"))] -use repository::FakeGitRepositoryState; +use repository::{FakeGitRepositoryState, GitFileStatus}; #[cfg(any(test, feature = "test-support"))] use std::sync::Weak; @@ -572,15 +572,15 @@ impl FakeFs { Ok(()) } - pub async fn pause_events(&self) { + pub fn pause_events(&self) { self.state.lock().events_paused = true; } - pub async fn buffered_event_count(&self) -> usize { + pub fn buffered_event_count(&self) -> usize { self.state.lock().buffered_events.len() } - pub async fn flush_events(&self, count: usize) { + pub fn flush_events(&self, count: usize) { self.state.lock().flush_events(count); } @@ -654,6 +654,17 @@ impl FakeFs { }); } + pub async fn set_status_for_repo(&self, dot_git: &Path, statuses: &[(&Path, GitFileStatus)]) { + self.with_git_state(dot_git, |state| { + state.worktree_statuses.clear(); + state.worktree_statuses.extend( + statuses + .iter() + .map(|(path, content)| ((**path).into(), content.clone())), + ); + }); + } + pub fn paths(&self) -> Vec { let mut result = Vec::new(); let mut queue = collections::VecDeque::new(); @@ -821,14 +832,16 @@ impl Fs for FakeFs { let old_path = normalize_path(old_path); let new_path = normalize_path(new_path); + let mut state = self.state.lock(); let moved_entry = state.write_path(&old_path, |e| { if let btree_map::Entry::Occupied(e) = e { - Ok(e.remove()) + Ok(e.get().clone()) } else { Err(anyhow!("path does not exist: {}", &old_path.display())) } })?; + state.write_path(&new_path, |e| { match e { btree_map::Entry::Occupied(mut e) => { @@ -844,6 +857,17 @@ impl Fs for FakeFs { } Ok(()) })?; + + state + .write_path(&old_path, |e| { + if let btree_map::Entry::Occupied(e) = e { + Ok(e.remove()) + } else { + unreachable!() + } + }) + .unwrap(); + state.emit_event(&[old_path, new_path]); Ok(()) } diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index 5624ce42f1..2c309351fc 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -1,10 +1,15 @@ use anyhow::Result; use collections::HashMap; use parking_lot::Mutex; +use serde_derive::{Deserialize, Serialize}; use std::{ + cmp::Ordering, + ffi::OsStr, + os::unix::prelude::OsStrExt, path::{Component, Path, PathBuf}, sync::Arc, }; +use sum_tree::{MapSeekTarget, TreeMap}; use util::ResultExt; pub use git2::Repository as LibGitRepository; @@ -16,6 +21,10 @@ pub trait GitRepository: Send { fn load_index_text(&self, relative_file_path: &Path) -> Option; fn branch_name(&self) -> Option; + + fn statuses(&self) -> Option>; + + fn status(&self, path: &RepoPath) -> Option; } impl std::fmt::Debug for dyn GitRepository { @@ -61,6 +70,48 @@ impl GitRepository for LibGitRepository { let branch = String::from_utf8_lossy(head.shorthand_bytes()); Some(branch.to_string()) } + + fn statuses(&self) -> Option> { + let statuses = self.statuses(None).log_err()?; + + let mut map = TreeMap::default(); + + for status in statuses + .iter() + .filter(|status| !status.status().contains(git2::Status::IGNORED)) + { + let path = RepoPath(PathBuf::from(OsStr::from_bytes(status.path_bytes()))); + let Some(status) = read_status(status.status()) else { + continue + }; + + map.insert(path, status) + } + + Some(map) + } + + fn status(&self, path: &RepoPath) -> Option { + let status = self.status_file(path).log_err()?; + read_status(status) + } +} + +fn read_status(status: git2::Status) -> Option { + if status.contains(git2::Status::CONFLICTED) { + Some(GitFileStatus::Conflict) + } else if status.intersects( + git2::Status::WT_MODIFIED + | git2::Status::WT_RENAMED + | git2::Status::INDEX_MODIFIED + | git2::Status::INDEX_RENAMED, + ) { + Some(GitFileStatus::Modified) + } else if status.intersects(git2::Status::WT_NEW | git2::Status::INDEX_NEW) { + Some(GitFileStatus::Added) + } else { + None + } } #[derive(Debug, Clone, Default)] @@ -71,6 +122,7 @@ pub struct FakeGitRepository { #[derive(Debug, Clone, Default)] pub struct FakeGitRepositoryState { pub index_contents: HashMap, + pub worktree_statuses: HashMap, pub branch_name: Option, } @@ -93,6 +145,20 @@ impl GitRepository for FakeGitRepository { let state = self.state.lock(); state.branch_name.clone() } + + fn statuses(&self) -> Option> { + let state = self.state.lock(); + let mut map = TreeMap::default(); + for (repo_path, status) in state.worktree_statuses.iter() { + map.insert(repo_path.to_owned(), status.to_owned()); + } + Some(map) + } + + fn status(&self, path: &RepoPath) -> Option { + let state = self.state.lock(); + state.worktree_statuses.get(path).cloned() + } } fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> { @@ -123,3 +189,66 @@ fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> { _ => Ok(()), } } + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum GitFileStatus { + Added, + Modified, + Conflict, +} + +#[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)] +pub struct RepoPath(PathBuf); + +impl RepoPath { + pub fn new(path: PathBuf) -> Self { + debug_assert!(path.is_relative(), "Repo paths must be relative"); + + RepoPath(path) + } +} + +impl From<&Path> for RepoPath { + fn from(value: &Path) -> Self { + RepoPath::new(value.to_path_buf()) + } +} + +impl From for RepoPath { + fn from(value: PathBuf) -> Self { + RepoPath::new(value) + } +} + +impl Default for RepoPath { + fn default() -> Self { + RepoPath(PathBuf::new()) + } +} + +impl AsRef for RepoPath { + fn as_ref(&self) -> &Path { + self.0.as_ref() + } +} + +impl std::ops::Deref for RepoPath { + type Target = PathBuf; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Debug)] +pub struct RepoPathDescendants<'a>(pub &'a Path); + +impl<'a> MapSeekTarget for RepoPathDescendants<'a> { + fn cmp_cursor(&self, key: &RepoPath) -> Ordering { + if key.starts_with(&self.0) { + Ordering::Greater + } else { + self.0.cmp(key) + } + } +} diff --git a/crates/git/src/diff.rs b/crates/git/src/diff.rs index b28af26f16..8704f85005 100644 --- a/crates/git/src/diff.rs +++ b/crates/git/src/diff.rs @@ -1,4 +1,4 @@ -use std::ops::Range; +use std::{iter, ops::Range}; use sum_tree::SumTree; use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point}; @@ -75,18 +75,58 @@ impl BufferDiff { &'a self, range: Range, buffer: &'a BufferSnapshot, - reversed: bool, ) -> impl 'a + Iterator> { let start = buffer.anchor_before(Point::new(range.start, 0)); let end = buffer.anchor_after(Point::new(range.end, 0)); - self.hunks_intersecting_range(start..end, buffer, reversed) + + self.hunks_intersecting_range(start..end, buffer) } pub fn hunks_intersecting_range<'a>( &'a self, range: Range, buffer: &'a BufferSnapshot, - reversed: bool, + ) -> impl 'a + Iterator> { + let mut cursor = self.tree.filter::<_, DiffHunkSummary>(move |summary| { + let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt(); + let after_end = summary.buffer_range.start.cmp(&range.end, buffer).is_gt(); + !before_start && !after_end + }); + + let anchor_iter = std::iter::from_fn(move || { + cursor.next(buffer); + cursor.item() + }) + .flat_map(move |hunk| { + [ + (&hunk.buffer_range.start, hunk.diff_base_byte_range.start), + (&hunk.buffer_range.end, hunk.diff_base_byte_range.end), + ] + .into_iter() + }); + + let mut summaries = buffer.summaries_for_anchors_with_payload::(anchor_iter); + iter::from_fn(move || { + let (start_point, start_base) = summaries.next()?; + let (end_point, end_base) = summaries.next()?; + + let end_row = if end_point.column > 0 { + end_point.row + 1 + } else { + end_point.row + }; + + Some(DiffHunk { + buffer_range: start_point.row..end_row, + diff_base_byte_range: start_base..end_base, + }) + }) + } + + pub fn hunks_intersecting_range_rev<'a>( + &'a self, + range: Range, + buffer: &'a BufferSnapshot, ) -> impl 'a + Iterator> { let mut cursor = self.tree.filter::<_, DiffHunkSummary>(move |summary| { let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt(); @@ -95,14 +135,9 @@ impl BufferDiff { }); std::iter::from_fn(move || { - if reversed { - cursor.prev(buffer); - } else { - cursor.next(buffer); - } + cursor.prev(buffer); let hunk = cursor.item()?; - let range = hunk.buffer_range.to_point(buffer); let end_row = if range.end.column > 0 { range.end.row + 1 @@ -151,7 +186,7 @@ impl BufferDiff { fn hunks<'a>(&'a self, text: &'a BufferSnapshot) -> impl 'a + Iterator> { let start = text.anchor_before(Point::new(0, 0)); let end = text.anchor_after(Point::new(u32::MAX, u32::MAX)); - self.hunks_intersecting_range(start..end, text, false) + self.hunks_intersecting_range(start..end, text) } fn diff<'a>(head: &'a str, current: &'a str) -> Option> { @@ -279,6 +314,8 @@ pub fn assert_hunks( #[cfg(test)] mod tests { + use std::assert_eq; + use super::*; use text::Buffer; use unindent::Unindent as _; @@ -365,7 +402,7 @@ mod tests { assert_eq!(diff.hunks(&buffer).count(), 8); assert_hunks( - diff.hunks_in_row_range(7..12, &buffer, false), + diff.hunks_in_row_range(7..12, &buffer), &buffer, &diff_base, &[ diff --git a/crates/go_to_line/Cargo.toml b/crates/go_to_line/Cargo.toml index f279aca569..b32b4aaf13 100644 --- a/crates/go_to_line/Cargo.toml +++ b/crates/go_to_line/Cargo.toml @@ -16,3 +16,8 @@ settings = { path = "../settings" } text = { path = "../text" } workspace = { path = "../workspace" } postage.workspace = true +theme = { path = "../theme" } +util = { path = "../util" } + +[dev-dependencies] +editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index 90287e9270..0b41ee6dca 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -1,13 +1,13 @@ use std::sync::Arc; -use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, DisplayPoint, Editor}; +use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor}; use gpui::{ actions, elements::*, geometry::vector::Vector2F, AnyViewHandle, AppContext, Axis, Entity, View, ViewContext, ViewHandle, }; use menu::{Cancel, Confirm}; -use settings::Settings; use text::{Bias, Point}; +use util::paths::FILE_ROW_COLUMN_DELIMITER; use workspace::{Modal, Workspace}; actions!(go_to_line, [Toggle]); @@ -75,15 +75,16 @@ impl GoToLine { fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { self.prev_scroll_position.take(); - self.active_editor.update(cx, |active_editor, cx| { - if let Some(rows) = active_editor.highlighted_rows() { + if let Some(point) = self.point_from_query(cx) { + self.active_editor.update(cx, |active_editor, cx| { let snapshot = active_editor.snapshot(cx).display_snapshot; - let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot); + let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left); active_editor.change_selections(Some(Autoscroll::center()), cx, |s| { - s.select_ranges([position..position]) + s.select_ranges([point..point]) }); - } - }); + }); + } + cx.emit(Event::Dismissed); } @@ -96,16 +97,7 @@ impl GoToLine { match event { editor::Event::Blurred => cx.emit(Event::Dismissed), editor::Event::BufferEdited { .. } => { - let line_editor = self.line_editor.read(cx).text(cx); - let mut components = line_editor.trim().split(&[',', ':'][..]); - let row = components.next().and_then(|row| row.parse::().ok()); - let column = components.next().and_then(|row| row.parse::().ok()); - if let Some(point) = row.map(|row| { - Point::new( - row.saturating_sub(1), - column.map(|column| column.saturating_sub(1)).unwrap_or(0), - ) - }) { + if let Some(point) = self.point_from_query(cx) { self.active_editor.update(cx, |active_editor, cx| { let snapshot = active_editor.snapshot(cx).display_snapshot; let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left); @@ -120,6 +112,20 @@ impl GoToLine { _ => {} } } + + fn point_from_query(&self, cx: &ViewContext) -> Option { + let line_editor = self.line_editor.read(cx).text(cx); + let mut components = line_editor + .splitn(2, FILE_ROW_COLUMN_DELIMITER) + .map(str::trim) + .fuse(); + let row = components.next().and_then(|row| row.parse::().ok())?; + let column = components.next().and_then(|col| col.parse::().ok()); + Some(Point::new( + row.saturating_sub(1), + column.unwrap_or(0).saturating_sub(1), + )) + } } impl Entity for GoToLine { @@ -144,10 +150,10 @@ impl View for GoToLine { } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let theme = &cx.global::().theme.picker; + let theme = &theme::current(cx).picker; let label = format!( - "{},{} of {} lines", + "{}{FILE_ROW_COLUMN_DELIMITER}{} of {} lines", self.cursor_point.row + 1, self.cursor_point.column + 1, self.max_point.row + 1 diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 35c5010cdd..c1dc13084e 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -48,7 +48,7 @@ smallvec.workspace = true smol.workspace = true time.workspace = true tiny-skia = "0.5" -usvg = "0.14" +usvg = { version = "0.14", features = [] } uuid = { version = "1.1.2", features = ["v4"] } waker-fn = "1.1.0" @@ -72,7 +72,7 @@ cocoa = "0.24" core-foundation = { version = "0.9.3", features = ["with-uuid"] } core-graphics = "0.22.3" core-text = "19.2" -font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "8eaf7a918eafa28b0a37dc759e2e0e7683fa24f1" } +font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "b2f77d56f450338aa4f7dd2f0197d8c9acb0cf18" } foreign-types = "0.3" log.workspace = true metal = "0.21.0" diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 26963fa2dd..bc89503459 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1174,7 +1174,7 @@ impl AppContext { this.notify_global(type_id); result } else { - panic!("No global added for {}", std::any::type_name::()); + panic!("no global added for {}", std::any::type_name::()); } } @@ -1182,6 +1182,15 @@ impl AppContext { self.globals.clear(); } + pub fn remove_global(&mut self) -> T { + *self + .globals + .remove(&TypeId::of::()) + .unwrap_or_else(|| panic!("no global added for {}", std::any::type_name::())) + .downcast() + .unwrap() + } + pub fn add_model(&mut self, build_model: F) -> ModelHandle where T: Entity, diff --git a/crates/gpui/src/app/test_app_context.rs b/crates/gpui/src/app/test_app_context.rs index 4af436a7b8..e956c4ca0d 100644 --- a/crates/gpui/src/app/test_app_context.rs +++ b/crates/gpui/src/app/test_app_context.rs @@ -270,7 +270,7 @@ impl TestAppContext { .borrow_mut() .pop_front() .expect("prompt was not called"); - let _ = done_tx.try_send(answer); + done_tx.try_send(answer).ok(); } pub fn has_pending_prompt(&self, window_id: usize) -> bool { diff --git a/crates/gpui/src/color.rs b/crates/gpui/src/color.rs index cc725776b9..b6c1e3aff9 100644 --- a/crates/gpui/src/color.rs +++ b/crates/gpui/src/color.rs @@ -42,7 +42,7 @@ impl Color { } pub fn yellow() -> Self { - Self(ColorU::from_u32(0x00ffffff)) + Self(ColorU::from_u32(0xffff00ff)) } pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self { diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index a566751fd5..779f4b6ec3 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -576,6 +576,15 @@ pub struct ComponentHost> { view_type: PhantomData, } +impl> ComponentHost { + pub fn new(c: C) -> Self { + Self { + component: c, + view_type: PhantomData, + } + } +} + impl> Deref for ComponentHost { type Target = C; diff --git a/crates/gpui/src/executor.rs b/crates/gpui/src/executor.rs index 7baffab2d9..028656a027 100644 --- a/crates/gpui/src/executor.rs +++ b/crates/gpui/src/executor.rs @@ -477,6 +477,14 @@ impl Deterministic { state.rng = StdRng::seed_from_u64(state.seed); } + pub fn allow_parking(&self) { + use rand::prelude::*; + + let mut state = self.state.lock(); + state.forbid_parking = false; + state.rng = StdRng::seed_from_u64(state.seed); + } + pub async fn simulate_random_delay(&self) { use rand::prelude::*; use smol::future::yield_now; @@ -698,6 +706,14 @@ impl Foreground { } } + #[cfg(any(test, feature = "test-support"))] + pub fn allow_parking(&self) { + match self { + Self::Deterministic { executor, .. } => executor.allow_parking(), + _ => panic!("this method can only be called on a deterministic executor"), + } + } + #[cfg(any(test, feature = "test-support"))] pub fn advance_clock(&self, duration: Duration) { match self { diff --git a/crates/gpui/src/keymap_matcher/binding.rs b/crates/gpui/src/keymap_matcher/binding.rs index aa40e8c6af..4d8334128b 100644 --- a/crates/gpui/src/keymap_matcher/binding.rs +++ b/crates/gpui/src/keymap_matcher/binding.rs @@ -11,6 +11,19 @@ pub struct Binding { context_predicate: Option, } +impl std::fmt::Debug for Binding { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Binding {{ keystrokes: {:?}, action: {}::{}, context_predicate: {:?} }}", + self.keystrokes, + self.action.namespace(), + self.action.name(), + self.context_predicate + ) + } +} + impl Clone for Binding { fn clone(&self) -> Self { Self { diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 2d1de6df75..3c82538611 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -755,7 +755,7 @@ impl platform::Window for Window { let _ = postage::sink::Sink::try_send(&mut done_tx, answer.try_into().unwrap()); } }); - + let block = block.copy(); let native_window = self.0.borrow().native_window; self.0 .borrow() diff --git a/crates/journal/Cargo.toml b/crates/journal/Cargo.toml index b88e3e093a..b7cbc62559 100644 --- a/crates/journal/Cargo.toml +++ b/crates/journal/Cargo.toml @@ -13,9 +13,15 @@ editor = { path = "../editor" } gpui = { path = "../gpui" } util = { path = "../util" } workspace = { path = "../workspace" } +settings = { path = "../settings" } + anyhow.workspace = true chrono = "0.4" dirs = "4.0" +serde.workspace = true +schemars.workspace = true log.workspace = true -settings = { path = "../settings" } shellexpand = "2.1.0" + +[dev-dependencies] +editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/journal/src/journal.rs b/crates/journal/src/journal.rs index 4b9622ece9..99fe997dc5 100644 --- a/crates/journal/src/journal.rs +++ b/crates/journal/src/journal.rs @@ -1,7 +1,9 @@ +use anyhow::Result; use chrono::{Datelike, Local, NaiveTime, Timelike}; use editor::{scroll::autoscroll::Autoscroll, Editor}; use gpui::{actions, AppContext}; -use settings::{HourFormat, Settings}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; use std::{ fs::OpenOptions, path::{Path, PathBuf}, @@ -11,13 +13,48 @@ use workspace::AppState; actions!(journal, [NewJournalEntry]); +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct JournalSettings { + pub path: Option, + pub hour_format: Option, +} + +impl Default for JournalSettings { + fn default() -> Self { + Self { + path: Some("~".into()), + hour_format: Some(Default::default()), + } + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum HourFormat { + #[default] + Hour12, + Hour24, +} + +impl settings::Setting for JournalSettings { + const KEY: Option<&'static str> = Some("journal"); + + type FileContent = Self; + + fn load(default_value: &Self, user_values: &[&Self], _: &AppContext) -> Result { + Self::load_via_json_merge(default_value, user_values) + } +} + pub fn init(app_state: Arc, cx: &mut AppContext) { + settings::register::(cx); + cx.add_global_action(move |_: &NewJournalEntry, cx| new_journal_entry(app_state.clone(), cx)); } pub fn new_journal_entry(app_state: Arc, cx: &mut AppContext) { - let settings = cx.global::(); - let journal_dir = match journal_dir(&settings) { + let settings = settings::get::(cx); + let journal_dir = match journal_dir(settings.path.as_ref().unwrap()) { Some(journal_dir) => journal_dir, None => { log::error!("Can't determine journal directory"); @@ -31,8 +68,7 @@ pub fn new_journal_entry(app_state: Arc, cx: &mut AppContext) { .join(format!("{:02}", now.month())); let entry_path = month_dir.join(format!("{:02}.md", now.day())); let now = now.time(); - let hour_format = &settings.journal_overrides.hour_format; - let entry_heading = heading_entry(now, &hour_format); + let entry_heading = heading_entry(now, &settings.hour_format); let create_entry = cx.background().spawn(async move { std::fs::create_dir_all(month_dir)?; @@ -76,14 +112,8 @@ pub fn new_journal_entry(app_state: Arc, cx: &mut AppContext) { .detach_and_log_err(cx); } -fn journal_dir(settings: &Settings) -> Option { - let journal_dir = settings - .journal_overrides - .path - .as_ref() - .unwrap_or(settings.journal_defaults.path.as_ref()?); - - let expanded_journal_dir = shellexpand::full(&journal_dir) //TODO handle this better +fn journal_dir(path: &str) -> Option { + let expanded_journal_dir = shellexpand::full(path) //TODO handle this better .ok() .map(|dir| Path::new(&dir.to_string()).to_path_buf().join("journal")); diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 6e03a07bc3..7e81620e5c 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -36,16 +36,19 @@ sum_tree = { path = "../sum_tree" } text = { path = "../text" } theme = { path = "../theme" } util = { path = "../util" } + anyhow.workspace = true async-broadcast = "0.4" async-trait.workspace = true futures.workspace = true +globset.workspace = true lazy_static.workspace = true log.workspace = true parking_lot.workspace = true postage.workspace = true rand = { workspace = true, optional = true } regex.workspace = true +schemars.workspace = true serde.workspace = true serde_derive.workspace = true serde_json.workspace = true diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 43766192eb..3a97702487 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -5,6 +5,7 @@ pub use crate::{ }; use crate::{ diagnostic_set::{DiagnosticEntry, DiagnosticGroup}, + language_settings::{language_settings, LanguageSettings}, outline::OutlineItem, syntax_map::{ SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxSnapshot, ToTreeSitterPoint, @@ -18,7 +19,6 @@ use futures::FutureExt as _; use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, Task}; use lsp::LanguageServerId; use parking_lot::Mutex; -use settings::Settings; use similar::{ChangeTag, TextDiff}; use smallvec::SmallVec; use smol::future::yield_now; @@ -1827,11 +1827,11 @@ impl BufferSnapshot { pub fn language_indent_size_at(&self, position: T, cx: &AppContext) -> IndentSize { let language_name = self.language_at(position).map(|language| language.name()); - let settings = cx.global::(); - if settings.hard_tabs(language_name.as_deref()) { + let settings = language_settings(language_name.as_deref(), cx); + if settings.hard_tabs { IndentSize::tab() } else { - IndentSize::spaces(settings.tab_size(language_name.as_deref()).get()) + IndentSize::spaces(settings.tab_size.get()) } } @@ -2146,6 +2146,15 @@ impl BufferSnapshot { .or(self.language.as_ref()) } + pub fn settings_at<'a, D: ToOffset>( + &self, + position: D, + cx: &'a AppContext, + ) -> &'a LanguageSettings { + let language = self.language_at(position); + language_settings(language.map(|l| l.name()).as_deref(), cx) + } + pub fn language_scope_at(&self, position: D) -> Option { let offset = position.to_offset(self); @@ -2500,18 +2509,22 @@ impl BufferSnapshot { pub fn git_diff_hunks_in_row_range<'a>( &'a self, range: Range, - reversed: bool, ) -> impl 'a + Iterator> { - self.git_diff.hunks_in_row_range(range, self, reversed) + self.git_diff.hunks_in_row_range(range, self) } pub fn git_diff_hunks_intersecting_range<'a>( &'a self, range: Range, - reversed: bool, ) -> impl 'a + Iterator> { - self.git_diff - .hunks_intersecting_range(range, self, reversed) + self.git_diff.hunks_intersecting_range(range, self) + } + + pub fn git_diff_hunks_intersecting_range_rev<'a>( + &'a self, + range: Range, + ) -> impl 'a + Iterator> { + self.git_diff.hunks_intersecting_range_rev(range, self) } pub fn diagnostics_in_range<'a, T, O>( diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index eeac1a4818..be573aa895 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -1,3 +1,7 @@ +use crate::language_settings::{ + AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent, +}; + use super::*; use clock::ReplicaId; use collections::BTreeMap; @@ -7,7 +11,7 @@ use indoc::indoc; use proto::deserialize_operation; use rand::prelude::*; use regex::RegexBuilder; -use settings::Settings; +use settings::SettingsStore; use std::{ cell::RefCell, env, @@ -36,7 +40,8 @@ fn init_logger() { #[gpui::test] fn test_line_endings(cx: &mut gpui::AppContext) { - cx.set_global(Settings::test(cx)); + 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); @@ -862,8 +867,7 @@ fn test_range_for_syntax_ancestor(cx: &mut AppContext) { #[gpui::test] fn test_autoindent_with_soft_tabs(cx: &mut AppContext) { - let settings = Settings::test(cx); - cx.set_global(settings); + init_settings(cx, |_| {}); cx.add_model(|cx| { let text = "fn a() {}"; @@ -903,9 +907,9 @@ fn test_autoindent_with_soft_tabs(cx: &mut AppContext) { #[gpui::test] fn test_autoindent_with_hard_tabs(cx: &mut AppContext) { - let mut settings = Settings::test(cx); - settings.editor_overrides.hard_tabs = Some(true); - cx.set_global(settings); + init_settings(cx, |settings| { + settings.defaults.hard_tabs = Some(true); + }); cx.add_model(|cx| { let text = "fn a() {}"; @@ -945,8 +949,7 @@ fn test_autoindent_with_hard_tabs(cx: &mut AppContext) { #[gpui::test] fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppContext) { - let settings = Settings::test(cx); - cx.set_global(settings); + init_settings(cx, |_| {}); cx.add_model(|cx| { let mut buffer = Buffer::new( @@ -1082,8 +1085,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppC #[gpui::test] fn test_autoindent_does_not_adjust_lines_within_newly_created_errors(cx: &mut AppContext) { - let settings = Settings::test(cx); - cx.set_global(settings); + init_settings(cx, |_| {}); cx.add_model(|cx| { let mut buffer = Buffer::new( @@ -1145,7 +1147,8 @@ fn test_autoindent_does_not_adjust_lines_within_newly_created_errors(cx: &mut Ap #[gpui::test] fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut AppContext) { - cx.set_global(Settings::test(cx)); + init_settings(cx, |_| {}); + cx.add_model(|cx| { let mut buffer = Buffer::new( 0, @@ -1201,7 +1204,8 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut AppContext) { #[gpui::test] fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut AppContext) { - cx.set_global(Settings::test(cx)); + init_settings(cx, |_| {}); + cx.add_model(|cx| { let text = "a\nb"; let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); @@ -1217,7 +1221,8 @@ fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut AppContext) { #[gpui::test] fn test_autoindent_multi_line_insertion(cx: &mut AppContext) { - cx.set_global(Settings::test(cx)); + init_settings(cx, |_| {}); + cx.add_model(|cx| { let text = " const a: usize = 1; @@ -1257,7 +1262,8 @@ fn test_autoindent_multi_line_insertion(cx: &mut AppContext) { #[gpui::test] fn test_autoindent_block_mode(cx: &mut AppContext) { - cx.set_global(Settings::test(cx)); + init_settings(cx, |_| {}); + cx.add_model(|cx| { let text = r#" fn a() { @@ -1339,7 +1345,8 @@ fn test_autoindent_block_mode(cx: &mut AppContext) { #[gpui::test] fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut AppContext) { - cx.set_global(Settings::test(cx)); + init_settings(cx, |_| {}); + cx.add_model(|cx| { let text = r#" fn a() { @@ -1417,7 +1424,8 @@ fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut AppContex #[gpui::test] fn test_autoindent_language_without_indents_query(cx: &mut AppContext) { - cx.set_global(Settings::test(cx)); + init_settings(cx, |_| {}); + cx.add_model(|cx| { let text = " * one @@ -1460,25 +1468,23 @@ fn test_autoindent_language_without_indents_query(cx: &mut AppContext) { #[gpui::test] fn test_autoindent_with_injected_languages(cx: &mut AppContext) { - cx.set_global({ - let mut settings = Settings::test(cx); - settings.language_overrides.extend([ + init_settings(cx, |settings| { + settings.languages.extend([ ( "HTML".into(), - settings::EditorSettings { + LanguageSettingsContent { tab_size: Some(2.try_into().unwrap()), ..Default::default() }, ), ( "JavaScript".into(), - settings::EditorSettings { + LanguageSettingsContent { tab_size: Some(8.try_into().unwrap()), ..Default::default() }, ), - ]); - settings + ]) }); let html_language = Arc::new( @@ -1574,9 +1580,10 @@ fn test_autoindent_with_injected_languages(cx: &mut AppContext) { #[gpui::test] fn test_autoindent_query_with_outdent_captures(cx: &mut AppContext) { - let mut settings = Settings::test(cx); - settings.editor_defaults.tab_size = Some(2.try_into().unwrap()); - cx.set_global(settings); + init_settings(cx, |settings| { + settings.defaults.tab_size = Some(2.try_into().unwrap()); + }); + cx.add_model(|cx| { let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(ruby_lang()), cx); @@ -1617,7 +1624,8 @@ fn test_autoindent_query_with_outdent_captures(cx: &mut AppContext) { #[gpui::test] fn test_language_config_at(cx: &mut AppContext) { - cx.set_global(Settings::test(cx)); + init_settings(cx, |_| {}); + cx.add_model(|cx| { let language = Language::new( LanguageConfig { @@ -2199,7 +2207,6 @@ fn assert_bracket_pairs( language: Language, cx: &mut AppContext, ) { - cx.set_global(Settings::test(cx)); 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) @@ -2222,3 +2229,11 @@ fn assert_bracket_pairs( bracket_pairs ); } + +fn init_settings(cx: &mut AppContext, f: fn(&mut AllLanguageSettingsContent)) { + cx.set_global(SettingsStore::test(cx)); + crate::init(cx); + cx.update_global::(|settings, cx| { + settings.update_user_settings::(cx, f); + }); +} diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 85c9089952..87e4880b99 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -1,6 +1,7 @@ mod buffer; mod diagnostic_set; mod highlight_map; +pub mod language_settings; mod outline; pub mod proto; mod syntax_map; @@ -58,6 +59,10 @@ pub use lsp::LanguageServerId; pub use outline::{Outline, OutlineItem}; pub use tree_sitter::{Parser, Tree}; +pub fn init(cx: &mut AppContext) { + language_settings::init(cx); +} + thread_local! { static PARSER: RefCell = RefCell::new(Parser::new()); } diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs new file mode 100644 index 0000000000..d877304f1d --- /dev/null +++ b/crates/language/src/language_settings.rs @@ -0,0 +1,338 @@ +use anyhow::Result; +use collections::HashMap; +use globset::GlobMatcher; +use gpui::AppContext; +use schemars::{ + schema::{InstanceType, ObjectValidation, Schema, SchemaObject}, + JsonSchema, +}; +use serde::{Deserialize, Serialize}; +use std::{num::NonZeroU32, path::Path, sync::Arc}; + +pub fn init(cx: &mut AppContext) { + settings::register::(cx); +} + +pub fn language_settings<'a>(language: Option<&str>, cx: &'a AppContext) -> &'a LanguageSettings { + settings::get::(cx).language(language) +} + +pub fn all_language_settings<'a>(cx: &'a AppContext) -> &'a AllLanguageSettings { + settings::get::(cx) +} + +#[derive(Debug, Clone)] +pub struct AllLanguageSettings { + pub copilot: CopilotSettings, + defaults: LanguageSettings, + languages: HashMap, LanguageSettings>, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct LanguageSettings { + pub tab_size: NonZeroU32, + pub hard_tabs: bool, + pub soft_wrap: SoftWrap, + pub preferred_line_length: u32, + pub format_on_save: FormatOnSave, + pub remove_trailing_whitespace_on_save: bool, + pub ensure_final_newline_on_save: bool, + pub formatter: Formatter, + pub enable_language_server: bool, + pub show_copilot_suggestions: bool, + pub show_whitespaces: ShowWhitespaceSetting, +} + +#[derive(Clone, Debug, Default)] +pub struct CopilotSettings { + pub feature_enabled: bool, + pub disabled_globs: Vec, +} + +#[derive(Clone, Serialize, Deserialize, JsonSchema)] +pub struct AllLanguageSettingsContent { + #[serde(default)] + pub features: Option, + #[serde(default)] + pub copilot: Option, + #[serde(flatten)] + pub defaults: LanguageSettingsContent, + #[serde(default, alias = "language_overrides")] + pub languages: HashMap, LanguageSettingsContent>, +} + +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] +pub struct LanguageSettingsContent { + #[serde(default)] + pub tab_size: Option, + #[serde(default)] + pub hard_tabs: Option, + #[serde(default)] + pub soft_wrap: Option, + #[serde(default)] + pub preferred_line_length: Option, + #[serde(default)] + pub format_on_save: Option, + #[serde(default)] + pub remove_trailing_whitespace_on_save: Option, + #[serde(default)] + pub ensure_final_newline_on_save: Option, + #[serde(default)] + pub formatter: Option, + #[serde(default)] + pub enable_language_server: Option, + #[serde(default)] + pub show_copilot_suggestions: Option, + #[serde(default)] + pub show_whitespaces: Option, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +pub struct CopilotSettingsContent { + #[serde(default)] + pub disabled_globs: Option>, +} + +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct FeaturesContent { + pub copilot: Option, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum SoftWrap { + None, + EditorWidth, + PreferredLineLength, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum FormatOnSave { + On, + Off, + LanguageServer, + External { + command: Arc, + arguments: Arc<[String]>, + }, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ShowWhitespaceSetting { + Selection, + None, + All, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum Formatter { + LanguageServer, + External { + command: Arc, + arguments: Arc<[String]>, + }, +} + +impl AllLanguageSettings { + pub fn language<'a>(&'a self, language_name: Option<&str>) -> &'a LanguageSettings { + if let Some(name) = language_name { + if let Some(overrides) = self.languages.get(name) { + return overrides; + } + } + &self.defaults + } + + pub fn copilot_enabled_for_path(&self, path: &Path) -> bool { + !self + .copilot + .disabled_globs + .iter() + .any(|glob| glob.is_match(path)) + } + + pub fn copilot_enabled(&self, language_name: Option<&str>, path: Option<&Path>) -> bool { + if !self.copilot.feature_enabled { + return false; + } + + if let Some(path) = path { + if !self.copilot_enabled_for_path(path) { + return false; + } + } + + self.language(language_name).show_copilot_suggestions + } +} + +impl settings::Setting for AllLanguageSettings { + const KEY: Option<&'static str> = None; + + type FileContent = AllLanguageSettingsContent; + + fn load( + default_value: &Self::FileContent, + user_settings: &[&Self::FileContent], + _: &AppContext, + ) -> Result { + // A default is provided for all settings. + let mut defaults: LanguageSettings = + serde_json::from_value(serde_json::to_value(&default_value.defaults)?)?; + + let mut languages = HashMap::default(); + for (language_name, settings) in &default_value.languages { + let mut language_settings = defaults.clone(); + merge_settings(&mut language_settings, &settings); + languages.insert(language_name.clone(), language_settings); + } + + let mut copilot_enabled = default_value + .features + .as_ref() + .and_then(|f| f.copilot) + .ok_or_else(Self::missing_default)?; + let mut copilot_globs = default_value + .copilot + .as_ref() + .and_then(|c| c.disabled_globs.as_ref()) + .ok_or_else(Self::missing_default)?; + + for user_settings in user_settings { + if let Some(copilot) = user_settings.features.as_ref().and_then(|f| f.copilot) { + copilot_enabled = copilot; + } + if let Some(globs) = user_settings + .copilot + .as_ref() + .and_then(|f| f.disabled_globs.as_ref()) + { + copilot_globs = globs; + } + + // A user's global settings override the default global settings and + // all default language-specific settings. + merge_settings(&mut defaults, &user_settings.defaults); + for language_settings in languages.values_mut() { + merge_settings(language_settings, &user_settings.defaults); + } + + // A user's language-specific settings override default language-specific settings. + for (language_name, user_language_settings) in &user_settings.languages { + merge_settings( + languages + .entry(language_name.clone()) + .or_insert_with(|| defaults.clone()), + &user_language_settings, + ); + } + } + + Ok(Self { + copilot: CopilotSettings { + feature_enabled: copilot_enabled, + disabled_globs: copilot_globs + .iter() + .filter_map(|g| Some(globset::Glob::new(g).ok()?.compile_matcher())) + .collect(), + }, + defaults, + languages, + }) + } + + fn json_schema( + generator: &mut schemars::gen::SchemaGenerator, + params: &settings::SettingsJsonSchemaParams, + _: &AppContext, + ) -> schemars::schema::RootSchema { + let mut root_schema = generator.root_schema_for::(); + + // Create a schema for a 'languages overrides' object, associating editor + // settings with specific langauges. + assert!(root_schema + .definitions + .contains_key("LanguageSettingsContent")); + + let languages_object_schema = SchemaObject { + instance_type: Some(InstanceType::Object.into()), + object: Some(Box::new(ObjectValidation { + properties: params + .language_names + .iter() + .map(|name| { + ( + name.clone(), + Schema::new_ref("#/definitions/LanguageSettingsContent".into()), + ) + }) + .collect(), + ..Default::default() + })), + ..Default::default() + }; + + root_schema + .definitions + .extend([("Languages".into(), languages_object_schema.into())]); + + root_schema + .schema + .object + .as_mut() + .unwrap() + .properties + .extend([ + ( + "languages".to_owned(), + Schema::new_ref("#/definitions/Languages".into()), + ), + // For backward compatibility + ( + "language_overrides".to_owned(), + Schema::new_ref("#/definitions/Languages".into()), + ), + ]); + + root_schema + } +} + +fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent) { + merge(&mut settings.tab_size, src.tab_size); + merge(&mut settings.hard_tabs, src.hard_tabs); + merge(&mut settings.soft_wrap, src.soft_wrap); + merge( + &mut settings.preferred_line_length, + src.preferred_line_length, + ); + merge(&mut settings.formatter, src.formatter.clone()); + merge(&mut settings.format_on_save, src.format_on_save.clone()); + merge( + &mut settings.remove_trailing_whitespace_on_save, + src.remove_trailing_whitespace_on_save, + ); + merge( + &mut settings.ensure_final_newline_on_save, + src.ensure_final_newline_on_save, + ); + merge( + &mut settings.enable_language_server, + src.enable_language_server, + ); + merge( + &mut settings.show_copilot_suggestions, + src.show_copilot_suggestions, + ); + merge(&mut settings.show_whitespaces, src.show_whitespaces); + + fn merge(target: &mut T, value: Option) { + if let Some(value) = value { + *target = value; + } + } +} diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index 88de8e690a..ce1196e946 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -1114,6 +1114,8 @@ fn get_injections( let mut query_cursor = QueryCursorHandle::new(); let mut prev_match = None; + // Ensure that a `ParseStep` is created for every combined injection language, even + // if there currently no matches for that injection. combined_injection_ranges.clear(); for pattern in &config.patterns { if let (Some(language_name), true) = (pattern.language.as_ref(), pattern.combined) { @@ -1174,8 +1176,8 @@ fn get_injections( if let Some(language) = language { if combined { combined_injection_ranges - .get_mut(&language.clone()) - .unwrap() + .entry(language.clone()) + .or_default() .extend(content_ranges); } else { queue.push(ParseStep { diff --git a/crates/language_selector/Cargo.toml b/crates/language_selector/Cargo.toml index e7b3d8d4be..f6e213f25f 100644 --- a/crates/language_selector/Cargo.toml +++ b/crates/language_selector/Cargo.toml @@ -20,3 +20,6 @@ settings = { path = "../settings" } util = { path = "../util" } workspace = { path = "../workspace" } anyhow.workspace = true + +[dev-dependencies] +editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/language_selector/src/active_buffer_language.rs b/crates/language_selector/src/active_buffer_language.rs index 425f4c8dd7..2c78b89f31 100644 --- a/crates/language_selector/src/active_buffer_language.rs +++ b/crates/language_selector/src/active_buffer_language.rs @@ -4,7 +4,6 @@ use gpui::{ platform::{CursorStyle, MouseButton}, Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, }; -use settings::Settings; use std::sync::Arc; use workspace::{item::ItemHandle, StatusItemView, Workspace}; @@ -55,7 +54,7 @@ impl View for ActiveBufferLanguage { }; MouseEventHandler::::new(0, cx, |state, cx| { - let theme = &cx.global::().theme.workspace.status_bar; + let theme = &theme::current(cx).workspace.status_bar; let style = theme.active_language.style_for(state, false); Label::new(active_language_text, style.text.clone()) .contained() diff --git a/crates/language_selector/src/language_selector.rs b/crates/language_selector/src/language_selector.rs index fd43111443..817901cd3a 100644 --- a/crates/language_selector/src/language_selector.rs +++ b/crates/language_selector/src/language_selector.rs @@ -8,7 +8,6 @@ use gpui::{actions, elements::*, AppContext, ModelHandle, MouseState, ViewContex use language::{Buffer, LanguageRegistry}; use picker::{Picker, PickerDelegate, PickerEvent}; use project::Project; -use settings::Settings; use std::sync::Arc; use util::ResultExt; use workspace::Workspace; @@ -179,8 +178,7 @@ impl PickerDelegate for LanguageSelectorDelegate { selected: bool, cx: &AppContext, ) -> AnyElement> { - let settings = cx.global::(); - let theme = &settings.theme; + let theme = theme::current(cx); let mat = &self.matches[ix]; let style = theme.picker.item.style_for(mouse_state, selected); let buffer_language_name = self.buffer.read(cx).language().map(|l| l.name()); diff --git a/crates/lsp_log/Cargo.toml b/crates/lsp_log/Cargo.toml index 8741f0a4cf..6f47057b44 100644 --- a/crates/lsp_log/Cargo.toml +++ b/crates/lsp_log/Cargo.toml @@ -24,6 +24,7 @@ serde.workspace = true anyhow.workspace = true [dev-dependencies] +editor = { path = "../editor", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } unindent.workspace = true diff --git a/crates/lsp_log/src/lsp_log.rs b/crates/lsp_log/src/lsp_log.rs index 0efcd3afc8..db41c6ff4d 100644 --- a/crates/lsp_log/src/lsp_log.rs +++ b/crates/lsp_log/src/lsp_log.rs @@ -13,7 +13,6 @@ use gpui::{ }; use language::{Buffer, LanguageServerId, LanguageServerName}; use project::{Project, WorktreeId}; -use settings::Settings; use std::{borrow::Cow, sync::Arc}; use theme::{ui, Theme}; use workspace::{ @@ -304,7 +303,7 @@ impl View for LspLogToolbarItemView { } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let theme = cx.global::().theme.clone(); + let theme = theme::current(cx).clone(); let Some(log_view) = self.log_view.as_ref() else { return Empty::new().into_any() }; let project = self.project.read(cx); let log_view = log_view.read(cx); diff --git a/crates/outline/Cargo.toml b/crates/outline/Cargo.toml index 91e5011b2a..f4e2b849fa 100644 --- a/crates/outline/Cargo.toml +++ b/crates/outline/Cargo.toml @@ -16,7 +16,12 @@ language = { path = "../language" } picker = { path = "../picker" } settings = { path = "../settings" } text = { path = "../text" } +theme = { path = "../theme" } workspace = { path = "../workspace" } + ordered-float.workspace = true postage.workspace = true smol.workspace = true + +[dev-dependencies] +editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index 6ecaf370e4..1e364f5fc8 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -10,7 +10,6 @@ use gpui::{ use language::Outline; use ordered_float::OrderedFloat; use picker::{Picker, PickerDelegate, PickerEvent}; -use settings::Settings; use std::{ cmp::{self, Reverse}, sync::Arc, @@ -34,7 +33,7 @@ pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext().theme.editor.syntax.as_ref())); + .outline(Some(theme::current(cx).editor.syntax.as_ref())); if let Some(outline) = outline { workspace.toggle_modal(cx, |_, cx| { cx.add_view(|cx| { @@ -204,9 +203,9 @@ impl PickerDelegate for OutlineViewDelegate { selected: bool, cx: &AppContext, ) -> AnyElement> { - let settings = cx.global::(); + let theme = theme::current(cx); + let style = theme.picker.item.style_for(mouse_state, selected); let string_match = &self.matches[ix]; - let style = settings.theme.picker.item.style_for(mouse_state, selected); let outline_item = &self.outline.items[string_match.candidate_id]; Text::new(outline_item.text.clone(), style.label.text.clone()) diff --git a/crates/picker/Cargo.toml b/crates/picker/Cargo.toml index b723cd788c..54e4b15ad5 100644 --- a/crates/picker/Cargo.toml +++ b/crates/picker/Cargo.toml @@ -20,6 +20,7 @@ workspace = { path = "../workspace" } parking_lot.workspace = true [dev-dependencies] +editor = { path = "../editor", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } serde_json.workspace = true workspace = { path = "../workspace", features = ["test-support"] } diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 01749ccf84..69f16e4949 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -57,7 +57,7 @@ impl View for Picker { } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let theme = (self.theme.lock())(&cx.global::().theme); + let theme = (self.theme.lock())(theme::current(cx).as_ref()); let query = self.query(cx); let match_count = self.delegate.match_count(); diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 2b4892aab9..d6578c87ba 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -42,7 +42,7 @@ anyhow.workspace = true async-trait.workspace = true backtrace = "0.3" futures.workspace = true -glob.workspace = true +globset.workspace = true ignore = "0.4" lazy_static.workspace = true log.workspace = true @@ -50,6 +50,7 @@ parking_lot.workspace = true postage.workspace = true rand.workspace = true regex.workspace = true +schemars.workspace = true serde.workspace = true serde_derive.workspace = true serde_json.workspace = true @@ -57,7 +58,7 @@ sha2 = "0.10" similar = "1.3" smol.workspace = true thiserror.workspace = true -toml = "0.5" +toml.workspace = true itertools = "0.10" [dev-dependencies] @@ -74,5 +75,6 @@ lsp = { path = "../lsp", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } rpc = { path = "../rpc", features = ["test-support"] } +git2 = { version = "0.15", default-features = false } tempdir.workspace = true unindent.workspace = true diff --git a/crates/project/src/lsp_glob_set.rs b/crates/project/src/lsp_glob_set.rs deleted file mode 100644 index daac344a0a..0000000000 --- a/crates/project/src/lsp_glob_set.rs +++ /dev/null @@ -1,121 +0,0 @@ -use anyhow::{anyhow, Result}; -use std::path::Path; - -#[derive(Default)] -pub struct LspGlobSet { - patterns: Vec, -} - -impl LspGlobSet { - pub fn clear(&mut self) { - self.patterns.clear(); - } - - /// Add a pattern to the glob set. - /// - /// LSP's glob syntax supports bash-style brace expansion. For example, - /// the pattern '*.{js,ts}' would match all JavaScript or TypeScript files. - /// This is not a part of the standard libc glob syntax, and isn't supported - /// by the `glob` crate. So we pre-process the glob patterns, producing a - /// separate glob `Pattern` object for each part of a brace expansion. - pub fn add_pattern(&mut self, pattern: &str) -> Result<()> { - // Find all of the ranges of `pattern` that contain matched curly braces. - let mut expansion_ranges = Vec::new(); - let mut expansion_start_ix = None; - for (ix, c) in pattern.match_indices(|c| ['{', '}'].contains(&c)) { - match c { - "{" => { - if expansion_start_ix.is_some() { - return Err(anyhow!("nested braces in glob patterns aren't supported")); - } - expansion_start_ix = Some(ix); - } - "}" => { - if let Some(start_ix) = expansion_start_ix { - expansion_ranges.push(start_ix..ix + 1); - } - expansion_start_ix = None; - } - _ => {} - } - } - - // Starting with a single pattern, process each brace expansion by cloning - // the pattern once per element of the expansion. - let mut unexpanded_patterns = vec![]; - let mut expanded_patterns = vec![pattern.to_string()]; - - for outer_range in expansion_ranges.into_iter().rev() { - let inner_range = (outer_range.start + 1)..(outer_range.end - 1); - std::mem::swap(&mut unexpanded_patterns, &mut expanded_patterns); - for unexpanded_pattern in unexpanded_patterns.drain(..) { - for part in unexpanded_pattern[inner_range.clone()].split(',') { - let mut expanded_pattern = unexpanded_pattern.clone(); - expanded_pattern.replace_range(outer_range.clone(), part); - expanded_patterns.push(expanded_pattern); - } - } - } - - // Parse the final glob patterns and add them to the set. - for pattern in expanded_patterns { - let pattern = glob::Pattern::new(&pattern)?; - self.patterns.push(pattern); - } - - Ok(()) - } - - pub fn matches(&self, path: &Path) -> bool { - self.patterns - .iter() - .any(|pattern| pattern.matches_path(path)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_glob_set() { - let mut watch = LspGlobSet::default(); - watch.add_pattern("/a/**/*.rs").unwrap(); - watch.add_pattern("/a/**/Cargo.toml").unwrap(); - - assert!(watch.matches("/a/b.rs".as_ref())); - assert!(watch.matches("/a/b/c.rs".as_ref())); - - assert!(!watch.matches("/b/c.rs".as_ref())); - assert!(!watch.matches("/a/b.ts".as_ref())); - } - - #[test] - fn test_brace_expansion() { - let mut watch = LspGlobSet::default(); - watch.add_pattern("/a/*.{ts,js,tsx}").unwrap(); - - assert!(watch.matches("/a/one.js".as_ref())); - assert!(watch.matches("/a/two.ts".as_ref())); - assert!(watch.matches("/a/three.tsx".as_ref())); - - assert!(!watch.matches("/a/one.j".as_ref())); - assert!(!watch.matches("/a/two.s".as_ref())); - assert!(!watch.matches("/a/three.t".as_ref())); - assert!(!watch.matches("/a/four.t".as_ref())); - assert!(!watch.matches("/a/five.xt".as_ref())); - } - - #[test] - fn test_multiple_brace_expansion() { - let mut watch = LspGlobSet::default(); - watch.add_pattern("/a/{one,two,three}.{b*c,d*e}").unwrap(); - - assert!(watch.matches("/a/one.bic".as_ref())); - assert!(watch.matches("/a/two.dole".as_ref())); - assert!(watch.matches("/a/three.deeee".as_ref())); - - assert!(!watch.matches("/a/four.bic".as_ref())); - assert!(!watch.matches("/a/one.be".as_ref())); - } -} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 94872708c4..f91cd999f9 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1,6 +1,6 @@ mod ignore; mod lsp_command; -mod lsp_glob_set; +mod project_settings; pub mod search; pub mod terminals; pub mod worktree; @@ -18,11 +18,13 @@ use futures::{ future::{try_join_all, Shared}, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt, }; +use globset::{Glob, GlobSet, GlobSetBuilder}; use gpui::{ AnyModelHandle, AppContext, AsyncAppContext, BorrowAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle, }; use language::{ + language_settings::{all_language_settings, language_settings, FormatOnSave, Formatter}, point_to_lsp, proto::{ deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version, @@ -39,12 +41,12 @@ use lsp::{ DocumentHighlightKind, LanguageServer, LanguageServerId, }; use lsp_command::*; -use lsp_glob_set::LspGlobSet; use postage::watch; +use project_settings::ProjectSettings; use rand::prelude::*; use search::SearchQuery; use serde::Serialize; -use settings::{FormatOnSave, Formatter, Settings}; +use settings::SettingsStore; use sha2::{Digest, Sha256}; use similar::{ChangeTag, TextDiff}; use std::{ @@ -64,9 +66,7 @@ use std::{ }, time::{Duration, Instant, SystemTime}, }; - use terminals::Terminals; - use util::{debug_panic, defer, merge_json_value_into, post_inc, ResultExt, TryFutureExt as _}; pub use fs::*; @@ -122,6 +122,8 @@ pub struct Project { loading_local_worktrees: HashMap, Shared, Arc>>>>, opened_buffers: HashMap, + local_buffer_ids_by_path: HashMap, + local_buffer_ids_by_entry_id: HashMap, /// A mapping from a buffer ID to None means that we've started waiting for an ID but haven't finished loading it. /// Used for re-issuing buffer requests when peers temporarily disconnect incomplete_remote_buffers: HashMap>>, @@ -210,6 +212,7 @@ pub enum Event { RemoteIdChanged(Option), DisconnectedFromHost, Closed, + DeletedEntry(ProjectEntryId), CollaboratorUpdated { old_peer_id: proto::PeerId, new_peer_id: proto::PeerId, @@ -223,7 +226,7 @@ pub enum LanguageServerState { language: Arc, adapter: Arc, server: Arc, - watched_paths: LspGlobSet, + watched_paths: HashMap, simulate_disk_based_diagnostics_completion: Option>, }, } @@ -386,7 +389,13 @@ impl FormatTrigger { } impl Project { - pub fn init(client: &Arc) { + pub fn init_settings(cx: &mut AppContext) { + settings::register::(cx); + } + + pub fn init(client: &Arc, cx: &mut AppContext) { + Self::init_settings(cx); + client.add_model_message_handler(Self::handle_add_collaborator); client.add_model_message_handler(Self::handle_update_project_collaborator); client.add_model_message_handler(Self::handle_remove_collaborator); @@ -449,12 +458,16 @@ impl Project { incomplete_remote_buffers: Default::default(), loading_buffers_by_path: Default::default(), loading_local_worktrees: Default::default(), + local_buffer_ids_by_path: Default::default(), + local_buffer_ids_by_entry_id: Default::default(), buffer_snapshots: Default::default(), join_project_response_message_id: 0, client_state: None, opened_buffer: watch::channel(), client_subscriptions: Vec::new(), - _subscriptions: vec![cx.observe_global::(Self::on_settings_changed)], + _subscriptions: vec![ + cx.observe_global::(Self::on_settings_changed) + ], _maintain_buffer_languages: Self::maintain_buffer_languages(&languages, cx), _maintain_workspace_config: Self::maintain_workspace_config(languages.clone(), cx), active_entry: None, @@ -517,6 +530,8 @@ impl Project { shared_buffers: Default::default(), incomplete_remote_buffers: Default::default(), loading_local_worktrees: Default::default(), + local_buffer_ids_by_path: Default::default(), + local_buffer_ids_by_entry_id: Default::default(), active_entry: None, collaborators: Default::default(), join_project_response_message_id: response.message_id, @@ -595,12 +610,6 @@ impl Project { root_paths: impl IntoIterator, cx: &mut gpui::TestAppContext, ) -> ModelHandle { - if !cx.read(|cx| cx.has_global::()) { - cx.update(|cx| { - cx.set_global(Settings::test(cx)); - }); - } - let mut languages = LanguageRegistry::test(); languages.set_executor(cx.background()); let http_client = util::http::FakeHttpClient::with_404_response(); @@ -622,7 +631,7 @@ impl Project { } fn on_settings_changed(&mut self, cx: &mut ModelContext) { - let settings = cx.global::(); + let settings = all_language_settings(cx); let mut language_servers_to_start = Vec::new(); for buffer in self.opened_buffers.values() { @@ -630,7 +639,10 @@ impl Project { let buffer = buffer.read(cx); if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language()) { - if settings.enable_language_server(Some(&language.name())) { + if settings + .language(Some(&language.name())) + .enable_language_server + { let worktree = file.worktree.read(cx); language_servers_to_start.push(( worktree.id(), @@ -645,7 +657,10 @@ impl Project { let mut language_servers_to_stop = Vec::new(); for language in self.languages.to_vec() { for lsp_adapter in language.lsp_adapters() { - if !settings.enable_language_server(Some(&language.name())) { + if !settings + .language(Some(&language.name())) + .enable_language_server + { let lsp_name = &lsp_adapter.name; for (worktree_id, started_lsp_name) in self.language_server_ids.keys() { if lsp_name == started_lsp_name { @@ -962,6 +977,9 @@ impl Project { cx: &mut ModelContext, ) -> Option>> { let worktree = self.worktree_for_entry(entry_id, cx)?; + + cx.emit(Event::DeletedEntry(entry_id)); + if self.is_local() { worktree.update(cx, |worktree, cx| { worktree.as_local_mut().unwrap().delete_entry(entry_id, cx) @@ -1628,6 +1646,21 @@ impl Project { }) .detach(); + if let Some(file) = File::from_dyn(buffer.read(cx).file()) { + if file.is_local { + self.local_buffer_ids_by_path.insert( + ProjectPath { + worktree_id: file.worktree_id(cx), + path: file.path.clone(), + }, + remote_id, + ); + + self.local_buffer_ids_by_entry_id + .insert(file.entry_id, remote_id); + } + } + self.detect_language_for_buffer(buffer, cx); self.register_buffer_with_language_servers(buffer, cx); self.register_buffer_with_copilot(buffer, cx); @@ -2101,7 +2134,7 @@ impl Project { let (mut settings_changed_tx, mut settings_changed_rx) = watch::channel(); let _ = postage::stream::Stream::try_recv(&mut settings_changed_rx); - let settings_observation = cx.observe_global::(move |_, _| { + let settings_observation = cx.observe_global::(move |_, _| { *settings_changed_tx.borrow_mut() = (); }); cx.spawn_weak(|this, mut cx| async move { @@ -2178,10 +2211,7 @@ impl Project { language: Arc, cx: &mut ModelContext, ) { - if !cx - .global::() - .enable_language_server(Some(&language.name())) - { + if !language_settings(Some(&language.name()), cx).enable_language_server { return; } @@ -2202,7 +2232,9 @@ impl Project { None => continue, }; - let lsp = &cx.global::().lsp.get(&adapter.name.0); + let lsp = settings::get::(cx) + .lsp + .get(&adapter.name.0); let override_options = lsp.map(|s| s.initialization_options.clone()).flatten(); let mut initialization_options = adapter.initialization_options.clone(); @@ -2838,10 +2870,37 @@ impl Project { if let Some(LanguageServerState::Running { watched_paths, .. }) = self.language_servers.get_mut(&language_server_id) { - watched_paths.clear(); + let mut builders = HashMap::default(); for watcher in params.watchers { - watched_paths.add_pattern(&watcher.glob_pattern).log_err(); + for worktree in &self.worktrees { + if let Some(worktree) = worktree.upgrade(cx) { + let worktree = worktree.read(cx); + if let Some(abs_path) = worktree.abs_path().to_str() { + if let Some(suffix) = watcher + .glob_pattern + .strip_prefix(abs_path) + .and_then(|s| s.strip_prefix(std::path::MAIN_SEPARATOR)) + { + if let Some(glob) = Glob::new(suffix).log_err() { + builders + .entry(worktree.id()) + .or_insert_with(|| GlobSetBuilder::new()) + .add(glob); + } + break; + } + } + } + } } + + watched_paths.clear(); + for (worktree_id, builder) in builders { + if let Ok(globset) = builder.build() { + watched_paths.insert(worktree_id, globset); + } + } + cx.notify(); } } @@ -3228,24 +3287,17 @@ impl Project { let mut project_transaction = ProjectTransaction::default(); for (buffer, buffer_abs_path, language_server) in &buffers_with_paths_and_servers { - let ( - format_on_save, - remove_trailing_whitespace, - ensure_final_newline, - formatter, - tab_size, - ) = buffer.read_with(&cx, |buffer, cx| { - let settings = cx.global::(); + let settings = buffer.read_with(&cx, |buffer, cx| { let language_name = buffer.language().map(|language| language.name()); - ( - settings.format_on_save(language_name.as_deref()), - settings.remove_trailing_whitespace_on_save(language_name.as_deref()), - settings.ensure_final_newline_on_save(language_name.as_deref()), - settings.formatter(language_name.as_deref()), - settings.tab_size(language_name.as_deref()), - ) + language_settings(language_name.as_deref(), cx).clone() }); + let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save; + let ensure_final_newline = settings.ensure_final_newline_on_save; + let format_on_save = settings.format_on_save.clone(); + let formatter = settings.formatter.clone(); + let tab_size = settings.tab_size; + // First, format buffer's whitespace according to the settings. let trailing_whitespace_diff = if remove_trailing_whitespace { Some( @@ -4536,7 +4588,7 @@ impl Project { if worktree.read(cx).is_local() { cx.subscribe(worktree, |this, worktree, event, cx| match event { worktree::Event::UpdatedEntries(changes) => { - this.update_local_worktree_buffers(&worktree, cx); + this.update_local_worktree_buffers(&worktree, &changes, cx); this.update_local_worktree_language_servers(&worktree, changes, cx); } worktree::Event::UpdatedGitRepositories(updated_repos) => { @@ -4570,80 +4622,106 @@ impl Project { fn update_local_worktree_buffers( &mut self, worktree_handle: &ModelHandle, + changes: &HashMap<(Arc, ProjectEntryId), PathChange>, cx: &mut ModelContext, ) { let snapshot = worktree_handle.read(cx).snapshot(); - let mut buffers_to_delete = Vec::new(); let mut renamed_buffers = Vec::new(); + for (path, entry_id) in changes.keys() { + let worktree_id = worktree_handle.read(cx).id(); + let project_path = ProjectPath { + worktree_id, + path: path.clone(), + }; - for (buffer_id, buffer) in &self.opened_buffers { - if let Some(buffer) = buffer.upgrade(cx) { - buffer.update(cx, |buffer, cx| { - if let Some(old_file) = File::from_dyn(buffer.file()) { - if old_file.worktree != *worktree_handle { - return; - } + let buffer_id = match self.local_buffer_ids_by_entry_id.get(entry_id) { + Some(&buffer_id) => buffer_id, + None => match self.local_buffer_ids_by_path.get(&project_path) { + Some(&buffer_id) => buffer_id, + None => continue, + }, + }; - let new_file = if let Some(entry) = snapshot.entry_for_id(old_file.entry_id) - { - File { - is_local: true, - entry_id: entry.id, - mtime: entry.mtime, - path: entry.path.clone(), - worktree: worktree_handle.clone(), - is_deleted: false, - } - } else if let Some(entry) = - snapshot.entry_for_path(old_file.path().as_ref()) - { - File { - is_local: true, - entry_id: entry.id, - mtime: entry.mtime, - path: entry.path.clone(), - worktree: worktree_handle.clone(), - is_deleted: false, - } - } else { - File { - is_local: true, - entry_id: old_file.entry_id, - path: old_file.path().clone(), - mtime: old_file.mtime(), - worktree: worktree_handle.clone(), - is_deleted: true, - } - }; - - let old_path = old_file.abs_path(cx); - if new_file.abs_path(cx) != old_path { - renamed_buffers.push((cx.handle(), old_file.clone())); - } - - if new_file != *old_file { - if let Some(project_id) = self.remote_id() { - self.client - .send(proto::UpdateBufferFile { - project_id, - buffer_id: *buffer_id as u64, - file: Some(new_file.to_proto()), - }) - .log_err(); - } - - buffer.file_updated(Arc::new(new_file), cx).detach(); - } - } - }); + let open_buffer = self.opened_buffers.get(&buffer_id); + let buffer = if let Some(buffer) = open_buffer.and_then(|buffer| buffer.upgrade(cx)) { + buffer } else { - buffers_to_delete.push(*buffer_id); - } - } + self.opened_buffers.remove(&buffer_id); + self.local_buffer_ids_by_path.remove(&project_path); + self.local_buffer_ids_by_entry_id.remove(entry_id); + continue; + }; - for buffer_id in buffers_to_delete { - self.opened_buffers.remove(&buffer_id); + buffer.update(cx, |buffer, cx| { + if let Some(old_file) = File::from_dyn(buffer.file()) { + if old_file.worktree != *worktree_handle { + return; + } + + let new_file = if let Some(entry) = snapshot.entry_for_id(old_file.entry_id) { + File { + is_local: true, + entry_id: entry.id, + mtime: entry.mtime, + path: entry.path.clone(), + worktree: worktree_handle.clone(), + is_deleted: false, + } + } else if let Some(entry) = snapshot.entry_for_path(old_file.path().as_ref()) { + File { + is_local: true, + entry_id: entry.id, + mtime: entry.mtime, + path: entry.path.clone(), + worktree: worktree_handle.clone(), + is_deleted: false, + } + } else { + File { + is_local: true, + entry_id: old_file.entry_id, + path: old_file.path().clone(), + mtime: old_file.mtime(), + worktree: worktree_handle.clone(), + is_deleted: true, + } + }; + + let old_path = old_file.abs_path(cx); + if new_file.abs_path(cx) != old_path { + renamed_buffers.push((cx.handle(), old_file.clone())); + self.local_buffer_ids_by_path.remove(&project_path); + self.local_buffer_ids_by_path.insert( + ProjectPath { + worktree_id, + path: path.clone(), + }, + buffer_id, + ); + } + + if new_file.entry_id != *entry_id { + self.local_buffer_ids_by_entry_id.remove(entry_id); + self.local_buffer_ids_by_entry_id + .insert(new_file.entry_id, buffer_id); + } + + if new_file != *old_file { + if let Some(project_id) = self.remote_id() { + self.client + .send(proto::UpdateBufferFile { + project_id, + buffer_id: buffer_id as u64, + file: Some(new_file.to_proto()), + }) + .log_err(); + } + + buffer.file_updated(Arc::new(new_file), cx).detach(); + } + } + }); } for (buffer, old_file) in renamed_buffers { @@ -4656,28 +4734,42 @@ impl Project { fn update_local_worktree_language_servers( &mut self, worktree_handle: &ModelHandle, - changes: &HashMap, PathChange>, + changes: &HashMap<(Arc, ProjectEntryId), PathChange>, cx: &mut ModelContext, ) { + if changes.is_empty() { + return; + } + let worktree_id = worktree_handle.read(cx).id(); + let mut language_server_ids = self + .language_server_ids + .iter() + .filter_map(|((server_worktree_id, _), server_id)| { + (*server_worktree_id == worktree_id).then_some(*server_id) + }) + .collect::>(); + language_server_ids.sort(); + language_server_ids.dedup(); + let abs_path = worktree_handle.read(cx).abs_path(); - for ((server_worktree_id, _), server_id) in &self.language_server_ids { - if *server_worktree_id == worktree_id { - if let Some(server) = self.language_servers.get(server_id) { - if let LanguageServerState::Running { - server, - watched_paths, - .. - } = server - { + for server_id in &language_server_ids { + if let Some(server) = self.language_servers.get(server_id) { + if let LanguageServerState::Running { + server, + watched_paths, + .. + } = server + { + if let Some(watched_paths) = watched_paths.get(&worktree_id) { let params = lsp::DidChangeWatchedFilesParams { changes: changes .iter() - .filter_map(|(path, change)| { - let path = abs_path.join(path); - if watched_paths.matches(&path) { + .filter_map(|((path, _), change)| { + if watched_paths.is_match(&path) { Some(lsp::FileEvent { - uri: lsp::Url::from_file_path(path).unwrap(), + uri: lsp::Url::from_file_path(abs_path.join(path)) + .unwrap(), typ: match change { PathChange::Added => lsp::FileChangeType::CREATED, PathChange::Removed => lsp::FileChangeType::DELETED, @@ -5098,6 +5190,9 @@ impl Project { mut cx: AsyncAppContext, ) -> Result { let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id); + + this.update(&mut cx, |_, cx| cx.emit(Event::DeletedEntry(entry_id))); + let worktree = this.read_with(&cx, |this, cx| { this.worktree_for_entry(entry_id, cx) .ok_or_else(|| anyhow!("worktree not found")) diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs new file mode 100644 index 0000000000..92e8cfcca7 --- /dev/null +++ b/crates/project/src/project_settings.rs @@ -0,0 +1,31 @@ +use collections::HashMap; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::Setting; +use std::sync::Arc; + +#[derive(Clone, Serialize, Deserialize, JsonSchema)] +pub struct ProjectSettings { + #[serde(default)] + pub lsp: HashMap, LspSettings>, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct LspSettings { + pub initialization_options: Option, +} + +impl Setting for ProjectSettings { + const KEY: Option<&'static str> = None; + + type FileContent = Self; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &gpui::AppContext, + ) -> anyhow::Result { + Self::load_via_json_merge(default_value, user_values) + } +} diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 894b27f2ee..69bcea8ce0 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1,10 +1,10 @@ use crate::{worktree::WorktreeHandle, Event, *}; -use fs::LineEnding; -use fs::{FakeFs, RealFs}; +use fs::{FakeFs, LineEnding, RealFs}; use futures::{future, StreamExt}; -use gpui::AppContext; -use gpui::{executor::Deterministic, test::subscribe}; +use globset::Glob; +use gpui::{executor::Deterministic, test::subscribe, AppContext}; use language::{ + language_settings::{AllLanguageSettings, LanguageSettingsContent}, tree_sitter_rust, tree_sitter_typescript, Diagnostic, FakeLspAdapter, LanguageConfig, OffsetRangeExt, Point, ToPoint, }; @@ -26,6 +26,9 @@ fn init_logger() { #[gpui::test] async fn test_symlinks(cx: &mut gpui::TestAppContext) { + init_test(cx); + cx.foreground().allow_parking(); + let dir = temp_tree(json!({ "root": { "apple": "", @@ -65,7 +68,7 @@ async fn test_managing_language_servers( deterministic: Arc, cx: &mut gpui::TestAppContext, ) { - cx.foreground().forbid_parking(); + init_test(cx); let mut rust_language = Language::new( LanguageConfig { @@ -451,7 +454,7 @@ async fn test_managing_language_servers( #[gpui::test] async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); + init_test(cx); let mut language = Language::new( LanguageConfig { @@ -503,7 +506,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon register_options: serde_json::to_value( lsp::DidChangeWatchedFilesRegistrationOptions { watchers: vec![lsp::FileSystemWatcher { - glob_pattern: "*.{rs,c}".to_string(), + glob_pattern: "/the-root/*.{rs,c}".to_string(), kind: None, }], }, @@ -556,7 +559,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon #[gpui::test] async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); + init_test(cx); let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -648,7 +651,7 @@ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); + init_test(cx); let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -719,7 +722,7 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); + init_test(cx); let progress_token = "the-progress-token"; let mut language = Language::new( @@ -847,7 +850,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); + init_test(cx); let progress_token = "the-progress-token"; let mut language = Language::new( @@ -925,7 +928,7 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC #[gpui::test] async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); + init_test(cx); let mut language = Language::new( LanguageConfig { @@ -973,11 +976,8 @@ async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::T } #[gpui::test] -async fn test_toggling_enable_language_server( - deterministic: Arc, - cx: &mut gpui::TestAppContext, -) { - deterministic.forbid_parking(); +async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) { + init_test(cx); let mut rust = Language::new( LanguageConfig { @@ -1051,14 +1051,16 @@ async fn test_toggling_enable_language_server( // Disable Rust language server, ensuring only that server gets stopped. cx.update(|cx| { - cx.update_global(|settings: &mut Settings, _| { - settings.language_overrides.insert( - Arc::from("Rust"), - settings::EditorSettings { - enable_language_server: Some(false), - ..Default::default() - }, - ); + cx.update_global(|settings: &mut SettingsStore, cx| { + settings.update_user_settings::(cx, |settings| { + settings.languages.insert( + Arc::from("Rust"), + LanguageSettingsContent { + enable_language_server: Some(false), + ..Default::default() + }, + ); + }); }) }); fake_rust_server_1 @@ -1068,21 +1070,23 @@ async fn test_toggling_enable_language_server( // Enable Rust and disable JavaScript language servers, ensuring that the // former gets started again and that the latter stops. cx.update(|cx| { - cx.update_global(|settings: &mut Settings, _| { - settings.language_overrides.insert( - Arc::from("Rust"), - settings::EditorSettings { - enable_language_server: Some(true), - ..Default::default() - }, - ); - settings.language_overrides.insert( - Arc::from("JavaScript"), - settings::EditorSettings { - enable_language_server: Some(false), - ..Default::default() - }, - ); + cx.update_global(|settings: &mut SettingsStore, cx| { + settings.update_user_settings::(cx, |settings| { + settings.languages.insert( + Arc::from("Rust"), + LanguageSettingsContent { + enable_language_server: Some(true), + ..Default::default() + }, + ); + settings.languages.insert( + Arc::from("JavaScript"), + LanguageSettingsContent { + enable_language_server: Some(false), + ..Default::default() + }, + ); + }); }) }); let mut fake_rust_server_2 = fake_rust_servers.next().await.unwrap(); @@ -1102,7 +1106,7 @@ async fn test_toggling_enable_language_server( #[gpui::test] async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); + init_test(cx); let mut language = Language::new( LanguageConfig { @@ -1388,7 +1392,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); + init_test(cx); let text = concat!( "let one = ;\n", // @@ -1457,9 +1461,7 @@ async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppContext) { - println!("hello from stdout"); - eprintln!("hello from stderr"); - cx.foreground().forbid_parking(); + init_test(cx); let fs = FakeFs::new(cx.background()); fs.insert_tree("/dir", json!({ "a.rs": "one two three" })) @@ -1515,7 +1517,7 @@ async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppC #[gpui::test] async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); + init_test(cx); let mut language = Language::new( LanguageConfig { @@ -1673,7 +1675,7 @@ async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_edits_from_lsp_with_edits_on_adjacent_lines(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); + init_test(cx); let text = " use a::b; @@ -1781,7 +1783,7 @@ async fn test_edits_from_lsp_with_edits_on_adjacent_lines(cx: &mut gpui::TestApp #[gpui::test] async fn test_invalid_edits_from_lsp(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); + init_test(cx); let text = " use a::b; @@ -1902,6 +1904,8 @@ fn chunks_with_diagnostics( #[gpui::test(iterations = 10)] async fn test_definition(cx: &mut gpui::TestAppContext) { + init_test(cx); + let mut language = Language::new( LanguageConfig { name: "Rust".into(), @@ -2001,6 +2005,8 @@ async fn test_definition(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) { + init_test(cx); + let mut language = Language::new( LanguageConfig { name: "TypeScript".into(), @@ -2085,6 +2091,8 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) { + init_test(cx); + let mut language = Language::new( LanguageConfig { name: "TypeScript".into(), @@ -2138,6 +2146,8 @@ async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) { #[gpui::test(iterations = 10)] async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) { + init_test(cx); + let mut language = Language::new( LanguageConfig { name: "TypeScript".into(), @@ -2254,6 +2264,8 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) { #[gpui::test(iterations = 10)] async fn test_save_file(cx: &mut gpui::TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background()); fs.insert_tree( "/dir", @@ -2284,6 +2296,8 @@ async fn test_save_file(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background()); fs.insert_tree( "/dir", @@ -2313,6 +2327,8 @@ async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_save_as(cx: &mut gpui::TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background()); fs.insert_tree("/dir", json!({})).await; @@ -2373,6 +2389,9 @@ async fn test_rescan_and_remote_updates( deterministic: Arc, cx: &mut gpui::TestAppContext, ) { + init_test(cx); + cx.foreground().allow_parking(); + let dir = temp_tree(json!({ "a": { "file1": "", @@ -2529,6 +2548,8 @@ async fn test_buffer_identity_across_renames( deterministic: Arc, cx: &mut gpui::TestAppContext, ) { + init_test(cx); + let fs = FakeFs::new(cx.background()); fs.insert_tree( "/dir", @@ -2577,6 +2598,8 @@ async fn test_buffer_identity_across_renames( #[gpui::test] async fn test_buffer_deduping(cx: &mut gpui::TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background()); fs.insert_tree( "/dir", @@ -2621,6 +2644,8 @@ async fn test_buffer_deduping(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background()); fs.insert_tree( "/dir", @@ -2765,6 +2790,8 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) { + init_test(cx); + let initial_contents = "aaa\nbbbbb\nc\n"; let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -2844,6 +2871,8 @@ async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background()); fs.insert_tree( "/dir", @@ -2904,7 +2933,7 @@ async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); + init_test(cx); let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -3146,7 +3175,7 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_rename(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); + init_test(cx); let mut language = Language::new( LanguageConfig { @@ -3284,6 +3313,8 @@ async fn test_rename(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_search(cx: &mut gpui::TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background()); fs.insert_tree( "/dir", @@ -3339,6 +3370,8 @@ async fn test_search(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) { + init_test(cx); + let search_query = "file"; let fs = FakeFs::new(cx.background()); @@ -3361,7 +3394,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) { search_query, false, true, - vec![glob::Pattern::new("*.odd").unwrap()], + vec![Glob::new("*.odd").unwrap().compile_matcher()], Vec::new() ), cx @@ -3379,7 +3412,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) { search_query, false, true, - vec![glob::Pattern::new("*.rs").unwrap()], + vec![Glob::new("*.rs").unwrap().compile_matcher()], Vec::new() ), cx @@ -3401,8 +3434,8 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) { false, true, vec![ - glob::Pattern::new("*.ts").unwrap(), - glob::Pattern::new("*.odd").unwrap(), + Glob::new("*.ts").unwrap().compile_matcher(), + Glob::new("*.odd").unwrap().compile_matcher(), ], Vec::new() ), @@ -3425,9 +3458,9 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) { false, true, vec![ - glob::Pattern::new("*.rs").unwrap(), - glob::Pattern::new("*.ts").unwrap(), - glob::Pattern::new("*.odd").unwrap(), + Glob::new("*.rs").unwrap().compile_matcher(), + Glob::new("*.ts").unwrap().compile_matcher(), + Glob::new("*.odd").unwrap().compile_matcher(), ], Vec::new() ), @@ -3447,6 +3480,8 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) { + init_test(cx); + let search_query = "file"; let fs = FakeFs::new(cx.background()); @@ -3470,7 +3505,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) { false, true, Vec::new(), - vec![glob::Pattern::new("*.odd").unwrap()], + vec![Glob::new("*.odd").unwrap().compile_matcher()], ), cx ) @@ -3493,7 +3528,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) { false, true, Vec::new(), - vec![glob::Pattern::new("*.rs").unwrap()], + vec![Glob::new("*.rs").unwrap().compile_matcher()], ), cx ) @@ -3515,8 +3550,8 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) { true, Vec::new(), vec![ - glob::Pattern::new("*.ts").unwrap(), - glob::Pattern::new("*.odd").unwrap(), + Glob::new("*.ts").unwrap().compile_matcher(), + Glob::new("*.odd").unwrap().compile_matcher(), ], ), cx @@ -3539,9 +3574,9 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) { true, Vec::new(), vec![ - glob::Pattern::new("*.rs").unwrap(), - glob::Pattern::new("*.ts").unwrap(), - glob::Pattern::new("*.odd").unwrap(), + Glob::new("*.rs").unwrap().compile_matcher(), + Glob::new("*.ts").unwrap().compile_matcher(), + Glob::new("*.odd").unwrap().compile_matcher(), ], ), cx @@ -3554,6 +3589,8 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContext) { + init_test(cx); + let search_query = "file"; let fs = FakeFs::new(cx.background()); @@ -3576,8 +3613,8 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex search_query, false, true, - vec![glob::Pattern::new("*.odd").unwrap()], - vec![glob::Pattern::new("*.odd").unwrap()], + vec![Glob::new("*.odd").unwrap().compile_matcher()], + vec![Glob::new("*.odd").unwrap().compile_matcher()], ), cx ) @@ -3594,8 +3631,8 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex search_query, false, true, - vec![glob::Pattern::new("*.ts").unwrap()], - vec![glob::Pattern::new("*.ts").unwrap()], + vec![Glob::new("*.ts").unwrap().compile_matcher()], + vec![Glob::new("*.ts").unwrap().compile_matcher()], ), cx ) @@ -3613,12 +3650,12 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex false, true, vec![ - glob::Pattern::new("*.ts").unwrap(), - glob::Pattern::new("*.odd").unwrap() + Glob::new("*.ts").unwrap().compile_matcher(), + Glob::new("*.odd").unwrap().compile_matcher() ], vec![ - glob::Pattern::new("*.ts").unwrap(), - glob::Pattern::new("*.odd").unwrap() + Glob::new("*.ts").unwrap().compile_matcher(), + Glob::new("*.odd").unwrap().compile_matcher() ], ), cx @@ -3637,12 +3674,12 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex false, true, vec![ - glob::Pattern::new("*.ts").unwrap(), - glob::Pattern::new("*.odd").unwrap() + Glob::new("*.ts").unwrap().compile_matcher(), + Glob::new("*.odd").unwrap().compile_matcher() ], vec![ - glob::Pattern::new("*.rs").unwrap(), - glob::Pattern::new("*.odd").unwrap() + Glob::new("*.rs").unwrap().compile_matcher(), + Glob::new("*.odd").unwrap().compile_matcher() ], ), cx @@ -3680,3 +3717,13 @@ async fn search( }) .collect()) } + +fn init_test(cx: &mut gpui::TestAppContext) { + cx.foreground().forbid_parking(); + + cx.update(|cx| { + cx.set_global(SettingsStore::test(cx)); + language::init(cx); + Project::init_settings(cx); + }); +} diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index ed139c97d3..4b4126fef2 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -1,6 +1,7 @@ use aho_corasick::{AhoCorasick, AhoCorasickBuilder}; use anyhow::Result; use client::proto; +use globset::{Glob, GlobMatcher}; use itertools::Itertools; use language::{char_kind, Rope}; use regex::{Regex, RegexBuilder}; @@ -19,8 +20,8 @@ pub enum SearchQuery { query: Arc, whole_word: bool, case_sensitive: bool, - files_to_include: Vec, - files_to_exclude: Vec, + files_to_include: Vec, + files_to_exclude: Vec, }, Regex { regex: Regex, @@ -28,8 +29,8 @@ pub enum SearchQuery { multiline: bool, whole_word: bool, case_sensitive: bool, - files_to_include: Vec, - files_to_exclude: Vec, + files_to_include: Vec, + files_to_exclude: Vec, }, } @@ -38,8 +39,8 @@ impl SearchQuery { query: impl ToString, whole_word: bool, case_sensitive: bool, - files_to_include: Vec, - files_to_exclude: Vec, + files_to_include: Vec, + files_to_exclude: Vec, ) -> Self { let query = query.to_string(); let search = AhoCorasickBuilder::new() @@ -60,8 +61,8 @@ impl SearchQuery { query: impl ToString, whole_word: bool, case_sensitive: bool, - files_to_include: Vec, - files_to_exclude: Vec, + files_to_include: Vec, + files_to_exclude: Vec, ) -> Result { let mut query = query.to_string(); let initial_query = Arc::from(query.as_str()); @@ -95,40 +96,16 @@ impl SearchQuery { message.query, message.whole_word, message.case_sensitive, - message - .files_to_include - .split(',') - .map(str::trim) - .filter(|glob_str| !glob_str.is_empty()) - .map(|glob_str| glob::Pattern::new(glob_str)) - .collect::>()?, - message - .files_to_exclude - .split(',') - .map(str::trim) - .filter(|glob_str| !glob_str.is_empty()) - .map(|glob_str| glob::Pattern::new(glob_str)) - .collect::>()?, + deserialize_globs(&message.files_to_include)?, + deserialize_globs(&message.files_to_exclude)?, ) } else { Ok(Self::text( message.query, message.whole_word, message.case_sensitive, - message - .files_to_include - .split(',') - .map(str::trim) - .filter(|glob_str| !glob_str.is_empty()) - .map(|glob_str| glob::Pattern::new(glob_str)) - .collect::>()?, - message - .files_to_exclude - .split(',') - .map(str::trim) - .filter(|glob_str| !glob_str.is_empty()) - .map(|glob_str| glob::Pattern::new(glob_str)) - .collect::>()?, + deserialize_globs(&message.files_to_include)?, + deserialize_globs(&message.files_to_exclude)?, )) } } @@ -143,12 +120,12 @@ impl SearchQuery { files_to_include: self .files_to_include() .iter() - .map(ToString::to_string) + .map(|g| g.glob().to_string()) .join(","), files_to_exclude: self .files_to_exclude() .iter() - .map(ToString::to_string) + .map(|g| g.glob().to_string()) .join(","), } } @@ -289,7 +266,7 @@ impl SearchQuery { matches!(self, Self::Regex { .. }) } - pub fn files_to_include(&self) -> &[glob::Pattern] { + pub fn files_to_include(&self) -> &[GlobMatcher] { match self { Self::Text { files_to_include, .. @@ -300,7 +277,7 @@ impl SearchQuery { } } - pub fn files_to_exclude(&self) -> &[glob::Pattern] { + pub fn files_to_exclude(&self) -> &[GlobMatcher] { match self { Self::Text { files_to_exclude, .. @@ -317,14 +294,23 @@ impl SearchQuery { !self .files_to_exclude() .iter() - .any(|exclude_glob| exclude_glob.matches_path(file_path)) + .any(|exclude_glob| exclude_glob.is_match(file_path)) && (self.files_to_include().is_empty() || self .files_to_include() .iter() - .any(|include_glob| include_glob.matches_path(file_path))) + .any(|include_glob| include_glob.is_match(file_path))) } None => self.files_to_include().is_empty(), } } } + +fn deserialize_globs(glob_set: &str) -> Result> { + glob_set + .split(',') + .map(str::trim) + .filter(|glob_str| !glob_str.is_empty()) + .map(|glob_str| Ok(Glob::new(glob_str)?.compile_matcher())) + .collect() +} diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 0f3092ca41..7bd9ce8aec 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -1,10 +1,7 @@ -use std::path::PathBuf; - -use gpui::{ModelContext, ModelHandle, WeakModelHandle}; -use settings::Settings; -use terminal::{Terminal, TerminalBuilder}; - use crate::Project; +use gpui::{ModelContext, ModelHandle, WeakModelHandle}; +use std::path::PathBuf; +use terminal::{Terminal, TerminalBuilder, TerminalSettings}; pub struct Terminals { pub(crate) local_handles: Vec>, @@ -22,17 +19,14 @@ impl Project { "creating terminals as a guest is not supported yet" )); } else { - let settings = cx.global::(); - let shell = settings.terminal_shell(); - let envs = settings.terminal_env(); - let scroll = settings.terminal_scroll(); + let settings = settings::get::(cx); let terminal = TerminalBuilder::new( working_directory.clone(), - shell, - envs, - settings.terminal_overrides.blinking.clone(), - scroll, + settings.shell.clone(), + settings.env.clone(), + Some(settings.blinking.clone()), + settings.alternate_scroll, window_id, ) .map(|builder| { diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 554304f3d3..4f898aa91d 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -6,7 +6,10 @@ use anyhow::{anyhow, Context, Result}; use client::{proto, Client}; use clock::ReplicaId; use collections::{HashMap, VecDeque}; -use fs::{repository::GitRepository, Fs, LineEnding}; +use fs::{ + repository::{GitFileStatus, GitRepository, RepoPath, RepoPathDescendants}, + Fs, LineEnding, +}; use futures::{ channel::{ mpsc::{self, UnboundedSender}, @@ -52,7 +55,7 @@ use std::{ time::{Duration, SystemTime}, }; use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet}; -use util::{paths::HOME, ResultExt, TryFutureExt}; +use util::{paths::HOME, ResultExt, TakeUntilExt, TryFutureExt}; #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)] pub struct WorktreeId(usize); @@ -117,10 +120,19 @@ pub struct Snapshot { completed_scan_id: usize, } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct RepositoryEntry { pub(crate) work_directory: WorkDirectoryEntry, pub(crate) branch: Option>, + pub(crate) statuses: TreeMap, +} + +fn read_git_status(git_status: i32) -> Option { + proto::GitStatus::from_i32(git_status).map(|status| match status { + proto::GitStatus::Added => GitFileStatus::Added, + proto::GitStatus::Modified => GitFileStatus::Modified, + proto::GitStatus::Conflict => GitFileStatus::Conflict, + }) } impl RepositoryEntry { @@ -138,8 +150,101 @@ impl RepositoryEntry { .map(|entry| RepositoryWorkDirectory(entry.path.clone())) } - pub(crate) fn contains(&self, snapshot: &Snapshot, path: &Path) -> bool { - self.work_directory.contains(snapshot, path) + pub fn status_for_path(&self, snapshot: &Snapshot, path: &Path) -> Option { + self.work_directory + .relativize(snapshot, path) + .and_then(|repo_path| { + self.statuses + .iter_from(&repo_path) + .take_while(|(key, _)| key.starts_with(&repo_path)) + // Short circut once we've found the highest level + .take_until(|(_, status)| status == &&GitFileStatus::Conflict) + .map(|(_, status)| status) + .reduce( + |status_first, status_second| match (status_first, status_second) { + (GitFileStatus::Conflict, _) | (_, GitFileStatus::Conflict) => { + &GitFileStatus::Conflict + } + (GitFileStatus::Modified, _) | (_, GitFileStatus::Modified) => { + &GitFileStatus::Modified + } + _ => &GitFileStatus::Added, + }, + ) + .copied() + }) + } + + #[cfg(any(test, feature = "test-support"))] + pub fn status_for_file(&self, snapshot: &Snapshot, path: &Path) -> Option { + self.work_directory + .relativize(snapshot, path) + .and_then(|repo_path| (&self.statuses).get(&repo_path)) + .cloned() + } + + pub fn build_update(&self, other: &Self) -> proto::RepositoryEntry { + let mut updated_statuses: Vec = Vec::new(); + let mut removed_statuses: Vec = Vec::new(); + + let mut self_statuses = self.statuses.iter().peekable(); + let mut other_statuses = other.statuses.iter().peekable(); + loop { + match (self_statuses.peek(), other_statuses.peek()) { + (Some((self_repo_path, self_status)), Some((other_repo_path, other_status))) => { + match Ord::cmp(self_repo_path, other_repo_path) { + Ordering::Less => { + updated_statuses.push(make_status_entry(self_repo_path, self_status)); + self_statuses.next(); + } + Ordering::Equal => { + if self_status != other_status { + updated_statuses + .push(make_status_entry(self_repo_path, self_status)); + } + + self_statuses.next(); + other_statuses.next(); + } + Ordering::Greater => { + removed_statuses.push(make_repo_path(other_repo_path)); + other_statuses.next(); + } + } + } + (Some((self_repo_path, self_status)), None) => { + updated_statuses.push(make_status_entry(self_repo_path, self_status)); + self_statuses.next(); + } + (None, Some((other_repo_path, _))) => { + removed_statuses.push(make_repo_path(other_repo_path)); + other_statuses.next(); + } + (None, None) => break, + } + } + + proto::RepositoryEntry { + work_directory_id: self.work_directory_id().to_proto(), + branch: self.branch.as_ref().map(|str| str.to_string()), + removed_repo_paths: removed_statuses, + updated_statuses, + } + } +} + +fn make_repo_path(path: &RepoPath) -> String { + path.as_os_str().to_string_lossy().to_string() +} + +fn make_status_entry(path: &RepoPath, status: &GitFileStatus) -> proto::StatusEntry { + proto::StatusEntry { + repo_path: make_repo_path(path), + status: match status { + GitFileStatus::Added => proto::GitStatus::Added.into(), + GitFileStatus::Modified => proto::GitStatus::Modified.into(), + GitFileStatus::Conflict => proto::GitStatus::Conflict.into(), + }, } } @@ -148,6 +253,12 @@ impl From<&RepositoryEntry> for proto::RepositoryEntry { proto::RepositoryEntry { work_directory_id: value.work_directory.to_proto(), branch: value.branch.as_ref().map(|str| str.to_string()), + updated_statuses: value + .statuses + .iter() + .map(|(repo_path, status)| make_status_entry(repo_path, status)) + .collect(), + removed_repo_paths: Default::default(), } } } @@ -162,23 +273,21 @@ impl Default for RepositoryWorkDirectory { } } +impl AsRef for RepositoryWorkDirectory { + fn as_ref(&self) -> &Path { + self.0.as_ref() + } +} + #[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] pub struct WorkDirectoryEntry(ProjectEntryId); impl WorkDirectoryEntry { - // Note that these paths should be relative to the worktree root. - pub(crate) fn contains(&self, snapshot: &Snapshot, path: &Path) -> bool { - snapshot - .entry_for_id(self.0) - .map(|entry| path.starts_with(&entry.path)) - .unwrap_or(false) - } - pub(crate) fn relativize(&self, worktree: &Snapshot, path: &Path) -> Option { worktree.entry_for_id(self.0).and_then(|entry| { path.strip_prefix(&entry.path) .ok() - .map(move |path| RepoPath(path.to_owned())) + .map(move |path| path.into()) }) } } @@ -197,43 +306,30 @@ impl<'a> From for WorkDirectoryEntry { } } -#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] -pub struct RepoPath(PathBuf); - -impl AsRef for RepoPath { - fn as_ref(&self) -> &Path { - self.0.as_ref() - } -} - -impl Deref for RepoPath { - type Target = PathBuf; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl AsRef for RepositoryWorkDirectory { - fn as_ref(&self) -> &Path { - self.0.as_ref() - } -} - #[derive(Debug, Clone)] pub struct LocalSnapshot { - ignores_by_parent_abs_path: HashMap, (Arc, usize)>, - // The ProjectEntryId corresponds to the entry for the .git dir - // work_directory_id - git_repositories: TreeMap, - removed_entry_ids: HashMap, - next_entry_id: Arc, snapshot: Snapshot, + /// All of the gitignore files in the worktree, indexed by their relative path. + /// The boolean indicates whether the gitignore needs to be updated. + ignores_by_parent_abs_path: HashMap, (Arc, bool)>, + /// All of the git repositories in the worktree, indexed by the project entry + /// id of their parent directory. + git_repositories: TreeMap, +} + +pub struct LocalMutableSnapshot { + snapshot: LocalSnapshot, + /// The ids of all of the entries that were removed from the snapshot + /// as part of the current update. These entry ids may be re-used + /// if the same inode is discovered at a new path, or if the given + /// path is re-created after being deleted. + removed_entry_ids: HashMap, } #[derive(Debug, Clone)] pub struct LocalRepositoryEntry { pub(crate) scan_id: usize, + pub(crate) full_scan_id: usize, pub(crate) repo_ptr: Arc>, /// Path to the actual .git folder. /// Note: if .git is a file, this points to the folder indicated by the .git file @@ -261,11 +357,25 @@ impl DerefMut for LocalSnapshot { } } +impl Deref for LocalMutableSnapshot { + type Target = LocalSnapshot; + + fn deref(&self) -> &Self::Target { + &self.snapshot + } +} + +impl DerefMut for LocalMutableSnapshot { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.snapshot + } +} + enum ScanState { Started, Updated { snapshot: LocalSnapshot, - changes: HashMap, PathChange>, + changes: HashMap<(Arc, ProjectEntryId), PathChange>, barrier: Option, scanning: bool, }, @@ -279,7 +389,7 @@ struct ShareState { } pub enum Event { - UpdatedEntries(HashMap, PathChange>), + UpdatedEntries(HashMap<(Arc, ProjectEntryId), PathChange>), UpdatedGitRepositories(HashMap, LocalRepositoryEntry>), } @@ -311,9 +421,7 @@ impl Worktree { let mut snapshot = LocalSnapshot { ignores_by_parent_abs_path: Default::default(), - removed_entry_ids: Default::default(), git_repositories: Default::default(), - next_entry_id, snapshot: Snapshot { id: WorktreeId::from_usize(cx.model_id()), abs_path: abs_path.clone(), @@ -332,7 +440,7 @@ impl Worktree { Entry::new( Arc::from(Path::new("")), &metadata, - &snapshot.next_entry_id, + &next_entry_id, snapshot.root_char_bag, ), fs.as_ref(), @@ -376,6 +484,7 @@ impl Worktree { let events = fs.watch(&abs_path, Duration::from_millis(100)).await; BackgroundScanner::new( snapshot, + next_entry_id, fs, scan_states_tx, background, @@ -796,7 +905,7 @@ impl LocalWorktree { let mut index_task = None; - if let Some(repo) = snapshot.repo_for(&path) { + if let Some(repo) = snapshot.repository_for_path(&path) { let repo_path = repo.work_directory.relativize(self, &path).unwrap(); if let Some(repo) = self.git_repositories.get(&*repo.work_directory) { let repo = repo.repo_ptr.to_owned(); @@ -1123,8 +1232,6 @@ impl LocalWorktree { let mut share_tx = Some(share_tx); let mut prev_snapshot = LocalSnapshot { ignores_by_parent_abs_path: Default::default(), - removed_entry_ids: Default::default(), - next_entry_id: Default::default(), git_repositories: Default::default(), snapshot: Snapshot { id: WorktreeId(worktree_id as usize), @@ -1424,13 +1531,41 @@ impl Snapshot { }); for repository in update.updated_repositories { - let repository = RepositoryEntry { - work_directory: ProjectEntryId::from_proto(repository.work_directory_id).into(), - branch: repository.branch.map(Into::into), - }; - if let Some(entry) = self.entry_for_id(repository.work_directory_id()) { - self.repository_entries - .insert(RepositoryWorkDirectory(entry.path.clone()), repository) + let work_directory_entry: WorkDirectoryEntry = + ProjectEntryId::from_proto(repository.work_directory_id).into(); + + if let Some(entry) = self.entry_for_id(*work_directory_entry) { + let mut statuses = TreeMap::default(); + for status_entry in repository.updated_statuses { + let Some(git_file_status) = read_git_status(status_entry.status) else { + continue; + }; + + let repo_path = RepoPath::new(status_entry.repo_path.into()); + statuses.insert(repo_path, git_file_status); + } + + let work_directory = RepositoryWorkDirectory(entry.path.clone()); + if self.repository_entries.get(&work_directory).is_some() { + self.repository_entries.update(&work_directory, |repo| { + repo.branch = repository.branch.map(Into::into); + repo.statuses.insert_tree(statuses); + + for repo_path in repository.removed_repo_paths { + let repo_path = RepoPath::new(repo_path.into()); + repo.statuses.remove(&repo_path); + } + }); + } else { + self.repository_entries.insert( + work_directory, + RepositoryEntry { + work_directory: work_directory_entry, + branch: repository.branch.map(Into::into), + statuses, + }, + ) + } } else { log::error!("no work directory entry for repository {:?}", repository) } @@ -1498,8 +1633,63 @@ impl Snapshot { self.traverse_from_offset(true, include_ignored, 0) } - pub fn repositories(&self) -> impl Iterator { - self.repository_entries.values() + pub fn repositories(&self) -> impl Iterator, &RepositoryEntry)> { + self.repository_entries + .iter() + .map(|(path, entry)| (&path.0, entry)) + } + + /// Get the repository whose work directory contains the given path. + pub fn repository_for_work_directory(&self, path: &Path) -> Option { + self.repository_entries + .get(&RepositoryWorkDirectory(path.into())) + .cloned() + } + + /// Get the repository whose work directory contains the given path. + pub fn repository_for_path(&self, path: &Path) -> Option { + let mut max_len = 0; + let mut current_candidate = None; + for (work_directory, repo) in (&self.repository_entries).iter() { + if path.starts_with(&work_directory.0) { + if work_directory.0.as_os_str().len() >= max_len { + current_candidate = Some(repo); + max_len = work_directory.0.as_os_str().len(); + } else { + break; + } + } + } + + current_candidate.cloned() + } + + /// Given an ordered iterator of entries, returns an iterator of those entries, + /// along with their containing git repository. + pub fn entries_with_repositories<'a>( + &'a self, + entries: impl 'a + Iterator, + ) -> impl 'a + Iterator)> { + let mut containing_repos = Vec::<(&Arc, &RepositoryEntry)>::new(); + let mut repositories = self.repositories().peekable(); + entries.map(move |entry| { + while let Some((repo_path, _)) = containing_repos.last() { + if !entry.path.starts_with(repo_path) { + containing_repos.pop(); + } else { + break; + } + } + while let Some((repo_path, _)) = repositories.peek() { + if entry.path.starts_with(repo_path) { + containing_repos.push(repositories.next().unwrap()); + } else { + break; + } + } + let repo = containing_repos.last().map(|(_, repo)| *repo); + (entry, repo) + }) } pub fn paths(&self) -> impl Iterator> { @@ -1524,6 +1714,30 @@ impl Snapshot { } } + fn descendent_entries<'a>( + &'a self, + include_dirs: bool, + include_ignored: bool, + parent_path: &'a Path, + ) -> DescendentEntriesIter<'a> { + let mut cursor = self.entries_by_path.cursor(); + cursor.seek(&TraversalTarget::Path(parent_path), Bias::Left, &()); + let mut traversal = Traversal { + cursor, + include_dirs, + include_ignored, + }; + + if traversal.end_offset() == traversal.start_offset() { + traversal.advance(); + } + + DescendentEntriesIter { + traversal, + parent_path, + } + } + pub fn root_entry(&self) -> Option<&Entry> { self.entry_for_path("") } @@ -1570,32 +1784,17 @@ impl Snapshot { } impl LocalSnapshot { - pub(crate) fn repo_for(&self, path: &Path) -> Option { - let mut max_len = 0; - let mut current_candidate = None; - for (work_directory, repo) in (&self.repository_entries).iter() { - if repo.contains(self, path) { - if work_directory.0.as_os_str().len() >= max_len { - current_candidate = Some(repo); - max_len = work_directory.0.as_os_str().len(); - } else { - break; - } - } - } - - current_candidate.map(|entry| entry.to_owned()) + pub(crate) fn get_local_repo(&self, repo: &RepositoryEntry) -> Option<&LocalRepositoryEntry> { + self.git_repositories.get(&repo.work_directory.0) } pub(crate) fn repo_for_metadata( &self, path: &Path, - ) -> Option<(ProjectEntryId, Arc>)> { - let (entry_id, local_repo) = self - .git_repositories + ) -> Option<(&ProjectEntryId, &LocalRepositoryEntry)> { + self.git_repositories .iter() - .find(|(_, repo)| repo.in_dot_git(path))?; - Some((*entry_id, local_repo.repo_ptr.to_owned())) + .find(|(_, repo)| repo.in_dot_git(path)) } #[cfg(test)] @@ -1685,7 +1884,7 @@ impl LocalSnapshot { } Ordering::Equal => { if self_repo != other_repo { - updated_repositories.push((*self_repo).into()); + updated_repositories.push(self_repo.build_update(other_repo)); } self_repos.next(); @@ -1728,10 +1927,8 @@ impl LocalSnapshot { let abs_path = self.abs_path.join(&entry.path); match smol::block_on(build_gitignore(&abs_path, fs)) { Ok(ignore) => { - self.ignores_by_parent_abs_path.insert( - abs_path.parent().unwrap().into(), - (Arc::new(ignore), self.scan_id), - ); + self.ignores_by_parent_abs_path + .insert(abs_path.parent().unwrap().into(), (Arc::new(ignore), true)); } Err(error) => { log::error!( @@ -1743,8 +1940,6 @@ impl LocalSnapshot { } } - self.reuse_entry_id(&mut entry); - if entry.kind == EntryKind::PendingDir { if let Some(existing_entry) = self.entries_by_path.get(&PathKey(entry.path.clone()), &()) @@ -1773,62 +1968,6 @@ impl LocalSnapshot { entry } - fn populate_dir( - &mut self, - parent_path: Arc, - entries: impl IntoIterator, - ignore: Option>, - fs: &dyn Fs, - ) { - let mut parent_entry = if let Some(parent_entry) = - self.entries_by_path.get(&PathKey(parent_path.clone()), &()) - { - parent_entry.clone() - } else { - log::warn!( - "populating a directory {:?} that has been removed", - parent_path - ); - return; - }; - - match parent_entry.kind { - EntryKind::PendingDir => { - parent_entry.kind = EntryKind::Dir; - } - EntryKind::Dir => {} - _ => return, - } - - if let Some(ignore) = ignore { - self.ignores_by_parent_abs_path.insert( - self.abs_path.join(&parent_path).into(), - (ignore, self.scan_id), - ); - } - - if parent_path.file_name() == Some(&DOT_GIT) { - self.build_repo(parent_path, fs); - } - - let mut entries_by_path_edits = vec![Edit::Insert(parent_entry)]; - let mut entries_by_id_edits = Vec::new(); - - for mut entry in entries { - self.reuse_entry_id(&mut entry); - entries_by_id_edits.push(Edit::Insert(PathEntry { - id: entry.id, - path: entry.path.clone(), - is_ignored: entry.is_ignored, - scan_id: self.scan_id, - })); - entries_by_path_edits.push(Edit::Insert(entry)); - } - - self.entries_by_path.edit(entries_by_path_edits, &()); - self.entries_by_id.edit(entries_by_id_edits, &()); - } - fn build_repo(&mut self, parent_path: Arc, fs: &dyn Fs) -> Option<()> { let abs_path = self.abs_path.join(&parent_path); let work_dir: Arc = parent_path.parent().unwrap().into(); @@ -1852,11 +1991,13 @@ impl LocalSnapshot { let scan_id = self.scan_id; let repo_lock = repo.lock(); + self.repository_entries.insert( work_directory, RepositoryEntry { work_directory: work_dir_id.into(), branch: repo_lock.branch_name().map(Into::into), + statuses: repo_lock.statuses().unwrap_or_default(), }, ); drop(repo_lock); @@ -1865,6 +2006,7 @@ impl LocalSnapshot { work_dir_id, LocalRepositoryEntry { scan_id, + full_scan_id: scan_id, repo_ptr: repo, git_dir_path: parent_path.clone(), }, @@ -1873,46 +2015,6 @@ impl LocalSnapshot { Some(()) } - fn reuse_entry_id(&mut self, entry: &mut Entry) { - if let Some(removed_entry_id) = self.removed_entry_ids.remove(&entry.inode) { - entry.id = removed_entry_id; - } else if let Some(existing_entry) = self.entry_for_path(&entry.path) { - entry.id = existing_entry.id; - } - } - - fn remove_path(&mut self, path: &Path) { - let mut new_entries; - let removed_entries; - { - let mut cursor = self.entries_by_path.cursor::(); - new_entries = cursor.slice(&TraversalTarget::Path(path), Bias::Left, &()); - removed_entries = cursor.slice(&TraversalTarget::PathSuccessor(path), Bias::Left, &()); - new_entries.push_tree(cursor.suffix(&()), &()); - } - self.entries_by_path = new_entries; - - let mut entries_by_id_edits = Vec::new(); - for entry in removed_entries.cursor::<()>() { - let removed_entry_id = self - .removed_entry_ids - .entry(entry.inode) - .or_insert(entry.id); - *removed_entry_id = cmp::max(*removed_entry_id, entry.id); - entries_by_id_edits.push(Edit::Remove(entry.id)); - } - self.entries_by_id.edit(entries_by_id_edits, &()); - - if path.file_name() == Some(&GITIGNORE) { - let abs_parent_path = self.abs_path.join(path.parent().unwrap()); - if let Some((_, scan_id)) = self - .ignores_by_parent_abs_path - .get_mut(abs_parent_path.as_path()) - { - *scan_id = self.snapshot.scan_id; - } - } - } fn ancestor_inodes_for_path(&self, path: &Path) -> TreeSet { let mut inodes = TreeSet::default(); @@ -1952,6 +2054,109 @@ impl LocalSnapshot { } } +impl LocalMutableSnapshot { + fn reuse_entry_id(&mut self, entry: &mut Entry) { + if let Some(removed_entry_id) = self.removed_entry_ids.remove(&entry.inode) { + entry.id = removed_entry_id; + } else if let Some(existing_entry) = self.entry_for_path(&entry.path) { + entry.id = existing_entry.id; + } + } + + fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry { + self.reuse_entry_id(&mut entry); + self.snapshot.insert_entry(entry, fs) + } + + fn populate_dir( + &mut self, + parent_path: Arc, + entries: impl IntoIterator, + ignore: Option>, + fs: &dyn Fs, + ) { + let mut parent_entry = if let Some(parent_entry) = + self.entries_by_path.get(&PathKey(parent_path.clone()), &()) + { + parent_entry.clone() + } else { + log::warn!( + "populating a directory {:?} that has been removed", + parent_path + ); + return; + }; + + match parent_entry.kind { + EntryKind::PendingDir => { + parent_entry.kind = EntryKind::Dir; + } + EntryKind::Dir => {} + _ => return, + } + + if let Some(ignore) = ignore { + let abs_parent_path = self.abs_path.join(&parent_path).into(); + self.ignores_by_parent_abs_path + .insert(abs_parent_path, (ignore, false)); + } + + if parent_path.file_name() == Some(&DOT_GIT) { + self.build_repo(parent_path, fs); + } + + let mut entries_by_path_edits = vec![Edit::Insert(parent_entry)]; + let mut entries_by_id_edits = Vec::new(); + + for mut entry in entries { + self.reuse_entry_id(&mut entry); + entries_by_id_edits.push(Edit::Insert(PathEntry { + id: entry.id, + path: entry.path.clone(), + is_ignored: entry.is_ignored, + scan_id: self.scan_id, + })); + entries_by_path_edits.push(Edit::Insert(entry)); + } + + self.entries_by_path.edit(entries_by_path_edits, &()); + self.entries_by_id.edit(entries_by_id_edits, &()); + } + + fn remove_path(&mut self, path: &Path) { + let mut new_entries; + let removed_entries; + { + let mut cursor = self.entries_by_path.cursor::(); + new_entries = cursor.slice(&TraversalTarget::Path(path), Bias::Left, &()); + removed_entries = cursor.slice(&TraversalTarget::PathSuccessor(path), Bias::Left, &()); + new_entries.push_tree(cursor.suffix(&()), &()); + } + self.entries_by_path = new_entries; + + let mut entries_by_id_edits = Vec::new(); + for entry in removed_entries.cursor::<()>() { + let removed_entry_id = self + .removed_entry_ids + .entry(entry.inode) + .or_insert(entry.id); + *removed_entry_id = cmp::max(*removed_entry_id, entry.id); + entries_by_id_edits.push(Edit::Remove(entry.id)); + } + self.entries_by_id.edit(entries_by_id_edits, &()); + + if path.file_name() == Some(&GITIGNORE) { + let abs_parent_path = self.abs_path.join(path.parent().unwrap()); + if let Some((_, needs_update)) = self + .ignores_by_parent_abs_path + .get_mut(abs_parent_path.as_path()) + { + *needs_update = true; + } + } + } +} + async fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result { let contents = fs.load(abs_path).await?; let parent = abs_path.parent().unwrap_or_else(|| Path::new("/")); @@ -2394,18 +2599,25 @@ impl<'a> sum_tree::Dimension<'a, EntrySummary> for PathKey { } struct BackgroundScanner { - snapshot: Mutex, + snapshot: Mutex, fs: Arc, status_updates_tx: UnboundedSender, executor: Arc, refresh_requests_rx: channel::Receiver<(Vec, barrier::Sender)>, - prev_state: Mutex<(Snapshot, Vec>)>, + prev_state: Mutex, + next_entry_id: Arc, finished_initial_scan: bool, } +struct BackgroundScannerState { + snapshot: Snapshot, + event_paths: Vec>, +} + impl BackgroundScanner { fn new( snapshot: LocalSnapshot, + next_entry_id: Arc, fs: Arc, status_updates_tx: UnboundedSender, executor: Arc, @@ -2416,8 +2628,15 @@ impl BackgroundScanner { status_updates_tx, executor, refresh_requests_rx, - prev_state: Mutex::new((snapshot.snapshot.clone(), Vec::new())), - snapshot: Mutex::new(snapshot), + next_entry_id, + prev_state: Mutex::new(BackgroundScannerState { + snapshot: snapshot.snapshot.clone(), + event_paths: Default::default(), + }), + snapshot: Mutex::new(LocalMutableSnapshot { + snapshot, + removed_entry_ids: Default::default(), + }), finished_initial_scan: false, } } @@ -2444,7 +2663,7 @@ impl BackgroundScanner { self.snapshot .lock() .ignores_by_parent_abs_path - .insert(ancestor.into(), (ignore.into(), 0)); + .insert(ancestor.into(), (ignore.into(), false)); } } { @@ -2497,7 +2716,7 @@ impl BackgroundScanner { // these before handling changes reported by the filesystem. request = self.refresh_requests_rx.recv().fuse() => { let Ok((paths, barrier)) = request else { break }; - if !self.process_refresh_request(paths, barrier).await { + if !self.process_refresh_request(paths.clone(), barrier).await { return; } } @@ -2508,25 +2727,37 @@ impl BackgroundScanner { while let Poll::Ready(Some(more_events)) = futures::poll!(events_rx.next()) { paths.extend(more_events.into_iter().map(|e| e.path)); } - self.process_events(paths).await; + self.process_events(paths.clone()).await; } } } } async fn process_refresh_request(&self, paths: Vec, barrier: barrier::Sender) -> bool { - self.reload_entries_for_paths(paths, None).await; + if let Some(mut paths) = self.reload_entries_for_paths(paths, None).await { + paths.sort_unstable(); + util::extend_sorted( + &mut self.prev_state.lock().event_paths, + paths, + usize::MAX, + Ord::cmp, + ); + } self.send_status_update(false, Some(barrier)) } async fn process_events(&mut self, paths: Vec) { let (scan_job_tx, scan_job_rx) = channel::unbounded(); - if let Some(mut paths) = self + let paths = self .reload_entries_for_paths(paths, Some(scan_job_tx.clone())) - .await - { - paths.sort_unstable(); - util::extend_sorted(&mut self.prev_state.lock().1, paths, usize::MAX, Ord::cmp); + .await; + if let Some(paths) = &paths { + util::extend_sorted( + &mut self.prev_state.lock().event_paths, + paths.iter().cloned(), + usize::MAX, + Ord::cmp, + ); } drop(scan_job_tx); self.scan_dirs(false, scan_job_rx).await; @@ -2535,6 +2766,12 @@ impl BackgroundScanner { let mut snapshot = self.snapshot.lock(); + if let Some(paths) = paths { + for path in paths { + self.reload_repo_for_file_path(&path, &mut *snapshot, self.fs.as_ref()); + } + } + let mut git_repositories = mem::take(&mut snapshot.git_repositories); git_repositories.retain(|work_directory_id, _| { snapshot @@ -2553,13 +2790,11 @@ impl BackgroundScanner { .is_some() }); snapshot.snapshot.repository_entries = git_repository_entries; - - snapshot.removed_entry_ids.clear(); snapshot.completed_scan_id = snapshot.scan_id; - drop(snapshot); self.send_status_update(false, None); + self.prev_state.lock().event_paths.clear(); } async fn scan_dirs( @@ -2637,14 +2872,18 @@ impl BackgroundScanner { fn send_status_update(&self, scanning: bool, barrier: Option) -> bool { let mut prev_state = self.prev_state.lock(); - let snapshot = self.snapshot.lock().clone(); - let mut old_snapshot = snapshot.snapshot.clone(); - mem::swap(&mut old_snapshot, &mut prev_state.0); - let changed_paths = mem::take(&mut prev_state.1); - let changes = self.build_change_set(&old_snapshot, &snapshot.snapshot, changed_paths); + let new_snapshot = self.snapshot.lock().clone(); + let old_snapshot = mem::replace(&mut prev_state.snapshot, new_snapshot.snapshot.clone()); + + let changes = self.build_change_set( + &old_snapshot, + &new_snapshot.snapshot, + &prev_state.event_paths, + ); + self.status_updates_tx .unbounded_send(ScanState::Updated { - snapshot, + snapshot: new_snapshot, changes, scanning, barrier, @@ -2662,7 +2901,7 @@ impl BackgroundScanner { ( snapshot.abs_path().clone(), snapshot.root_char_bag, - snapshot.next_entry_id.clone(), + self.next_entry_id.clone(), ) }; let mut child_paths = self.fs.read_dir(&job.abs_path).await?; @@ -2834,33 +3073,12 @@ impl BackgroundScanner { let mut fs_entry = Entry::new( path.clone(), &metadata, - snapshot.next_entry_id.as_ref(), + self.next_entry_id.as_ref(), snapshot.root_char_bag, ); fs_entry.is_ignored = ignore_stack.is_all(); snapshot.insert_entry(fs_entry, self.fs.as_ref()); - let scan_id = snapshot.scan_id; - - let repo_with_path_in_dotgit = snapshot.repo_for_metadata(&path); - if let Some((entry_id, repo)) = repo_with_path_in_dotgit { - let work_dir = snapshot - .entry_for_id(entry_id) - .map(|entry| RepositoryWorkDirectory(entry.path.clone()))?; - - let repo = repo.lock(); - repo.reload_index(); - let branch = repo.branch_name(); - - snapshot.git_repositories.update(&entry_id, |entry| { - entry.scan_id = scan_id; - }); - - snapshot - .repository_entries - .update(&work_dir, |entry| entry.branch = branch.map(Into::into)); - } - if let Some(scan_queue_tx) = &scan_queue_tx { let mut ancestor_inodes = snapshot.ancestor_inodes_for_path(&path); if metadata.is_dir && !ancestor_inodes.contains(&metadata.inode) { @@ -2876,7 +3094,9 @@ impl BackgroundScanner { } } } - Ok(None) => {} + Ok(None) => { + self.remove_repo_path(&path, &mut snapshot); + } Err(err) => { // TODO - create a special 'error' entry in the entries tree to mark this log::error!("error reading file on event {:?}", err); @@ -2887,22 +3107,154 @@ impl BackgroundScanner { Some(event_paths) } + fn remove_repo_path(&self, path: &Path, snapshot: &mut LocalSnapshot) -> Option<()> { + if !path + .components() + .any(|component| component.as_os_str() == *DOT_GIT) + { + let scan_id = snapshot.scan_id; + + if let Some(repository) = snapshot.repository_for_work_directory(path) { + let entry = repository.work_directory.0; + snapshot.git_repositories.remove(&entry); + snapshot + .snapshot + .repository_entries + .remove(&RepositoryWorkDirectory(path.into())); + return Some(()); + } + + let repo = snapshot.repository_for_path(&path)?; + + let repo_path = repo.work_directory.relativize(&snapshot, &path)?; + + let work_dir = repo.work_directory(snapshot)?; + let work_dir_id = repo.work_directory; + + snapshot + .git_repositories + .update(&work_dir_id, |entry| entry.scan_id = scan_id); + + snapshot.repository_entries.update(&work_dir, |entry| { + entry + .statuses + .remove_range(&repo_path, &RepoPathDescendants(&repo_path)) + }); + } + + Some(()) + } + + fn reload_repo_for_file_path( + &self, + path: &Path, + snapshot: &mut LocalSnapshot, + fs: &dyn Fs, + ) -> Option<()> { + let scan_id = snapshot.scan_id; + + if path + .components() + .any(|component| component.as_os_str() == *DOT_GIT) + { + let (entry_id, repo_ptr) = { + let Some((entry_id, repo)) = snapshot.repo_for_metadata(&path) else { + let dot_git_dir = path.ancestors() + .skip_while(|ancestor| ancestor.file_name() != Some(&*DOT_GIT)) + .next()?; + + snapshot.build_repo(dot_git_dir.into(), fs); + return None; + }; + if repo.full_scan_id == scan_id { + return None; + } + (*entry_id, repo.repo_ptr.to_owned()) + }; + + let work_dir = snapshot + .entry_for_id(entry_id) + .map(|entry| RepositoryWorkDirectory(entry.path.clone()))?; + + let repo = repo_ptr.lock(); + repo.reload_index(); + let branch = repo.branch_name(); + let statuses = repo.statuses().unwrap_or_default(); + + snapshot.git_repositories.update(&entry_id, |entry| { + entry.scan_id = scan_id; + entry.full_scan_id = scan_id; + }); + + snapshot.repository_entries.update(&work_dir, |entry| { + entry.branch = branch.map(Into::into); + entry.statuses = statuses; + }); + } else { + if snapshot + .entry_for_path(&path) + .map(|entry| entry.is_ignored) + .unwrap_or(false) + { + self.remove_repo_path(&path, snapshot); + return None; + } + + let repo = snapshot.repository_for_path(&path)?; + + let work_dir = repo.work_directory(snapshot)?; + let work_dir_id = repo.work_directory.clone(); + + snapshot + .git_repositories + .update(&work_dir_id, |entry| entry.scan_id = scan_id); + + let local_repo = snapshot.get_local_repo(&repo)?.to_owned(); + + // Short circuit if we've already scanned everything + if local_repo.full_scan_id == scan_id { + return None; + } + + let mut repository = snapshot.repository_entries.remove(&work_dir)?; + + for entry in snapshot.descendent_entries(false, false, path) { + let Some(repo_path) = repo.work_directory.relativize(snapshot, &entry.path) else { + continue; + }; + + let status = local_repo.repo_ptr.lock().status(&repo_path); + if let Some(status) = status { + repository.statuses.insert(repo_path.clone(), status); + } else { + repository.statuses.remove(&repo_path); + } + } + + snapshot.repository_entries.insert(work_dir, repository) + } + + Some(()) + } + async fn update_ignore_statuses(&self) { use futures::FutureExt as _; let mut snapshot = self.snapshot.lock().clone(); let mut ignores_to_update = Vec::new(); let mut ignores_to_delete = Vec::new(); - for (parent_abs_path, (_, scan_id)) in &snapshot.ignores_by_parent_abs_path { - if let Ok(parent_path) = parent_abs_path.strip_prefix(&snapshot.abs_path) { - if *scan_id > snapshot.completed_scan_id - && snapshot.entry_for_path(parent_path).is_some() - { - ignores_to_update.push(parent_abs_path.clone()); + let abs_path = snapshot.abs_path.clone(); + for (parent_abs_path, (_, needs_update)) in &mut snapshot.ignores_by_parent_abs_path { + if let Ok(parent_path) = parent_abs_path.strip_prefix(&abs_path) { + if *needs_update { + *needs_update = false; + if snapshot.snapshot.entry_for_path(parent_path).is_some() { + ignores_to_update.push(parent_abs_path.clone()); + } } let ignore_path = parent_path.join(&*GITIGNORE); - if snapshot.entry_for_path(ignore_path).is_none() { + if snapshot.snapshot.entry_for_path(ignore_path).is_none() { ignores_to_delete.push(parent_abs_path.clone()); } } @@ -3012,8 +3364,8 @@ impl BackgroundScanner { &self, old_snapshot: &Snapshot, new_snapshot: &Snapshot, - event_paths: Vec>, - ) -> HashMap, PathChange> { + event_paths: &[Arc], + ) -> HashMap<(Arc, ProjectEntryId), PathChange> { use PathChange::{Added, AddedOrUpdated, Removed, Updated}; let mut changes = HashMap::default(); @@ -3022,7 +3374,7 @@ impl BackgroundScanner { let received_before_initialized = !self.finished_initial_scan; for path in event_paths { - let path = PathKey(path); + let path = PathKey(path.clone()); old_paths.seek(&path, Bias::Left, &()); new_paths.seek(&path, Bias::Left, &()); @@ -3039,7 +3391,7 @@ impl BackgroundScanner { match Ord::cmp(&old_entry.path, &new_entry.path) { Ordering::Less => { - changes.insert(old_entry.path.clone(), Removed); + changes.insert((old_entry.path.clone(), old_entry.id), Removed); old_paths.next(&()); } Ordering::Equal => { @@ -3047,31 +3399,35 @@ impl BackgroundScanner { // If the worktree was not fully initialized when this event was generated, // we can't know whether this entry was added during the scan or whether // it was merely updated. - changes.insert(new_entry.path.clone(), AddedOrUpdated); + changes.insert( + (new_entry.path.clone(), new_entry.id), + AddedOrUpdated, + ); } else if old_entry.mtime != new_entry.mtime { - changes.insert(new_entry.path.clone(), Updated); + changes.insert((new_entry.path.clone(), new_entry.id), Updated); } old_paths.next(&()); new_paths.next(&()); } Ordering::Greater => { - changes.insert(new_entry.path.clone(), Added); + changes.insert((new_entry.path.clone(), new_entry.id), Added); new_paths.next(&()); } } } (Some(old_entry), None) => { - changes.insert(old_entry.path.clone(), Removed); + changes.insert((old_entry.path.clone(), old_entry.id), Removed); old_paths.next(&()); } (None, Some(new_entry)) => { - changes.insert(new_entry.path.clone(), Added); + changes.insert((new_entry.path.clone(), new_entry.id), Added); new_paths.next(&()); } (None, None) => break, } } } + changes } @@ -3212,17 +3568,13 @@ pub struct Traversal<'a> { impl<'a> Traversal<'a> { pub fn advance(&mut self) -> bool { - self.advance_to_offset(self.offset() + 1) - } - - pub fn advance_to_offset(&mut self, offset: usize) -> bool { self.cursor.seek_forward( &TraversalTarget::Count { - count: offset, + count: self.end_offset() + 1, include_dirs: self.include_dirs, include_ignored: self.include_ignored, }, - Bias::Right, + Bias::Left, &(), ) } @@ -3249,11 +3601,17 @@ impl<'a> Traversal<'a> { self.cursor.item() } - pub fn offset(&self) -> usize { + pub fn start_offset(&self) -> usize { self.cursor .start() .count(self.include_dirs, self.include_ignored) } + + pub fn end_offset(&self) -> usize { + self.cursor + .end(&()) + .count(self.include_dirs, self.include_ignored) + } } impl<'a> Iterator for Traversal<'a> { @@ -3322,6 +3680,25 @@ impl<'a> Iterator for ChildEntriesIter<'a> { } } +struct DescendentEntriesIter<'a> { + parent_path: &'a Path, + traversal: Traversal<'a>, +} + +impl<'a> Iterator for DescendentEntriesIter<'a> { + type Item = &'a Entry; + + fn next(&mut self) -> Option { + if let Some(item) = self.traversal.entry() { + if item.path.starts_with(&self.parent_path) { + self.traversal.advance(); + return Some(item); + } + } + None + } +} + impl<'a> From<&'a Entry> for proto::Entry { fn from(entry: &'a Entry) -> Self { Self { @@ -3436,6 +3813,105 @@ mod tests { }) } + #[gpui::test] + async fn test_descendent_entries(cx: &mut TestAppContext) { + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/root", + json!({ + "a": "", + "b": { + "c": { + "d": "" + }, + "e": {} + }, + "f": "", + "g": { + "h": {} + }, + "i": { + "j": { + "k": "" + }, + "l": { + + } + }, + ".gitignore": "i/j\n", + }), + ) + .await; + + let http_client = FakeHttpClient::with_404_response(); + let client = cx.read(|cx| Client::new(http_client, cx)); + + let tree = Worktree::local( + client, + Path::new("/root"), + true, + fs, + Default::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + + tree.read_with(cx, |tree, _| { + assert_eq!( + tree.descendent_entries(false, false, Path::new("b")) + .map(|entry| entry.path.as_ref()) + .collect::>(), + vec![Path::new("b/c/d"),] + ); + assert_eq!( + tree.descendent_entries(true, false, Path::new("b")) + .map(|entry| entry.path.as_ref()) + .collect::>(), + vec![ + Path::new("b"), + Path::new("b/c"), + Path::new("b/c/d"), + Path::new("b/e"), + ] + ); + + assert_eq!( + tree.descendent_entries(false, false, Path::new("g")) + .map(|entry| entry.path.as_ref()) + .collect::>(), + Vec::::new() + ); + assert_eq!( + tree.descendent_entries(true, false, Path::new("g")) + .map(|entry| entry.path.as_ref()) + .collect::>(), + vec![Path::new("g"), Path::new("g/h"),] + ); + + assert_eq!( + tree.descendent_entries(false, false, Path::new("i")) + .map(|entry| entry.path.as_ref()) + .collect::>(), + Vec::::new() + ); + assert_eq!( + tree.descendent_entries(false, true, Path::new("i")) + .map(|entry| entry.path.as_ref()) + .collect::>(), + vec![Path::new("i/j/k")] + ); + assert_eq!( + tree.descendent_entries(true, false, Path::new("i")) + .map(|entry| entry.path.as_ref()) + .collect::>(), + vec![Path::new("i"), Path::new("i/l"),] + ); + }) + } + #[gpui::test(iterations = 10)] async fn test_circular_symlinks(executor: Arc, cx: &mut TestAppContext) { let fs = FakeFs::new(cx.background()); @@ -3518,6 +3994,8 @@ mod tests { #[gpui::test] async fn test_rescan_with_gitignore(cx: &mut TestAppContext) { + // .gitignores are handled explicitly by Zed and do not use the git + // machinery that the git_tests module checks let parent_dir = temp_tree(json!({ ".gitignore": "ancestor-ignored-file1\nancestor-ignored-file2\n", "tree": { @@ -3595,97 +4073,6 @@ mod tests { }); } - #[gpui::test] - async fn test_git_repository_for_path(cx: &mut TestAppContext) { - let root = temp_tree(json!({ - "dir1": { - ".git": {}, - "deps": { - "dep1": { - ".git": {}, - "src": { - "a.txt": "" - } - } - }, - "src": { - "b.txt": "" - } - }, - "c.txt": "", - })); - - let http_client = FakeHttpClient::with_404_response(); - let client = cx.read(|cx| Client::new(http_client, cx)); - let tree = Worktree::local( - client, - root.path(), - true, - Arc::new(RealFs), - Default::default(), - &mut cx.to_async(), - ) - .await - .unwrap(); - - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - tree.flush_fs_events(cx).await; - - tree.read_with(cx, |tree, _cx| { - let tree = tree.as_local().unwrap(); - - assert!(tree.repo_for("c.txt".as_ref()).is_none()); - - let entry = tree.repo_for("dir1/src/b.txt".as_ref()).unwrap(); - assert_eq!( - entry - .work_directory(tree) - .map(|directory| directory.as_ref().to_owned()), - Some(Path::new("dir1").to_owned()) - ); - - let entry = tree.repo_for("dir1/deps/dep1/src/a.txt".as_ref()).unwrap(); - assert_eq!( - entry - .work_directory(tree) - .map(|directory| directory.as_ref().to_owned()), - Some(Path::new("dir1/deps/dep1").to_owned()) - ); - }); - - let repo_update_events = Arc::new(Mutex::new(vec![])); - tree.update(cx, |_, cx| { - let repo_update_events = repo_update_events.clone(); - cx.subscribe(&tree, move |_, _, event, _| { - if let Event::UpdatedGitRepositories(update) = event { - repo_update_events.lock().push(update.clone()); - } - }) - .detach(); - }); - - std::fs::write(root.path().join("dir1/.git/random_new_file"), "hello").unwrap(); - tree.flush_fs_events(cx).await; - - assert_eq!( - repo_update_events.lock()[0] - .keys() - .cloned() - .collect::>>(), - vec![Path::new("dir1").into()] - ); - - std::fs::remove_dir_all(root.path().join("dir1/.git")).unwrap(); - tree.flush_fs_events(cx).await; - - tree.read_with(cx, |tree, _cx| { - let tree = tree.as_local().unwrap(); - - assert!(tree.repo_for("dir1/src/b.txt".as_ref()).is_none()); - }); - } - #[gpui::test] async fn test_write_file(cx: &mut TestAppContext) { let dir = temp_tree(json!({ @@ -3911,7 +4298,7 @@ mod tests { cx.subscribe(&worktree, move |tree, _, event, _| { if let Event::UpdatedEntries(changes) = event { - for (path, change_type) in changes.iter() { + for ((path, _), change_type) in changes.iter() { let path = path.clone(); let ix = match paths.binary_search(&path) { Ok(ix) | Err(ix) => ix, @@ -3921,13 +4308,16 @@ mod tests { assert_ne!(paths.get(ix), Some(&path)); paths.insert(ix, path); } + PathChange::Removed => { assert_eq!(paths.get(ix), Some(&path)); paths.remove(ix); } + PathChange::Updated => { assert_eq!(paths.get(ix), Some(&path)); } + PathChange::AddedOrUpdated => { if paths[ix] != path { paths.insert(ix, path); @@ -3935,6 +4325,7 @@ mod tests { } } } + let new_paths = tree.paths().cloned().collect::>(); assert_eq!(paths, new_paths, "incorrect changes: {:?}", changes); } @@ -3942,15 +4333,26 @@ mod tests { .detach(); }); + fs.as_fake().pause_events(); let mut snapshots = Vec::new(); let mut mutations_len = operations; while mutations_len > 1 { - randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await; - let buffered_event_count = fs.as_fake().buffered_event_count().await; + if rng.gen_bool(0.2) { + worktree + .update(cx, |worktree, cx| { + randomly_mutate_worktree(worktree, &mut rng, cx) + }) + .await + .log_err(); + } else { + randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await; + } + + let buffered_event_count = fs.as_fake().buffered_event_count(); if buffered_event_count > 0 && rng.gen_bool(0.3) { let len = rng.gen_range(0..=buffered_event_count); log::info!("flushing {} events", len); - fs.as_fake().flush_events(len).await; + fs.as_fake().flush_events(len); } else { randomly_mutate_fs(&fs, root_dir, 0.6, &mut rng).await; mutations_len -= 1; @@ -3966,7 +4368,7 @@ mod tests { } log::info!("quiescing"); - fs.as_fake().flush_events(usize::MAX).await; + fs.as_fake().flush_events(usize::MAX); cx.foreground().run_until_parked(); let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()); snapshot.check_invariants(); @@ -4026,6 +4428,7 @@ mod tests { rng: &mut impl Rng, cx: &mut ModelContext, ) -> Task> { + log::info!("mutating worktree"); let worktree = worktree.as_local_mut().unwrap(); let snapshot = worktree.snapshot(); let entry = snapshot.entries(false).choose(rng).unwrap(); @@ -4087,6 +4490,7 @@ mod tests { insertion_probability: f64, rng: &mut impl Rng, ) { + log::info!("mutating fs"); let mut files = Vec::new(); let mut dirs = Vec::new(); for path in fs.as_fake().paths() { @@ -4321,4 +4725,478 @@ mod tests { paths } } + + mod git_tests { + use super::*; + use pretty_assertions::assert_eq; + + #[gpui::test] + async fn test_rename_work_directory(cx: &mut TestAppContext) { + let root = temp_tree(json!({ + "projects": { + "project1": { + "a": "", + "b": "", + } + }, + + })); + let root_path = root.path(); + + let http_client = FakeHttpClient::with_404_response(); + let client = cx.read(|cx| Client::new(http_client, cx)); + let tree = Worktree::local( + client, + root_path, + true, + Arc::new(RealFs), + Default::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + + let repo = git_init(&root_path.join("projects/project1")); + git_add("a", &repo); + git_commit("init", &repo); + std::fs::write(root_path.join("projects/project1/a"), "aa").ok(); + + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + + tree.flush_fs_events(cx).await; + + cx.read(|cx| { + let tree = tree.read(cx); + let (work_dir, repo) = tree.repositories().next().unwrap(); + assert_eq!(work_dir.as_ref(), Path::new("projects/project1")); + assert_eq!( + repo.status_for_file(tree, Path::new("projects/project1/a")), + Some(GitFileStatus::Modified) + ); + assert_eq!( + repo.status_for_file(tree, Path::new("projects/project1/b")), + Some(GitFileStatus::Added) + ); + }); + + std::fs::rename( + root_path.join("projects/project1"), + root_path.join("projects/project2"), + ) + .ok(); + tree.flush_fs_events(cx).await; + + cx.read(|cx| { + let tree = tree.read(cx); + let (work_dir, repo) = tree.repositories().next().unwrap(); + assert_eq!(work_dir.as_ref(), Path::new("projects/project2")); + assert_eq!( + repo.status_for_file(tree, Path::new("projects/project2/a")), + Some(GitFileStatus::Modified) + ); + assert_eq!( + repo.status_for_file(tree, Path::new("projects/project2/b")), + Some(GitFileStatus::Added) + ); + }); + } + + #[gpui::test] + async fn test_git_repository_for_path(cx: &mut TestAppContext) { + let root = temp_tree(json!({ + "c.txt": "", + "dir1": { + ".git": {}, + "deps": { + "dep1": { + ".git": {}, + "src": { + "a.txt": "" + } + } + }, + "src": { + "b.txt": "" + } + }, + })); + + let http_client = FakeHttpClient::with_404_response(); + let client = cx.read(|cx| Client::new(http_client, cx)); + let tree = Worktree::local( + client, + root.path(), + true, + Arc::new(RealFs), + Default::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + tree.flush_fs_events(cx).await; + + tree.read_with(cx, |tree, _cx| { + let tree = tree.as_local().unwrap(); + + assert!(tree.repository_for_path("c.txt".as_ref()).is_none()); + + let entry = tree.repository_for_path("dir1/src/b.txt".as_ref()).unwrap(); + assert_eq!( + entry + .work_directory(tree) + .map(|directory| directory.as_ref().to_owned()), + Some(Path::new("dir1").to_owned()) + ); + + let entry = tree + .repository_for_path("dir1/deps/dep1/src/a.txt".as_ref()) + .unwrap(); + assert_eq!( + entry + .work_directory(tree) + .map(|directory| directory.as_ref().to_owned()), + Some(Path::new("dir1/deps/dep1").to_owned()) + ); + + let entries = tree.files(false, 0); + + let paths_with_repos = tree + .entries_with_repositories(entries) + .map(|(entry, repo)| { + ( + entry.path.as_ref(), + repo.and_then(|repo| { + repo.work_directory(&tree) + .map(|work_directory| work_directory.0.to_path_buf()) + }), + ) + }) + .collect::>(); + + assert_eq!( + paths_with_repos, + &[ + (Path::new("c.txt"), None), + ( + Path::new("dir1/deps/dep1/src/a.txt"), + Some(Path::new("dir1/deps/dep1").into()) + ), + (Path::new("dir1/src/b.txt"), Some(Path::new("dir1").into())), + ] + ); + }); + + let repo_update_events = Arc::new(Mutex::new(vec![])); + tree.update(cx, |_, cx| { + let repo_update_events = repo_update_events.clone(); + cx.subscribe(&tree, move |_, _, event, _| { + if let Event::UpdatedGitRepositories(update) = event { + repo_update_events.lock().push(update.clone()); + } + }) + .detach(); + }); + + std::fs::write(root.path().join("dir1/.git/random_new_file"), "hello").unwrap(); + tree.flush_fs_events(cx).await; + + assert_eq!( + repo_update_events.lock()[0] + .keys() + .cloned() + .collect::>>(), + vec![Path::new("dir1").into()] + ); + + std::fs::remove_dir_all(root.path().join("dir1/.git")).unwrap(); + tree.flush_fs_events(cx).await; + + tree.read_with(cx, |tree, _cx| { + let tree = tree.as_local().unwrap(); + + assert!(tree + .repository_for_path("dir1/src/b.txt".as_ref()) + .is_none()); + }); + } + + #[gpui::test] + async fn test_git_status(cx: &mut TestAppContext) { + const IGNORE_RULE: &'static str = "**/target"; + + let root = temp_tree(json!({ + "project": { + "a.txt": "a", + "b.txt": "bb", + "c": { + "d": { + "e.txt": "eee" + } + }, + "f.txt": "ffff", + "target": { + "build_file": "???" + }, + ".gitignore": IGNORE_RULE + }, + + })); + + let http_client = FakeHttpClient::with_404_response(); + let client = cx.read(|cx| Client::new(http_client, cx)); + let tree = Worktree::local( + client, + root.path(), + true, + Arc::new(RealFs), + Default::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + + const A_TXT: &'static str = "a.txt"; + const B_TXT: &'static str = "b.txt"; + const E_TXT: &'static str = "c/d/e.txt"; + const F_TXT: &'static str = "f.txt"; + const DOTGITIGNORE: &'static str = ".gitignore"; + const BUILD_FILE: &'static str = "target/build_file"; + + let work_dir = root.path().join("project"); + let mut repo = git_init(work_dir.as_path()); + repo.add_ignore_rule(IGNORE_RULE).unwrap(); + git_add(Path::new(A_TXT), &repo); + git_add(Path::new(E_TXT), &repo); + git_add(Path::new(DOTGITIGNORE), &repo); + git_commit("Initial commit", &repo); + + std::fs::write(work_dir.join(A_TXT), "aa").unwrap(); + + tree.flush_fs_events(cx).await; + + // Check that the right git state is observed on startup + tree.read_with(cx, |tree, _cx| { + let snapshot = tree.snapshot(); + assert_eq!(snapshot.repository_entries.iter().count(), 1); + let (dir, repo) = snapshot.repository_entries.iter().next().unwrap(); + assert_eq!(dir.0.as_ref(), Path::new("project")); + + assert_eq!(repo.statuses.iter().count(), 3); + assert_eq!( + repo.statuses.get(&Path::new(A_TXT).into()), + Some(&GitFileStatus::Modified) + ); + assert_eq!( + repo.statuses.get(&Path::new(B_TXT).into()), + Some(&GitFileStatus::Added) + ); + assert_eq!( + repo.statuses.get(&Path::new(F_TXT).into()), + Some(&GitFileStatus::Added) + ); + }); + + git_add(Path::new(A_TXT), &repo); + git_add(Path::new(B_TXT), &repo); + git_commit("Committing modified and added", &repo); + tree.flush_fs_events(cx).await; + + // Check that repo only changes are tracked + tree.read_with(cx, |tree, _cx| { + let snapshot = tree.snapshot(); + let (_, repo) = snapshot.repository_entries.iter().next().unwrap(); + + assert_eq!(repo.statuses.iter().count(), 1); + assert_eq!( + repo.statuses.get(&Path::new(F_TXT).into()), + Some(&GitFileStatus::Added) + ); + }); + + git_reset(0, &repo); + git_remove_index(Path::new(B_TXT), &repo); + git_stash(&mut repo); + std::fs::write(work_dir.join(E_TXT), "eeee").unwrap(); + std::fs::write(work_dir.join(BUILD_FILE), "this should be ignored").unwrap(); + tree.flush_fs_events(cx).await; + + // Check that more complex repo changes are tracked + tree.read_with(cx, |tree, _cx| { + let snapshot = tree.snapshot(); + let (_, repo) = snapshot.repository_entries.iter().next().unwrap(); + + assert_eq!(repo.statuses.iter().count(), 3); + assert_eq!(repo.statuses.get(&Path::new(A_TXT).into()), None); + assert_eq!( + repo.statuses.get(&Path::new(B_TXT).into()), + Some(&GitFileStatus::Added) + ); + assert_eq!( + repo.statuses.get(&Path::new(E_TXT).into()), + Some(&GitFileStatus::Modified) + ); + assert_eq!( + repo.statuses.get(&Path::new(F_TXT).into()), + Some(&GitFileStatus::Added) + ); + }); + + std::fs::remove_file(work_dir.join(B_TXT)).unwrap(); + std::fs::remove_dir_all(work_dir.join("c")).unwrap(); + std::fs::write( + work_dir.join(DOTGITIGNORE), + [IGNORE_RULE, "f.txt"].join("\n"), + ) + .unwrap(); + + git_add(Path::new(DOTGITIGNORE), &repo); + git_commit("Committing modified git ignore", &repo); + + tree.flush_fs_events(cx).await; + + // Check that non-repo behavior is tracked + tree.read_with(cx, |tree, _cx| { + let snapshot = tree.snapshot(); + let (_, repo) = snapshot.repository_entries.iter().next().unwrap(); + + assert_eq!(repo.statuses.iter().count(), 0); + }); + + let mut renamed_dir_name = "first_directory/second_directory"; + const RENAMED_FILE: &'static str = "rf.txt"; + + std::fs::create_dir_all(work_dir.join(renamed_dir_name)).unwrap(); + std::fs::write( + work_dir.join(renamed_dir_name).join(RENAMED_FILE), + "new-contents", + ) + .unwrap(); + + tree.flush_fs_events(cx).await; + + tree.read_with(cx, |tree, _cx| { + let snapshot = tree.snapshot(); + let (_, repo) = snapshot.repository_entries.iter().next().unwrap(); + + assert_eq!(repo.statuses.iter().count(), 1); + assert_eq!( + repo.statuses + .get(&Path::new(renamed_dir_name).join(RENAMED_FILE).into()), + Some(&GitFileStatus::Added) + ); + }); + + renamed_dir_name = "new_first_directory/second_directory"; + + std::fs::rename( + work_dir.join("first_directory"), + work_dir.join("new_first_directory"), + ) + .unwrap(); + + tree.flush_fs_events(cx).await; + + tree.read_with(cx, |tree, _cx| { + let snapshot = tree.snapshot(); + let (_, repo) = snapshot.repository_entries.iter().next().unwrap(); + + assert_eq!(repo.statuses.iter().count(), 1); + assert_eq!( + repo.statuses + .get(&Path::new(renamed_dir_name).join(RENAMED_FILE).into()), + Some(&GitFileStatus::Added) + ); + }); + } + + #[track_caller] + fn git_init(path: &Path) -> git2::Repository { + git2::Repository::init(path).expect("Failed to initialize git repository") + } + + #[track_caller] + fn git_add>(path: P, repo: &git2::Repository) { + let path = path.as_ref(); + let mut index = repo.index().expect("Failed to get index"); + index.add_path(path).expect("Failed to add a.txt"); + index.write().expect("Failed to write index"); + } + + #[track_caller] + fn git_remove_index(path: &Path, repo: &git2::Repository) { + let mut index = repo.index().expect("Failed to get index"); + index.remove_path(path).expect("Failed to add a.txt"); + index.write().expect("Failed to write index"); + } + + #[track_caller] + fn git_commit(msg: &'static str, repo: &git2::Repository) { + use git2::Signature; + + let signature = Signature::now("test", "test@zed.dev").unwrap(); + let oid = repo.index().unwrap().write_tree().unwrap(); + let tree = repo.find_tree(oid).unwrap(); + if let Some(head) = repo.head().ok() { + let parent_obj = head.peel(git2::ObjectType::Commit).unwrap(); + + let parent_commit = parent_obj.as_commit().unwrap(); + + repo.commit( + Some("HEAD"), + &signature, + &signature, + msg, + &tree, + &[parent_commit], + ) + .expect("Failed to commit with parent"); + } else { + repo.commit(Some("HEAD"), &signature, &signature, msg, &tree, &[]) + .expect("Failed to commit"); + } + } + + #[track_caller] + fn git_stash(repo: &mut git2::Repository) { + use git2::Signature; + + let signature = Signature::now("test", "test@zed.dev").unwrap(); + repo.stash_save(&signature, "N/A", None) + .expect("Failed to stash"); + } + + #[track_caller] + fn git_reset(offset: usize, repo: &git2::Repository) { + let head = repo.head().expect("Couldn't get repo head"); + let object = head.peel(git2::ObjectType::Commit).unwrap(); + let commit = object.as_commit().unwrap(); + let new_head = commit + .parents() + .inspect(|parnet| { + parnet.message(); + }) + .skip(offset) + .next() + .expect("Not enough history"); + repo.reset(&new_head.as_object(), git2::ResetType::Soft, None) + .expect("Could not reset"); + } + + #[allow(dead_code)] + #[track_caller] + fn git_status(repo: &git2::Repository) -> HashMap { + repo.statuses(None) + .unwrap() + .iter() + .map(|status| (status.path().unwrap().to_string(), status.status())) + .collect() + } + } } diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index d3b2876675..8cd5a06675 100644 --- a/crates/project_panel/Cargo.toml +++ b/crates/project_panel/Cargo.toml @@ -21,9 +21,13 @@ util = { path = "../util" } workspace = { path = "../workspace" } postage.workspace = true futures.workspace = true +schemars.workspace = true +serde.workspace = true unicase = "2.6" [dev-dependencies] +client = { path = "../client", features = ["test-support"] } +language = { path = "../language", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 3476a6a155..761d803084 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -6,7 +6,7 @@ use gpui::{ actions, anyhow::{anyhow, Result}, elements::{ - AnchorCorner, ChildView, ContainerStyle, Empty, Flex, Label, MouseEventHandler, + AnchorCorner, ChildView, ComponentHost, ContainerStyle, Empty, Flex, MouseEventHandler, ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState, }, geometry::vector::Vector2F, @@ -16,8 +16,13 @@ use gpui::{ ViewHandle, WeakViewHandle, WindowContext, }; use menu::{Confirm, SelectNext, SelectPrev}; -use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; -use settings::{settings_file::SettingsFile, Settings}; +use project::{ + repository::GitFileStatus, Entry, EntryKind, Fs, Project, ProjectEntryId, ProjectPath, + Worktree, WorktreeId, +}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::SettingsStore; use std::{ cmp::Ordering, collections::{hash_map, HashMap}, @@ -26,7 +31,7 @@ use std::{ path::Path, sync::Arc, }; -use theme::ProjectPanelEntry; +use theme::{ui::FileName, ProjectPanelEntry}; use unicase::UniCase; use workspace::{ dock::{DockPosition, Panel}, @@ -35,8 +40,41 @@ use workspace::{ const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX; +#[derive(Deserialize)] +pub struct ProjectPanelSettings { + dock: ProjectPanelDockPosition, + default_width: f32, +} + +impl settings::Setting for ProjectPanelSettings { + const KEY: Option<&'static str> = Some("project_panel"); + + type FileContent = ProjectPanelSettingsContent; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &AppContext, + ) -> Result { + Self::load_via_json_merge(default_value, user_values) + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +pub struct ProjectPanelSettingsContent { + dock: Option, + default_width: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub enum ProjectPanelDockPosition { + Left, + Right, +} + pub struct ProjectPanel { project: ModelHandle, + fs: Arc, list: UniformListState, visible_entries: Vec<(WorktreeId, Vec)>, last_worktree_root_id: Option, @@ -90,6 +128,7 @@ pub struct EntryDetails { is_editing: bool, is_processing: bool, is_cut: bool, + git_status: Option, } actions!( @@ -112,6 +151,7 @@ actions!( ); pub fn init(cx: &mut AppContext) { + settings::register::(cx); cx.add_action(ProjectPanel::expand_selected_entry); cx.add_action(ProjectPanel::collapse_selected_entry); cx.add_action(ProjectPanel::select_prev); @@ -205,6 +245,7 @@ impl ProjectPanel { let view_id = cx.view_id(); let mut this = Self { project: project.clone(), + fs: workspace.app_state().fs.clone(), list: Default::default(), visible_entries: Default::default(), last_worktree_root_id: Default::default(), @@ -222,7 +263,7 @@ impl ProjectPanel { // Update the dock position when the setting changes. let mut old_dock_position = this.position(cx); - cx.observe_global::(move |this, cx| { + cx.observe_global::(move |this, cx| { let new_dock_position = this.position(cx); if new_dock_position != old_dock_position { old_dock_position = new_dock_position; @@ -1027,7 +1068,13 @@ impl ProjectPanel { .unwrap_or(&[]); let entry_range = range.start.saturating_sub(ix)..end_ix - ix; - for entry in &visible_worktree_entries[entry_range] { + for (entry, repo) in + snapshot.entries_with_repositories(visible_worktree_entries[entry_range].iter()) + { + let status = (entry.path.parent().is_some() && !entry.is_ignored) + .then(|| repo.and_then(|repo| repo.status_for_path(&snapshot, &entry.path))) + .flatten(); + let mut details = EntryDetails { filename: entry .path @@ -1048,6 +1095,7 @@ impl ProjectPanel { is_cut: self .clipboard_entry .map_or(false, |e| e.is_cut() && e.entry_id() == entry.id), + git_status: status, }; if let Some(edit_state) = &self.edit_state { @@ -1116,12 +1164,16 @@ impl ProjectPanel { .flex(1.0, true) .into_any() } else { - Label::new(details.filename.clone(), style.text.clone()) - .contained() - .with_margin_left(style.icon_spacing) - .aligned() - .left() - .into_any() + ComponentHost::new(FileName::new( + details.filename.clone(), + details.git_status, + FileName::style(style.text.clone(), &theme::current(cx)), + )) + .contained() + .with_margin_left(style.icon_spacing) + .aligned() + .left() + .into_any() }) .constrained() .with_height(style.height) @@ -1225,7 +1277,7 @@ impl ProjectPanel { let row_container_style = theme.dragged_entry.container; move |_, cx: &mut ViewContext| { - let theme = cx.global::().theme.clone(); + let theme = theme::current(cx).clone(); Self::render_entry_visual_element( &details, None, @@ -1248,7 +1300,7 @@ impl View for ProjectPanel { fn render(&mut self, cx: &mut gpui::ViewContext) -> gpui::AnyElement { enum ProjectPanel {} - let theme = &cx.global::().theme.project_panel; + let theme = &theme::current(cx).project_panel; let mut container_style = theme.container; let padding = std::mem::take(&mut container_style.padding); let last_worktree_root_id = self.last_worktree_root_id; @@ -1267,7 +1319,7 @@ impl View for ProjectPanel { .sum(), cx, move |this, range, items, cx| { - let theme = cx.global::().theme.clone(); + let theme = theme::current(cx).clone(); let mut dragged_entry_destination = this.dragged_entry_destination.clone(); this.for_each_visible_entry(range, cx, |id, details, cx| { @@ -1304,8 +1356,7 @@ impl View for ProjectPanel { .with_child( MouseEventHandler::::new(2, cx, { let button_style = theme.open_project_button.clone(); - let context_menu_item_style = - cx.global::().theme.context_menu.item.clone(); + let context_menu_item_style = theme::current(cx).context_menu.item.clone(); move |state, cx| { let button_style = button_style.style_for(state, false).clone(); let context_menu_item = @@ -1360,10 +1411,9 @@ impl Entity for ProjectPanel { impl workspace::dock::Panel for ProjectPanel { fn position(&self, cx: &WindowContext) -> DockPosition { - let settings = cx.global::(); - match settings.project_panel.dock { - settings::ProjectPanelDockPosition::Left => DockPosition::Left, - settings::ProjectPanelDockPosition::Right => DockPosition::Right, + match settings::get::(cx).dock { + ProjectPanelDockPosition::Left => DockPosition::Left, + ProjectPanelDockPosition::Right => DockPosition::Right, } } @@ -1372,19 +1422,21 @@ impl workspace::dock::Panel for ProjectPanel { } fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { - SettingsFile::update(cx, move |settings| { - let dock = match position { - DockPosition::Left | DockPosition::Bottom => { - settings::ProjectPanelDockPosition::Left - } - DockPosition::Right => settings::ProjectPanelDockPosition::Right, - }; - settings.project_panel.dock = Some(dock); - }) + settings::update_settings_file::( + self.fs.clone(), + cx, + move |settings| { + let dock = match position { + DockPosition::Left | DockPosition::Bottom => ProjectPanelDockPosition::Left, + DockPosition::Right => ProjectPanelDockPosition::Right, + }; + settings.dock = Some(dock); + }, + ); } fn default_size(&self, cx: &WindowContext) -> f32 { - cx.global::().project_panel.default_width + settings::get::(cx).default_width } fn should_zoom_in_on_event(_: &Self::Event) -> bool { @@ -1459,15 +1511,13 @@ mod tests { use gpui::{TestAppContext, ViewHandle}; use project::FakeFs; use serde_json::json; + use settings::SettingsStore; use std::{collections::HashSet, path::Path}; + use workspace::{pane, AppState}; #[gpui::test] async fn test_visible_list(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); - cx.update(|cx| { - let settings = Settings::test(cx); - cx.set_global(settings); - }); + init_test(cx); let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -1555,11 +1605,7 @@ mod tests { #[gpui::test(iterations = 30)] async fn test_editing_files(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); - cx.update(|cx| { - let settings = Settings::test(cx); - cx.set_global(settings); - }); + init_test(cx); let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -1875,11 +1921,7 @@ mod tests { #[gpui::test] async fn test_copy_paste(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); - cx.update(|cx| { - let settings = Settings::test(cx); - cx.set_global(settings); - }); + init_test(cx); let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -1947,6 +1989,95 @@ mod tests { ); } + #[gpui::test] + async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) { + init_test_with_editor(cx); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/src", + json!({ + "test": { + "first.rs": "// First Rust file", + "second.rs": "// Second Rust file", + "third.rs": "// Third Rust file", + } + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; + let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + + toggle_expand_dir(&panel, "src/test", cx); + select_path(&panel, "src/test/first.rs", cx); + panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); + cx.foreground().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v src", + " v test", + " first.rs <== selected", + " second.rs", + " third.rs" + ] + ); + ensure_single_file_is_opened(window_id, &workspace, "test/first.rs", cx); + + submit_deletion(window_id, &panel, cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v src", + " v test", + " second.rs", + " third.rs" + ], + "Project panel should have no deleted file, no other file is selected in it" + ); + ensure_no_open_items_and_panes(window_id, &workspace, cx); + + select_path(&panel, "src/test/second.rs", cx); + panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); + cx.foreground().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v src", + " v test", + " second.rs <== selected", + " third.rs" + ] + ); + ensure_single_file_is_opened(window_id, &workspace, "test/second.rs", cx); + + cx.update_window(window_id, |cx| { + let active_items = workspace + .read(cx) + .panes() + .iter() + .filter_map(|pane| pane.read(cx).active_item()) + .collect::>(); + assert_eq!(active_items.len(), 1); + let open_editor = active_items + .into_iter() + .next() + .unwrap() + .downcast::() + .expect("Open item should be an editor"); + open_editor.update(cx, |editor, cx| editor.set_text("Another text!", cx)); + }); + submit_deletion(window_id, &panel, cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &["v src", " v test", " third.rs"], + "Project panel should have no deleted file, with one last file remaining" + ); + ensure_no_open_items_and_panes(window_id, &workspace, cx); + } + fn toggle_expand_dir( panel: &ViewHandle, path: impl AsRef, @@ -2039,4 +2170,105 @@ mod tests { result } + + fn init_test(cx: &mut TestAppContext) { + cx.foreground().forbid_parking(); + cx.update(|cx| { + cx.set_global(SettingsStore::test(cx)); + theme::init((), cx); + language::init(cx); + editor::init_settings(cx); + workspace::init_settings(cx); + }); + } + + fn init_test_with_editor(cx: &mut TestAppContext) { + cx.foreground().forbid_parking(); + cx.update(|cx| { + let app_state = AppState::test(cx); + theme::init((), cx); + language::init(cx); + editor::init(cx); + pane::init(cx); + workspace::init(app_state.clone(), cx); + }); + } + + fn ensure_single_file_is_opened( + window_id: usize, + workspace: &ViewHandle, + expected_path: &str, + cx: &mut TestAppContext, + ) { + cx.read_window(window_id, |cx| { + let workspace = workspace.read(cx); + let worktrees = workspace.worktrees(cx).collect::>(); + assert_eq!(worktrees.len(), 1); + let worktree_id = WorktreeId::from_usize(worktrees[0].id()); + + let open_project_paths = workspace + .panes() + .iter() + .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx)) + .collect::>(); + assert_eq!( + open_project_paths, + vec![ProjectPath { + worktree_id, + path: Arc::from(Path::new(expected_path)) + }], + "Should have opened file, selected in project panel" + ); + }); + } + + fn submit_deletion( + window_id: usize, + panel: &ViewHandle, + cx: &mut TestAppContext, + ) { + assert!( + !cx.has_pending_prompt(window_id), + "Should have no prompts before the deletion" + ); + panel.update(cx, |panel, cx| { + panel + .delete(&Delete, cx) + .expect("Deletion start") + .detach_and_log_err(cx); + }); + assert!( + cx.has_pending_prompt(window_id), + "Should have a prompt after the deletion" + ); + cx.simulate_prompt_answer(window_id, 0); + assert!( + !cx.has_pending_prompt(window_id), + "Should have no prompts after prompt was replied to" + ); + cx.foreground().run_until_parked(); + } + + fn ensure_no_open_items_and_panes( + window_id: usize, + workspace: &ViewHandle, + cx: &mut TestAppContext, + ) { + assert!( + !cx.has_pending_prompt(window_id), + "Should have no prompts after deletion operation closes the file" + ); + cx.read_window(window_id, |cx| { + let open_project_paths = workspace + .read(cx) + .panes() + .iter() + .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx)) + .collect::>(); + assert!( + open_project_paths.is_empty(), + "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}" + ); + }); + } } diff --git a/crates/project_symbols/Cargo.toml b/crates/project_symbols/Cargo.toml index 1c0d3a6165..85939634ad 100644 --- a/crates/project_symbols/Cargo.toml +++ b/crates/project_symbols/Cargo.toml @@ -17,7 +17,9 @@ project = { path = "../project" } text = { path = "../text" } settings = { path = "../settings" } workspace = { path = "../workspace" } +theme = { path = "../theme" } util = { path = "../util" } + anyhow.workspace = true ordered-float.workspace = true postage.workspace = true @@ -25,8 +27,11 @@ smol.workspace = true [dev-dependencies] futures.workspace = true +editor = { path = "../editor", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } language = { path = "../language", features = ["test-support"] } lsp = { path = "../lsp", features = ["test-support"] } project = { path = "../project", features = ["test-support"] } +theme = { path = "../theme", features = ["test-support"] } +workspace = { path = "../workspace", features = ["test-support"] } diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 25828f17ca..992283df01 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -9,7 +9,6 @@ use gpui::{ use ordered_float::OrderedFloat; use picker::{Picker, PickerDelegate, PickerEvent}; use project::{Project, Symbol}; -use settings::Settings; use std::{borrow::Cow, cmp::Reverse, sync::Arc}; use util::ResultExt; use workspace::Workspace; @@ -195,12 +194,13 @@ impl PickerDelegate for ProjectSymbolsDelegate { selected: bool, cx: &AppContext, ) -> AnyElement> { - let string_match = &self.matches[ix]; - let settings = cx.global::(); - let style = &settings.theme.picker.item; + let theme = theme::current(cx); + let style = &theme.picker.item; let current_style = style.style_for(mouse_state, selected); + + let string_match = &self.matches[ix]; let symbol = &self.symbols[string_match.candidate_id]; - let syntax_runs = styled_runs_for_code_label(&symbol.label, &settings.theme.editor.syntax); + let syntax_runs = styled_runs_for_code_label(&symbol.label, &theme.editor.syntax); let mut path = symbol.path.path.to_string_lossy(); if self.show_worktree_root_name { @@ -244,12 +244,12 @@ mod tests { use gpui::{serde_json::json, TestAppContext}; use language::{FakeLspAdapter, Language, LanguageConfig}; use project::FakeFs; + use settings::SettingsStore; use std::{path::Path, sync::Arc}; #[gpui::test] async fn test_project_symbols(cx: &mut TestAppContext) { - cx.foreground().forbid_parking(); - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx); let mut language = Language::new( LanguageConfig { @@ -368,6 +368,17 @@ mod tests { }); } + fn init_test(cx: &mut TestAppContext) { + cx.foreground().forbid_parking(); + cx.update(|cx| { + cx.set_global(SettingsStore::test(cx)); + theme::init((), cx); + language::init(cx); + Project::init_settings(cx); + workspace::init_settings(cx); + }); + } + fn symbol(name: &str, path: impl AsRef) -> lsp::SymbolInformation { #[allow(deprecated)] lsp::SymbolInformation { diff --git a/crates/recent_projects/Cargo.toml b/crates/recent_projects/Cargo.toml index 968ae8e9a4..14f8853c9c 100644 --- a/crates/recent_projects/Cargo.toml +++ b/crates/recent_projects/Cargo.toml @@ -18,8 +18,12 @@ picker = { path = "../picker" } settings = { path = "../settings" } text = { path = "../text" } util = { path = "../util"} +theme = { path = "../theme" } workspace = { path = "../workspace" } ordered-float.workspace = true postage.workspace = true smol.workspace = true + +[dev-dependencies] +editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index 644e74d878..a1dc8982c7 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -10,7 +10,6 @@ use gpui::{ use highlighted_workspace_location::HighlightedWorkspaceLocation; use ordered_float::OrderedFloat; use picker::{Picker, PickerDelegate, PickerEvent}; -use settings::Settings; use std::sync::Arc; use workspace::{ notifications::simple_message_notification::MessageNotification, Workspace, WorkspaceLocation, @@ -173,9 +172,10 @@ impl PickerDelegate for RecentProjectsDelegate { selected: bool, cx: &gpui::AppContext, ) -> AnyElement> { - let settings = cx.global::(); + let theme = theme::current(cx); + let style = theme.picker.item.style_for(mouse_state, selected); + let string_match = &self.matches[ix]; - let style = settings.theme.picker.item.style_for(mouse_state, selected); let highlighted_location = HighlightedWorkspaceLocation::new( &string_match, diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 220ef22fb7..eca5fda306 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -986,8 +986,22 @@ message Entry { message RepositoryEntry { uint64 work_directory_id = 1; optional string branch = 2; + repeated string removed_repo_paths = 3; + repeated StatusEntry updated_statuses = 4; } +message StatusEntry { + string repo_path = 1; + GitStatus status = 2; +} + +enum GitStatus { + Added = 0; + Modified = 1; + Conflict = 2; +} + + message BufferState { uint64 id = 1; optional File file = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 20a457cc4b..cef4e6867c 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -1,6 +1,7 @@ use super::{entity_messages, messages, request_messages, ConnectionId, TypedEnvelope}; use anyhow::{anyhow, Result}; use async_tungstenite::tungstenite::Message as WebSocketMessage; +use collections::HashMap; use futures::{SinkExt as _, StreamExt as _}; use prost::Message as _; use serde::Serialize; @@ -484,14 +485,21 @@ pub fn split_worktree_update( mut message: UpdateWorktree, max_chunk_size: usize, ) -> impl Iterator { - let mut done = false; + let mut done_files = false; + + let mut repository_map = message + .updated_repositories + .into_iter() + .map(|repo| (repo.work_directory_id, repo)) + .collect::>(); + iter::from_fn(move || { - if done { + if done_files { return None; } let updated_entries_chunk_size = cmp::min(message.updated_entries.len(), max_chunk_size); - let updated_entries = message + let updated_entries: Vec<_> = message .updated_entries .drain(..updated_entries_chunk_size) .collect(); @@ -502,22 +510,28 @@ pub fn split_worktree_update( .drain(..removed_entries_chunk_size) .collect(); - done = message.updated_entries.is_empty() && message.removed_entries.is_empty(); + done_files = message.updated_entries.is_empty() && message.removed_entries.is_empty(); - // Wait to send repositories until after we've guaranteed that their associated entries - // will be read - let updated_repositories = if done { - mem::take(&mut message.updated_repositories) - } else { - Default::default() - }; + let mut updated_repositories = Vec::new(); - let removed_repositories = if done { + if !repository_map.is_empty() { + for entry in &updated_entries { + if let Some(repo) = repository_map.remove(&entry.id) { + updated_repositories.push(repo) + } + } + } + + let removed_repositories = if done_files { mem::take(&mut message.removed_repositories) } else { Default::default() }; + if done_files { + updated_repositories.extend(mem::take(&mut repository_map).into_values()); + } + Some(UpdateWorktree { project_id: message.project_id, worktree_id: message.worktree_id, @@ -526,7 +540,7 @@ pub fn split_worktree_update( updated_entries, removed_entries, scan_id: message.scan_id, - is_last_update: done && message.is_last_update, + is_last_update: done_files && message.is_last_update, updated_repositories, removed_repositories, }) diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index e51ded5969..64fbf19462 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 = 54; +pub const PROTOCOL_VERSION: u32 = 55; diff --git a/crates/search/Cargo.toml b/crates/search/Cargo.toml index ab3c35c1fe..7ef388f7c0 100644 --- a/crates/search/Cargo.toml +++ b/crates/search/Cargo.toml @@ -27,9 +27,10 @@ serde.workspace = true serde_derive.workspace = true smallvec.workspace = true smol.workspace = true -glob.workspace = true +globset.workspace = true [dev-dependencies] +client = { path = "../client", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } serde_json.workspace = true diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index b0af51379d..87a8b265fb 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -13,7 +13,6 @@ use gpui::{ }; use project::search::SearchQuery; use serde::Deserialize; -use settings::Settings; use std::{any::Any, sync::Arc}; use util::ResultExt; use workspace::{ @@ -93,7 +92,7 @@ impl View for BufferSearchBar { } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let theme = cx.global::().theme.clone(); + let theme = theme::current(cx).clone(); let editor_container = if self.query_contains_error { theme.search.invalid_editor } else { @@ -324,16 +323,12 @@ impl BufferSearchBar { return None; } - let tooltip_style = cx.global::().theme.tooltip.clone(); + let tooltip_style = theme::current(cx).tooltip.clone(); let is_active = self.is_search_option_enabled(option); Some( MouseEventHandler::::new(option as usize, cx, |state, cx| { - let style = cx - .global::() - .theme - .search - .option_button - .style_for(state, is_active); + let theme = theme::current(cx); + let style = theme.search.option_button.style_for(state, is_active); Label::new(icon, style.text.clone()) .contained() .with_style(style.container) @@ -371,16 +366,12 @@ impl BufferSearchBar { tooltip = "Select Next Match"; } }; - let tooltip_style = cx.global::().theme.tooltip.clone(); + let tooltip_style = theme::current(cx).tooltip.clone(); enum NavButton {} MouseEventHandler::::new(direction as usize, cx, |state, cx| { - let style = cx - .global::() - .theme - .search - .option_button - .style_for(state, false); + let theme = theme::current(cx); + let style = theme.search.option_button.style_for(state, false); Label::new(icon, style.text.clone()) .contained() .with_style(style.container) @@ -408,7 +399,7 @@ impl BufferSearchBar { cx: &mut ViewContext, ) -> AnyElement { let tooltip = "Dismiss Buffer Search"; - let tooltip_style = cx.global::().theme.tooltip.clone(); + let tooltip_style = theme::current(cx).tooltip.clone(); enum CloseButton {} MouseEventHandler::::new(0, cx, |state, _| { @@ -655,19 +646,11 @@ mod tests { use editor::{DisplayPoint, Editor}; use gpui::{color::Color, test::EmptyView, TestAppContext}; use language::Buffer; - use std::sync::Arc; use unindent::Unindent as _; #[gpui::test] async fn test_search_simple(cx: &mut TestAppContext) { - let fonts = cx.font_cache(); - let mut theme = gpui::fonts::with_font_cache(fonts.clone(), theme::Theme::default); - theme.search.match_background = Color::red(); - cx.update(|cx| { - let mut settings = Settings::test(cx); - settings.theme = Arc::new(theme); - cx.set_global(settings) - }); + crate::project_search::tests::init_test(cx); let buffer = cx.add_model(|cx| { Buffer::new( diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 05d27b824c..d96d77eb00 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -2,12 +2,14 @@ use crate::{ SearchOption, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex, ToggleWholeWord, }; +use anyhow::Result; use collections::HashMap; use editor::{ items::active_match_index, scroll::autoscroll::Autoscroll, Anchor, Editor, MultiBuffer, SelectAll, MAX_TAB_TITLE_LEN, }; use futures::StreamExt; +use globset::{Glob, GlobMatcher}; use gpui::{ actions, elements::*, @@ -17,7 +19,6 @@ use gpui::{ }; use menu::Confirm; use project::{search::SearchQuery, Project}; -use settings::Settings; use smallvec::SmallVec; use std::{ any::{Any, TypeId}, @@ -195,7 +196,7 @@ impl View for ProjectSearchView { if model.match_ranges.is_empty() { enum Status {} - let theme = cx.global::().theme.clone(); + let theme = theme::current(cx).clone(); let text = if self.query_editor.read(cx).text(cx).is_empty() { "" } else if model.pending_search.is_some() { @@ -572,46 +573,30 @@ impl ProjectSearchView { fn build_search_query(&mut self, cx: &mut ViewContext) -> Option { let text = self.query_editor.read(cx).text(cx); - let included_files = match self - .included_files_editor - .read(cx) - .text(cx) - .split(',') - .map(str::trim) - .filter(|glob_str| !glob_str.is_empty()) - .map(|glob_str| glob::Pattern::new(glob_str)) - .collect::>() - { - Ok(included_files) => { - self.panels_with_errors.remove(&InputPanel::Include); - included_files - } - Err(_e) => { - self.panels_with_errors.insert(InputPanel::Include); - cx.notify(); - return None; - } - }; - let excluded_files = match self - .excluded_files_editor - .read(cx) - .text(cx) - .split(',') - .map(str::trim) - .filter(|glob_str| !glob_str.is_empty()) - .map(|glob_str| glob::Pattern::new(glob_str)) - .collect::>() - { - Ok(excluded_files) => { - self.panels_with_errors.remove(&InputPanel::Exclude); - excluded_files - } - Err(_e) => { - self.panels_with_errors.insert(InputPanel::Exclude); - cx.notify(); - return None; - } - }; + let included_files = + match Self::load_glob_set(&self.included_files_editor.read(cx).text(cx)) { + Ok(included_files) => { + self.panels_with_errors.remove(&InputPanel::Include); + included_files + } + Err(_e) => { + self.panels_with_errors.insert(InputPanel::Include); + cx.notify(); + return None; + } + }; + let excluded_files = + match Self::load_glob_set(&self.excluded_files_editor.read(cx).text(cx)) { + Ok(excluded_files) => { + self.panels_with_errors.remove(&InputPanel::Exclude); + excluded_files + } + Err(_e) => { + self.panels_with_errors.insert(InputPanel::Exclude); + cx.notify(); + return None; + } + }; if self.regex { match SearchQuery::regex( text, @@ -641,6 +626,14 @@ impl ProjectSearchView { } } + fn load_glob_set(text: &str) -> Result> { + text.split(',') + .map(str::trim) + .filter(|glob_str| !glob_str.is_empty()) + .map(|glob_str| anyhow::Ok(Glob::new(glob_str)?.compile_matcher())) + .collect() + } + fn select_match(&mut self, direction: Direction, cx: &mut ViewContext) { if let Some(index) = self.active_match_index { let match_ranges = self.model.read(cx).match_ranges.clone(); @@ -903,16 +896,12 @@ impl ProjectSearchBar { tooltip = "Select Next Match"; } }; - let tooltip_style = cx.global::().theme.tooltip.clone(); + let tooltip_style = theme::current(cx).tooltip.clone(); enum NavButton {} MouseEventHandler::::new(direction as usize, cx, |state, cx| { - let style = &cx - .global::() - .theme - .search - .option_button - .style_for(state, false); + let theme = theme::current(cx); + let style = theme.search.option_button.style_for(state, false); Label::new(icon, style.text.clone()) .contained() .with_style(style.container) @@ -939,15 +928,11 @@ impl ProjectSearchBar { option: SearchOption, cx: &mut ViewContext, ) -> AnyElement { - let tooltip_style = cx.global::().theme.tooltip.clone(); + let tooltip_style = theme::current(cx).tooltip.clone(); let is_active = self.is_option_enabled(option, cx); MouseEventHandler::::new(option as usize, cx, |state, cx| { - let style = &cx - .global::() - .theme - .search - .option_button - .style_for(state, is_active); + let theme = theme::current(cx); + let style = theme.search.option_button.style_for(state, is_active); Label::new(icon, style.text.clone()) .contained() .with_style(style.container) @@ -992,7 +977,7 @@ impl View for ProjectSearchBar { fn render(&mut self, cx: &mut ViewContext) -> AnyElement { if let Some(search) = self.active_project_search.as_ref() { let search = search.read(cx); - let theme = cx.global::().theme.clone(); + let theme = theme::current(cx).clone(); let query_container_style = if search.panels_with_errors.contains(&InputPanel::Query) { theme.search.invalid_editor } else { @@ -1146,25 +1131,19 @@ impl ToolbarItemView for ProjectSearchBar { } #[cfg(test)] -mod tests { +pub mod tests { use super::*; use editor::DisplayPoint; use gpui::{color::Color, executor::Deterministic, TestAppContext}; use project::FakeFs; use serde_json::json; + use settings::SettingsStore; use std::sync::Arc; + use theme::ThemeSettings; #[gpui::test] async fn test_project_search(deterministic: Arc, cx: &mut TestAppContext) { - let fonts = cx.font_cache(); - let mut theme = gpui::fonts::with_font_cache(fonts.clone(), theme::Theme::default); - theme.search.match_background = Color::red(); - cx.update(|cx| { - let mut settings = Settings::test(cx); - settings.theme = Arc::new(theme); - cx.set_global(settings); - cx.set_global(ActiveSearches::default()); - }); + init_test(cx); let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -1279,4 +1258,27 @@ mod tests { ); }); } + + pub fn init_test(cx: &mut TestAppContext) { + let fonts = cx.font_cache(); + let mut theme = gpui::fonts::with_font_cache(fonts.clone(), theme::Theme::default); + theme.search.match_background = Color::red(); + + cx.update(|cx| { + cx.set_global(SettingsStore::test(cx)); + cx.set_global(ActiveSearches::default()); + + theme::init((), cx); + cx.update_global::(|store, _| { + let mut settings = store.get::(None).clone(); + settings.theme = Arc::new(theme); + store.override_global(settings) + }); + + language::init(cx); + client::init_settings(cx); + editor::init_settings(cx); + workspace::init_settings(cx); + }); + } } diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index 9566103960..1ec0ff4a63 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -9,7 +9,7 @@ path = "src/settings.rs" doctest = false [features] -test-support = [] +test-support = ["gpui/test-support", "fs/test-support"] [dependencies] assets = { path = "../assets" } @@ -17,21 +17,20 @@ collections = { path = "../collections" } gpui = { path = "../gpui" } sqlez = { path = "../sqlez" } fs = { path = "../fs" } -anyhow.workspace = true -futures.workspace = true -theme = { path = "../theme" } staff_mode = { path = "../staff_mode" } util = { path = "../util" } -glob.workspace = true +anyhow.workspace = true +futures.workspace = true json_comments = "0.2" lazy_static.workspace = true postage.workspace = true -schemars = "0.8" +schemars.workspace = true serde.workspace = true serde_derive.workspace = true serde_json.workspace = true -toml = "0.5" +smallvec.workspace = true +toml.workspace = true tree-sitter = "*" tree-sitter-json = "*" diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index a45a53145e..0b638da924 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -1,4 +1,4 @@ -use crate::{parse_json_with_comments, Settings}; +use crate::settings_store::parse_json_with_comments; use anyhow::{Context, Result}; use assets::Assets; use collections::BTreeMap; @@ -41,20 +41,14 @@ impl JsonSchema for KeymapAction { struct ActionWithData(Box, Box); impl KeymapFileContent { - pub fn load_defaults(cx: &mut AppContext) { - for path in ["keymaps/default.json", "keymaps/vim.json"] { - Self::load(path, cx).unwrap(); - } - - if let Some(asset_path) = cx.global::().base_keymap.asset_path() { - Self::load(asset_path, cx).log_err(); - } - } - - pub fn load(asset_path: &str, cx: &mut AppContext) -> Result<()> { + pub fn load_asset(asset_path: &str, cx: &mut AppContext) -> Result<()> { let content = Assets::get(asset_path).unwrap().data; let content_str = std::str::from_utf8(content.as_ref()).unwrap(); - parse_json_with_comments::(content_str)?.add_to_cx(cx) + Self::parse(content_str)?.add_to_cx(cx) + } + + pub fn parse(content: &str) -> Result { + parse_json_with_comments::(content) } pub fn add_to_cx(self, cx: &mut AppContext) -> Result<()> { diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 618ccb89ac..840797c6ad 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -1,1595 +1,19 @@ mod keymap_file; -pub mod settings_file; -pub mod watched_json; - -use anyhow::Result; -use gpui::{ - font_cache::{FamilyId, FontCache}, - fonts, AssetSource, -}; -use lazy_static::lazy_static; -use schemars::{ - gen::{SchemaGenerator, SchemaSettings}, - schema::{InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec}, - JsonSchema, -}; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use serde_json::Value; -use std::{ - borrow::Cow, collections::HashMap, num::NonZeroU32, ops::Range, path::Path, str, sync::Arc, -}; -use theme::{Theme, ThemeRegistry}; -use tree_sitter::{Query, Tree}; -use util::{RangeExt, ResultExt as _}; +mod settings_file; +mod settings_store; +use gpui::AssetSource; pub use keymap_file::{keymap_file_json_schema, KeymapFileContent}; -pub use watched_json::watch_files; +pub use settings_file::*; +pub use settings_store::{Setting, SettingsJsonSchemaParams, SettingsStore}; +use std::{borrow::Cow, str}; pub const DEFAULT_SETTINGS_ASSET_PATH: &str = "settings/default.json"; pub const INITIAL_USER_SETTINGS_ASSET_PATH: &str = "settings/initial_user_settings.json"; -#[derive(Clone)] -pub struct Settings { - pub features: Features, - pub buffer_font_family_name: String, - pub buffer_font_features: fonts::Features, - pub buffer_font_family: FamilyId, - pub default_buffer_font_size: f32, - pub buffer_font_size: f32, - pub active_pane_magnification: f32, - pub cursor_blink: bool, - pub confirm_quit: bool, - pub hover_popover_enabled: bool, - pub show_completions_on_input: bool, - pub show_call_status_icon: bool, - pub vim_mode: bool, - pub autosave: Autosave, - pub project_panel: ProjectPanelSettings, - pub editor_defaults: EditorSettings, - pub editor_overrides: EditorSettings, - pub git: GitSettings, - pub git_overrides: GitSettings, - pub copilot: CopilotSettings, - pub journal_defaults: JournalSettings, - pub journal_overrides: JournalSettings, - pub terminal_defaults: TerminalSettings, - pub terminal_overrides: TerminalSettings, - pub language_defaults: HashMap, EditorSettings>, - pub language_overrides: HashMap, EditorSettings>, - pub lsp: HashMap, LspSettings>, - pub theme: Arc, - pub telemetry_defaults: TelemetrySettings, - pub telemetry_overrides: TelemetrySettings, - pub auto_update: bool, - pub base_keymap: BaseKeymap, -} - -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)] -pub enum BaseKeymap { - #[default] - VSCode, - JetBrains, - SublimeText, - Atom, - TextMate, -} - -impl BaseKeymap { - pub const OPTIONS: [(&'static str, Self); 5] = [ - ("VSCode (Default)", Self::VSCode), - ("Atom", Self::Atom), - ("JetBrains", Self::JetBrains), - ("Sublime Text", Self::SublimeText), - ("TextMate", Self::TextMate), - ]; - - pub fn asset_path(&self) -> Option<&'static str> { - match self { - BaseKeymap::JetBrains => Some("keymaps/jetbrains.json"), - BaseKeymap::SublimeText => Some("keymaps/sublime_text.json"), - BaseKeymap::Atom => Some("keymaps/atom.json"), - BaseKeymap::TextMate => Some("keymaps/textmate.json"), - BaseKeymap::VSCode => None, - } - } - - pub fn names() -> impl Iterator { - Self::OPTIONS.iter().map(|(name, _)| *name) - } - - pub fn from_names(option: &str) -> BaseKeymap { - Self::OPTIONS - .iter() - .copied() - .find_map(|(name, value)| (name == option).then(|| value)) - .unwrap_or_default() - } -} - -#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] -pub struct TelemetrySettings { - diagnostics: Option, - metrics: Option, -} - -impl TelemetrySettings { - pub fn metrics(&self) -> bool { - self.metrics.unwrap() - } - - pub fn diagnostics(&self) -> bool { - self.diagnostics.unwrap() - } - - pub fn set_metrics(&mut self, value: bool) { - self.metrics = Some(value); - } - - pub fn set_diagnostics(&mut self, value: bool) { - self.diagnostics = Some(value); - } -} - -#[derive(Clone, Debug, Default)] -pub struct CopilotSettings { - pub disabled_globs: Vec, -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] -pub struct CopilotSettingsContent { - #[serde(default)] - pub disabled_globs: Option>, -} - -#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] -pub struct GitSettings { - pub git_gutter: Option, - pub gutter_debounce: Option, -} - -#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum GitGutter { - #[default] - TrackedFiles, - Hide, -} - -pub struct GitGutterConfig {} - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] -pub struct ProjectPanelSettings { - pub dock: ProjectPanelDockPosition, - pub default_width: f32, -} -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] -pub struct ProjectPanelSettingsContent { - pub dock: Option, - pub default_width: Option, -} - -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "lowercase")] -pub enum ProjectPanelDockPosition { - Left, - Right, -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] -pub struct EditorSettings { - pub tab_size: Option, - pub hard_tabs: Option, - pub soft_wrap: Option, - pub preferred_line_length: Option, - pub format_on_save: Option, - pub remove_trailing_whitespace_on_save: Option, - pub ensure_final_newline_on_save: Option, - pub formatter: Option, - pub enable_language_server: Option, - pub show_copilot_suggestions: Option, - pub show_whitespaces: Option, -} - -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum SoftWrap { - None, - EditorWidth, - PreferredLineLength, -} -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum FormatOnSave { - On, - Off, - LanguageServer, - External { - command: String, - arguments: Vec, - }, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum Formatter { - LanguageServer, - External { - command: String, - arguments: Vec, - }, -} - -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum Autosave { - Off, - AfterDelay { milliseconds: u64 }, - OnFocusChange, - OnWindowChange, -} - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] -pub struct JournalSettings { - pub path: Option, - pub hour_format: Option, -} - -impl Default for JournalSettings { - fn default() -> Self { - Self { - path: Some("~".into()), - hour_format: Some(Default::default()), - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum HourFormat { - Hour12, - Hour24, -} - -impl Default for HourFormat { - fn default() -> Self { - Self::Hour12 - } -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] -pub struct TerminalSettings { - pub default_width: Option, - pub default_height: Option, - 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, -} - -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "lowercase")] -pub enum TerminalDockPosition { - Left, - Bottom, - Right, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)] -#[serde(rename_all = "snake_case")] -pub enum TerminalLineHeight { - #[default] - Comfortable, - Standard, - Custom(f32), -} - -impl TerminalLineHeight { - fn value(&self) -> f32 { - match self { - TerminalLineHeight::Comfortable => 1.618, - TerminalLineHeight::Standard => 1.3, - TerminalLineHeight::Custom(line_height) => *line_height, - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum TerminalBlink { - Off, - TerminalControlled, - On, -} - -impl Default for TerminalBlink { - fn default() -> Self { - TerminalBlink::TerminalControlled - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum Shell { - System, - Program(String), - WithArguments { program: String, args: Vec }, -} - -impl Default for Shell { - fn default() -> Self { - Shell::System - } -} - -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum AlternateScroll { - On, - Off, -} - -impl Default for AlternateScroll { - fn default() -> Self { - AlternateScroll::On - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum WorkingDirectory { - CurrentProjectDirectory, - FirstProjectDirectory, - AlwaysHome, - Always { directory: String }, -} - -impl Default for WorkingDirectory { - fn default() -> Self { - Self::CurrentProjectDirectory - } -} - -impl TerminalSettings { - fn line_height(&self) -> Option { - self.line_height - .to_owned() - .map(|line_height| line_height.value()) - } -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] -pub struct SettingsFileContent { - #[serde(default)] - pub buffer_font_family: Option, - #[serde(default)] - pub buffer_font_size: Option, - #[serde(default)] - pub buffer_font_features: Option, - #[serde(default)] - pub copilot: Option, - #[serde(default)] - pub active_pane_magnification: Option, - #[serde(default)] - pub cursor_blink: Option, - #[serde(default)] - pub confirm_quit: Option, - #[serde(default)] - pub hover_popover_enabled: Option, - #[serde(default)] - pub show_completions_on_input: Option, - #[serde(default)] - pub show_call_status_icon: Option, - #[serde(default)] - pub vim_mode: Option, - #[serde(default)] - pub autosave: Option, - #[serde(flatten)] - pub editor: EditorSettings, - #[serde(default)] - pub project_panel: ProjectPanelSettingsContent, - #[serde(default)] - pub journal: JournalSettings, - #[serde(default)] - pub terminal: TerminalSettings, - #[serde(default)] - pub git: Option, - #[serde(default)] - #[serde(alias = "language_overrides")] - pub languages: HashMap, EditorSettings>, - #[serde(default)] - pub lsp: HashMap, LspSettings>, - #[serde(default)] - pub theme: Option, - #[serde(default)] - pub telemetry: TelemetrySettings, - #[serde(default)] - pub auto_update: Option, - #[serde(default)] - pub base_keymap: Option, - #[serde(default)] - pub features: FeaturesContent, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub struct LspSettings { - pub initialization_options: Option, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Features { - pub copilot: bool, -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -pub struct FeaturesContent { - pub copilot: Option, -} - -#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum ShowWhitespaces { - #[default] - Selection, - None, - All, -} - -impl Settings { - pub fn initial_user_settings_content(assets: &'static impl AssetSource) -> Cow<'static, str> { - match assets.load(INITIAL_USER_SETTINGS_ASSET_PATH).unwrap() { - Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()), - Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()), - } - } - - /// Fill out the settings corresponding to the default.json file, overrides will be set later - pub fn defaults( - assets: impl AssetSource, - font_cache: &FontCache, - themes: &ThemeRegistry, - ) -> Self { - #[track_caller] - fn required(value: Option) -> Option { - assert!(value.is_some(), "missing default setting value"); - value - } - - let defaults: SettingsFileContent = parse_json_with_comments( - str::from_utf8(assets.load(DEFAULT_SETTINGS_ASSET_PATH).unwrap().as_ref()).unwrap(), - ) - .unwrap(); - - let buffer_font_features = defaults.buffer_font_features.unwrap(); - Self { - buffer_font_family: font_cache - .load_family( - &[defaults.buffer_font_family.as_ref().unwrap()], - &buffer_font_features, - ) - .unwrap(), - buffer_font_family_name: defaults.buffer_font_family.unwrap(), - buffer_font_features, - buffer_font_size: defaults.buffer_font_size.unwrap(), - active_pane_magnification: defaults.active_pane_magnification.unwrap(), - default_buffer_font_size: defaults.buffer_font_size.unwrap(), - confirm_quit: defaults.confirm_quit.unwrap(), - cursor_blink: defaults.cursor_blink.unwrap(), - hover_popover_enabled: defaults.hover_popover_enabled.unwrap(), - show_completions_on_input: defaults.show_completions_on_input.unwrap(), - show_call_status_icon: defaults.show_call_status_icon.unwrap(), - vim_mode: defaults.vim_mode.unwrap(), - autosave: defaults.autosave.unwrap(), - project_panel: ProjectPanelSettings { - dock: defaults.project_panel.dock.unwrap(), - default_width: defaults.project_panel.default_width.unwrap(), - }, - editor_defaults: EditorSettings { - tab_size: required(defaults.editor.tab_size), - hard_tabs: required(defaults.editor.hard_tabs), - soft_wrap: required(defaults.editor.soft_wrap), - preferred_line_length: required(defaults.editor.preferred_line_length), - remove_trailing_whitespace_on_save: required( - defaults.editor.remove_trailing_whitespace_on_save, - ), - ensure_final_newline_on_save: required( - defaults.editor.ensure_final_newline_on_save, - ), - format_on_save: required(defaults.editor.format_on_save), - formatter: required(defaults.editor.formatter), - enable_language_server: required(defaults.editor.enable_language_server), - show_copilot_suggestions: required(defaults.editor.show_copilot_suggestions), - show_whitespaces: required(defaults.editor.show_whitespaces), - }, - editor_overrides: Default::default(), - copilot: CopilotSettings { - disabled_globs: defaults - .copilot - .unwrap() - .disabled_globs - .unwrap() - .into_iter() - .map(|s| glob::Pattern::new(&s).unwrap()) - .collect(), - }, - git: defaults.git.unwrap(), - git_overrides: Default::default(), - journal_defaults: defaults.journal, - journal_overrides: Default::default(), - terminal_defaults: defaults.terminal, - terminal_overrides: Default::default(), - language_defaults: defaults.languages, - language_overrides: Default::default(), - lsp: defaults.lsp.clone(), - theme: themes.get(&defaults.theme.unwrap()).unwrap(), - telemetry_defaults: defaults.telemetry, - telemetry_overrides: Default::default(), - auto_update: defaults.auto_update.unwrap(), - base_keymap: Default::default(), - features: Features { - copilot: defaults.features.copilot.unwrap(), - }, - } - } - - // Fill out the overrride and etc. settings from the user's settings.json - pub fn set_user_settings( - &mut self, - data: SettingsFileContent, - theme_registry: &ThemeRegistry, - font_cache: &FontCache, - ) { - let mut family_changed = false; - if let Some(value) = data.buffer_font_family { - self.buffer_font_family_name = value; - family_changed = true; - } - if let Some(value) = data.buffer_font_features { - self.buffer_font_features = value; - family_changed = true; - } - if family_changed { - if let Some(id) = font_cache - .load_family(&[&self.buffer_font_family_name], &self.buffer_font_features) - .log_err() - { - self.buffer_font_family = id; - } - } - - if let Some(value) = &data.theme { - if let Some(theme) = theme_registry.get(value).log_err() { - self.theme = theme; - } - } - - merge(&mut self.buffer_font_size, data.buffer_font_size); - merge( - &mut self.active_pane_magnification, - data.active_pane_magnification, - ); - merge(&mut self.default_buffer_font_size, data.buffer_font_size); - merge(&mut self.cursor_blink, data.cursor_blink); - merge(&mut self.confirm_quit, data.confirm_quit); - merge(&mut self.hover_popover_enabled, data.hover_popover_enabled); - merge( - &mut self.show_completions_on_input, - data.show_completions_on_input, - ); - merge(&mut self.vim_mode, data.vim_mode); - merge(&mut self.autosave, data.autosave); - merge(&mut self.base_keymap, data.base_keymap); - merge(&mut self.features.copilot, data.features.copilot); - - if let Some(copilot) = data.copilot { - if let Some(disabled_globs) = copilot.disabled_globs { - self.copilot.disabled_globs = disabled_globs - .into_iter() - .filter_map(|s| glob::Pattern::new(&s).ok()) - .collect() - } - } - self.editor_overrides = data.editor; - merge(&mut self.project_panel.dock, data.project_panel.dock); - merge( - &mut self.project_panel.default_width, - data.project_panel.default_width, - ); - self.git_overrides = data.git.unwrap_or_default(); - self.journal_overrides = data.journal; - self.terminal_defaults.font_size = data.terminal.font_size; - self.terminal_overrides.copy_on_select = data.terminal.copy_on_select; - self.terminal_overrides = data.terminal; - self.language_overrides = data.languages; - self.telemetry_overrides = data.telemetry; - self.lsp = data.lsp; - merge(&mut self.auto_update, data.auto_update); - } - - pub fn with_language_defaults( - mut self, - language_name: impl Into>, - overrides: EditorSettings, - ) -> Self { - self.language_defaults - .insert(language_name.into(), overrides); - self - } - - pub fn features(&self) -> &Features { - &self.features - } - - pub fn show_copilot_suggestions(&self, language: Option<&str>, path: Option<&Path>) -> bool { - if !self.features.copilot { - return false; - } - - if !self.copilot_enabled_for_language(language) { - return false; - } - - if let Some(path) = path { - if !self.copilot_enabled_for_path(path) { - return false; - } - } - - true - } - - pub fn copilot_enabled_for_path(&self, path: &Path) -> bool { - !self - .copilot - .disabled_globs - .iter() - .any(|glob| glob.matches_path(path)) - } - - pub fn copilot_enabled_for_language(&self, language: Option<&str>) -> bool { - self.language_setting(language, |settings| settings.show_copilot_suggestions) - } - - pub fn tab_size(&self, language: Option<&str>) -> NonZeroU32 { - self.language_setting(language, |settings| settings.tab_size) - } - - pub fn show_whitespaces(&self, language: Option<&str>) -> ShowWhitespaces { - self.language_setting(language, |settings| settings.show_whitespaces) - } - - pub fn hard_tabs(&self, language: Option<&str>) -> bool { - self.language_setting(language, |settings| settings.hard_tabs) - } - - pub fn soft_wrap(&self, language: Option<&str>) -> SoftWrap { - self.language_setting(language, |settings| settings.soft_wrap) - } - - pub fn preferred_line_length(&self, language: Option<&str>) -> u32 { - self.language_setting(language, |settings| settings.preferred_line_length) - } - - pub fn remove_trailing_whitespace_on_save(&self, language: Option<&str>) -> bool { - self.language_setting(language, |settings| { - settings.remove_trailing_whitespace_on_save.clone() - }) - } - - pub fn ensure_final_newline_on_save(&self, language: Option<&str>) -> bool { - self.language_setting(language, |settings| { - settings.ensure_final_newline_on_save.clone() - }) - } - - pub fn format_on_save(&self, language: Option<&str>) -> FormatOnSave { - self.language_setting(language, |settings| settings.format_on_save.clone()) - } - - pub fn formatter(&self, language: Option<&str>) -> Formatter { - self.language_setting(language, |settings| settings.formatter.clone()) - } - - pub fn enable_language_server(&self, language: Option<&str>) -> bool { - self.language_setting(language, |settings| settings.enable_language_server) - } - - fn language_setting(&self, language: Option<&str>, f: F) -> R - where - F: Fn(&EditorSettings) -> Option, - { - None.or_else(|| language.and_then(|l| self.language_overrides.get(l).and_then(&f))) - .or_else(|| f(&self.editor_overrides)) - .or_else(|| language.and_then(|l| self.language_defaults.get(l).and_then(&f))) - .or_else(|| f(&self.editor_defaults)) - .expect("missing default") - } - - pub fn git_gutter(&self) -> GitGutter { - self.git_overrides.git_gutter.unwrap_or_else(|| { - self.git - .git_gutter - .expect("git_gutter should be some by setting setup") - }) - } - - pub fn telemetry(&self) -> TelemetrySettings { - TelemetrySettings { - diagnostics: Some(self.telemetry_diagnostics()), - metrics: Some(self.telemetry_metrics()), - } - } - - pub fn telemetry_diagnostics(&self) -> bool { - self.telemetry_overrides - .diagnostics - .or(self.telemetry_defaults.diagnostics) - .expect("missing default") - } - - pub fn telemetry_metrics(&self) -> bool { - self.telemetry_overrides - .metrics - .or(self.telemetry_defaults.metrics) - .expect("missing default") - } - - fn terminal_setting(&self, f: F) -> R - where - F: Fn(&TerminalSettings) -> Option, - { - None.or_else(|| f(&self.terminal_overrides)) - .or_else(|| f(&self.terminal_defaults)) - .expect("missing default") - } - - pub fn terminal_line_height(&self) -> f32 { - self.terminal_setting(|terminal_setting| terminal_setting.line_height()) - } - - pub fn terminal_scroll(&self) -> AlternateScroll { - self.terminal_setting(|terminal_setting| terminal_setting.alternate_scroll.to_owned()) - } - - pub fn terminal_shell(&self) -> Shell { - self.terminal_setting(|terminal_setting| terminal_setting.shell.to_owned()) - } - - pub fn terminal_env(&self) -> HashMap { - self.terminal_setting(|terminal_setting| terminal_setting.env.to_owned()) - } - - pub fn terminal_strategy(&self) -> WorkingDirectory { - self.terminal_setting(|terminal_setting| terminal_setting.working_directory.to_owned()) - } - - #[cfg(any(test, feature = "test-support"))] - pub fn test(cx: &gpui::AppContext) -> Settings { - Settings { - buffer_font_family_name: "Monaco".to_string(), - buffer_font_features: Default::default(), - buffer_font_family: cx - .font_cache() - .load_family(&["Monaco"], &Default::default()) - .unwrap(), - buffer_font_size: 14., - active_pane_magnification: 1., - default_buffer_font_size: 14., - confirm_quit: false, - cursor_blink: true, - hover_popover_enabled: true, - show_completions_on_input: true, - show_call_status_icon: true, - vim_mode: false, - autosave: Autosave::Off, - project_panel: ProjectPanelSettings { - dock: ProjectPanelDockPosition::Left, - default_width: 240., - }, - editor_defaults: EditorSettings { - tab_size: Some(4.try_into().unwrap()), - hard_tabs: Some(false), - soft_wrap: Some(SoftWrap::None), - preferred_line_length: Some(80), - remove_trailing_whitespace_on_save: Some(true), - ensure_final_newline_on_save: Some(true), - format_on_save: Some(FormatOnSave::On), - formatter: Some(Formatter::LanguageServer), - enable_language_server: Some(true), - show_copilot_suggestions: Some(true), - show_whitespaces: Some(ShowWhitespaces::None), - }, - editor_overrides: Default::default(), - copilot: Default::default(), - journal_defaults: Default::default(), - journal_overrides: Default::default(), - terminal_defaults: Default::default(), - terminal_overrides: Default::default(), - git: Default::default(), - git_overrides: Default::default(), - language_defaults: Default::default(), - language_overrides: Default::default(), - lsp: Default::default(), - theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), Default::default), - telemetry_defaults: TelemetrySettings { - diagnostics: Some(true), - metrics: Some(true), - }, - telemetry_overrides: Default::default(), - auto_update: true, - base_keymap: Default::default(), - features: Features { copilot: true }, - } - } - - #[cfg(any(test, feature = "test-support"))] - pub fn test_async(cx: &mut gpui::TestAppContext) { - cx.update(|cx| { - let settings = Self::test(cx); - cx.set_global(settings); - }); - } -} - -pub fn settings_file_json_schema( - theme_names: Vec, - language_names: &[String], -) -> serde_json::Value { - let settings = SchemaSettings::draft07().with(|settings| { - settings.option_add_null_type = false; - }); - let generator = SchemaGenerator::new(settings); - - let mut root_schema = generator.into_root_schema_for::(); - - // Create a schema for a theme name. - let theme_name_schema = SchemaObject { - instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))), - enum_values: Some(theme_names.into_iter().map(Value::String).collect()), - ..Default::default() - }; - - // Create a schema for a 'languages overrides' object, associating editor - // settings with specific langauges. - assert!(root_schema.definitions.contains_key("EditorSettings")); - - let languages_object_schema = SchemaObject { - instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))), - object: Some(Box::new(ObjectValidation { - properties: language_names - .iter() - .map(|name| { - ( - name.clone(), - Schema::new_ref("#/definitions/EditorSettings".into()), - ) - }) - .collect(), - ..Default::default() - })), - ..Default::default() - }; - - // Add these new schemas as definitions, and modify properties of the root - // schema to reference them. - root_schema.definitions.extend([ - ("ThemeName".into(), theme_name_schema.into()), - ("Languages".into(), languages_object_schema.into()), - ]); - let root_schema_object = &mut root_schema.schema.object.as_mut().unwrap(); - - root_schema_object.properties.extend([ - ( - "theme".to_owned(), - Schema::new_ref("#/definitions/ThemeName".into()), - ), - ( - "languages".to_owned(), - Schema::new_ref("#/definitions/Languages".into()), - ), - // For backward compatibility - ( - "language_overrides".to_owned(), - Schema::new_ref("#/definitions/Languages".into()), - ), - ]); - - serde_json::to_value(root_schema).unwrap() -} - -fn merge(target: &mut T, value: Option) { - if let Some(value) = value { - *target = value; - } -} - -pub fn parse_json_with_comments(content: &str) -> Result { - Ok(serde_json::from_reader( - json_comments::CommentSettings::c_style().strip_comments(content.as_bytes()), - )?) -} - -lazy_static! { - static ref PAIR_QUERY: Query = Query::new( - tree_sitter_json::language(), - " - (pair - key: (string) @key - value: (_) @value) - ", - ) - .unwrap(); -} - -fn update_object_in_settings_file<'a>( - old_object: &'a serde_json::Map, - new_object: &'a serde_json::Map, - text: &str, - syntax_tree: &Tree, - tab_size: usize, - key_path: &mut Vec<&'a str>, - edits: &mut Vec<(Range, String)>, -) { - for (key, old_value) in old_object.iter() { - key_path.push(key); - let new_value = new_object.get(key).unwrap_or(&Value::Null); - - // If the old and new values are both objects, then compare them key by key, - // preserving the comments and formatting of the unchanged parts. Otherwise, - // replace the old value with the new value. - if let (Value::Object(old_sub_object), Value::Object(new_sub_object)) = - (old_value, new_value) - { - update_object_in_settings_file( - old_sub_object, - new_sub_object, - text, - syntax_tree, - tab_size, - key_path, - edits, - ) - } else if old_value != new_value { - let (range, replacement) = - update_key_in_settings_file(text, syntax_tree, &key_path, tab_size, &new_value); - edits.push((range, replacement)); - } - - key_path.pop(); - } -} - -fn update_key_in_settings_file( - text: &str, - syntax_tree: &Tree, - key_path: &[&str], - tab_size: usize, - new_value: impl Serialize, -) -> (Range, String) { - const LANGUAGE_OVERRIDES: &'static str = "language_overrides"; - const LANGUAGES: &'static str = "languages"; - - let mut cursor = tree_sitter::QueryCursor::new(); - - let has_language_overrides = text.contains(LANGUAGE_OVERRIDES); - - let mut depth = 0; - let mut last_value_range = 0..0; - let mut first_key_start = None; - let mut existing_value_range = 0..text.len(); - let matches = cursor.matches(&PAIR_QUERY, syntax_tree.root_node(), text.as_bytes()); - for mat in matches { - if mat.captures.len() != 2 { - continue; - } - - let key_range = mat.captures[0].node.byte_range(); - let value_range = mat.captures[1].node.byte_range(); - - // Don't enter sub objects until we find an exact - // match for the current keypath - if last_value_range.contains_inclusive(&value_range) { - continue; - } - - last_value_range = value_range.clone(); - - if key_range.start > existing_value_range.end { - break; - } - - first_key_start.get_or_insert_with(|| key_range.start); - - let found_key = text - .get(key_range.clone()) - .map(|key_text| { - if key_path[depth] == LANGUAGES && has_language_overrides { - return key_text == format!("\"{}\"", LANGUAGE_OVERRIDES); - } else { - return key_text == format!("\"{}\"", key_path[depth]); - } - }) - .unwrap_or(false); - - if found_key { - existing_value_range = value_range; - // Reset last value range when increasing in depth - last_value_range = existing_value_range.start..existing_value_range.start; - depth += 1; - - if depth == key_path.len() { - break; - } else { - first_key_start = None; - } - } - } - - // We found the exact key we want, insert the new value - if depth == key_path.len() { - let new_val = to_pretty_json(&new_value, tab_size, tab_size * depth); - (existing_value_range, new_val) - } else { - // We have key paths, construct the sub objects - let new_key = if has_language_overrides && key_path[depth] == LANGUAGES { - LANGUAGE_OVERRIDES - } else { - key_path[depth] - }; - - // We don't have the key, construct the nested objects - let mut new_value = serde_json::to_value(new_value).unwrap(); - for key in key_path[(depth + 1)..].iter().rev() { - if has_language_overrides && key == &LANGUAGES { - new_value = serde_json::json!({ LANGUAGE_OVERRIDES.to_string(): new_value }); - } else { - new_value = serde_json::json!({ key.to_string(): new_value }); - } - } - - if let Some(first_key_start) = first_key_start { - let mut row = 0; - let mut column = 0; - for (ix, char) in text.char_indices() { - if ix == first_key_start { - break; - } - if char == '\n' { - row += 1; - column = 0; - } else { - column += char.len_utf8(); - } - } - - if row > 0 { - // depth is 0 based, but division needs to be 1 based. - let new_val = to_pretty_json(&new_value, column / (depth + 1), column); - let space = ' '; - let content = format!("\"{new_key}\": {new_val},\n{space:width$}", width = column); - (first_key_start..first_key_start, content) - } else { - let new_val = serde_json::to_string(&new_value).unwrap(); - let mut content = format!(r#""{new_key}": {new_val},"#); - content.push(' '); - (first_key_start..first_key_start, content) - } - } else { - new_value = serde_json::json!({ new_key.to_string(): new_value }); - let indent_prefix_len = 4 * depth; - let mut new_val = to_pretty_json(&new_value, 4, indent_prefix_len); - if depth == 0 { - new_val.push('\n'); - } - - (existing_value_range, new_val) - } - } -} - -fn to_pretty_json(value: &impl Serialize, indent_size: usize, indent_prefix_len: usize) -> String { - const SPACES: [u8; 32] = [b' '; 32]; - - debug_assert!(indent_size <= SPACES.len()); - debug_assert!(indent_prefix_len <= SPACES.len()); - - let mut output = Vec::new(); - let mut ser = serde_json::Serializer::with_formatter( - &mut output, - serde_json::ser::PrettyFormatter::with_indent(&SPACES[0..indent_size.min(SPACES.len())]), - ); - - value.serialize(&mut ser).unwrap(); - let text = String::from_utf8(output).unwrap(); - - let mut adjusted_text = String::new(); - for (i, line) in text.split('\n').enumerate() { - if i > 0 { - adjusted_text.push_str(str::from_utf8(&SPACES[0..indent_prefix_len]).unwrap()); - } - adjusted_text.push_str(line); - adjusted_text.push('\n'); - } - adjusted_text.pop(); - adjusted_text -} - -/// Update the settings file with the given callback. -/// -/// Returns a new JSON string and the offset where the first edit occurred. -fn update_settings_file( - text: &str, - mut old_file_content: SettingsFileContent, - tab_size: NonZeroU32, - update: impl FnOnce(&mut SettingsFileContent), -) -> Vec<(Range, String)> { - let mut new_file_content = old_file_content.clone(); - update(&mut new_file_content); - - if new_file_content.languages.len() != old_file_content.languages.len() { - for language in new_file_content.languages.keys() { - old_file_content - .languages - .entry(language.clone()) - .or_default(); - } - for language in old_file_content.languages.keys() { - new_file_content - .languages - .entry(language.clone()) - .or_default(); - } - } - - let mut parser = tree_sitter::Parser::new(); - parser.set_language(tree_sitter_json::language()).unwrap(); - let tree = parser.parse(text, None).unwrap(); - - let old_object = to_json_object(old_file_content); - let new_object = to_json_object(new_file_content); - let mut key_path = Vec::new(); - let mut edits = Vec::new(); - update_object_in_settings_file( - &old_object, - &new_object, - &text, - &tree, - tab_size.get() as usize, - &mut key_path, - &mut edits, - ); - edits.sort_unstable_by_key(|e| e.0.start); - return edits; -} - -fn to_json_object(settings_file: SettingsFileContent) -> serde_json::Map { - let tmp = serde_json::to_value(settings_file).unwrap(); - match tmp { - Value::Object(map) => map, - _ => unreachable!("SettingsFileContent represents a JSON map"), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use unindent::Unindent; - - fn assert_new_settings( - old_json: String, - update: fn(&mut SettingsFileContent), - expected_new_json: String, - ) { - let old_content: SettingsFileContent = serde_json::from_str(&old_json).unwrap_or_default(); - let edits = update_settings_file(&old_json, old_content, 4.try_into().unwrap(), update); - let mut new_json = old_json; - for (range, replacement) in edits.into_iter().rev() { - new_json.replace_range(range, &replacement); - } - pretty_assertions::assert_eq!(new_json, expected_new_json); - } - - #[test] - fn test_update_language_overrides_copilot() { - assert_new_settings( - r#" - { - "language_overrides": { - "JSON": { - "show_copilot_suggestions": false - } - } - } - "# - .unindent(), - |settings| { - settings.languages.insert( - "Rust".into(), - EditorSettings { - show_copilot_suggestions: Some(true), - ..Default::default() - }, - ); - }, - r#" - { - "language_overrides": { - "Rust": { - "show_copilot_suggestions": true - }, - "JSON": { - "show_copilot_suggestions": false - } - } - } - "# - .unindent(), - ); - } - - #[test] - fn test_update_copilot_globs() { - assert_new_settings( - r#" - { - } - "# - .unindent(), - |settings| { - settings.copilot = Some(CopilotSettingsContent { - disabled_globs: Some(vec![]), - }); - }, - r#" - { - "copilot": { - "disabled_globs": [] - } - } - "# - .unindent(), - ); - - assert_new_settings( - r#" - { - "copilot": { - "disabled_globs": [ - "**/*.json" - ] - } - } - "# - .unindent(), - |settings| { - settings - .copilot - .get_or_insert(Default::default()) - .disabled_globs - .as_mut() - .unwrap() - .push(".env".into()); - }, - r#" - { - "copilot": { - "disabled_globs": [ - "**/*.json", - ".env" - ] - } - } - "# - .unindent(), - ); - } - - #[test] - fn test_update_copilot() { - assert_new_settings( - r#" - { - "languages": { - "JSON": { - "show_copilot_suggestions": false - } - } - } - "# - .unindent(), - |settings| { - settings.editor.show_copilot_suggestions = Some(true); - }, - r#" - { - "show_copilot_suggestions": true, - "languages": { - "JSON": { - "show_copilot_suggestions": false - } - } - } - "# - .unindent(), - ); - } - - #[test] - fn test_update_language_copilot() { - assert_new_settings( - r#" - { - "languages": { - "JSON": { - "show_copilot_suggestions": false - } - } - } - "# - .unindent(), - |settings| { - settings.languages.insert( - "Rust".into(), - EditorSettings { - show_copilot_suggestions: Some(true), - ..Default::default() - }, - ); - }, - r#" - { - "languages": { - "Rust": { - "show_copilot_suggestions": true - }, - "JSON": { - "show_copilot_suggestions": false - } - } - } - "# - .unindent(), - ); - } - - #[test] - fn test_update_telemetry_setting_multiple_fields() { - assert_new_settings( - r#" - { - "telemetry": { - "metrics": false, - "diagnostics": false - } - } - "# - .unindent(), - |settings| { - settings.telemetry.set_diagnostics(true); - settings.telemetry.set_metrics(true); - }, - r#" - { - "telemetry": { - "metrics": true, - "diagnostics": true - } - } - "# - .unindent(), - ); - } - - #[test] - fn test_update_telemetry_setting_weird_formatting() { - assert_new_settings( - r#"{ - "telemetry": { "metrics": false, "diagnostics": true } - }"# - .unindent(), - |settings| settings.telemetry.set_diagnostics(false), - r#"{ - "telemetry": { "metrics": false, "diagnostics": false } - }"# - .unindent(), - ); - } - - #[test] - fn test_update_telemetry_setting_other_fields() { - assert_new_settings( - r#" - { - "telemetry": { - "metrics": false, - "diagnostics": true - } - } - "# - .unindent(), - |settings| settings.telemetry.set_diagnostics(false), - r#" - { - "telemetry": { - "metrics": false, - "diagnostics": false - } - } - "# - .unindent(), - ); - } - - #[test] - fn test_update_telemetry_setting_empty_telemetry() { - assert_new_settings( - r#" - { - "telemetry": {} - } - "# - .unindent(), - |settings| settings.telemetry.set_diagnostics(false), - r#" - { - "telemetry": { - "diagnostics": false - } - } - "# - .unindent(), - ); - } - - #[test] - fn test_update_telemetry_setting_pre_existing() { - assert_new_settings( - r#" - { - "telemetry": { - "diagnostics": true - } - } - "# - .unindent(), - |settings| settings.telemetry.set_diagnostics(false), - r#" - { - "telemetry": { - "diagnostics": false - } - } - "# - .unindent(), - ); - } - - #[test] - fn test_update_telemetry_setting() { - assert_new_settings( - "{}".into(), - |settings| settings.telemetry.set_diagnostics(true), - r#" - { - "telemetry": { - "diagnostics": true - } - } - "# - .unindent(), - ); - } - - #[test] - fn test_update_object_empty_doc() { - assert_new_settings( - "".into(), - |settings| settings.telemetry.set_diagnostics(true), - r#" - { - "telemetry": { - "diagnostics": true - } - } - "# - .unindent(), - ); - } - - #[test] - fn test_write_theme_into_settings_with_theme() { - assert_new_settings( - r#" - { - "theme": "One Dark" - } - "# - .unindent(), - |settings| settings.theme = Some("summerfruit-light".to_string()), - r#" - { - "theme": "summerfruit-light" - } - "# - .unindent(), - ); - } - - #[test] - fn test_write_theme_into_empty_settings() { - assert_new_settings( - r#" - { - } - "# - .unindent(), - |settings| settings.theme = Some("summerfruit-light".to_string()), - r#" - { - "theme": "summerfruit-light" - } - "# - .unindent(), - ); - } - - #[test] - fn write_key_no_document() { - assert_new_settings( - "".to_string(), - |settings| settings.theme = Some("summerfruit-light".to_string()), - r#" - { - "theme": "summerfruit-light" - } - "# - .unindent(), - ); - } - - #[test] - fn test_write_theme_into_single_line_settings_without_theme() { - assert_new_settings( - r#"{ "a": "", "ok": true }"#.to_string(), - |settings| settings.theme = Some("summerfruit-light".to_string()), - r#"{ "theme": "summerfruit-light", "a": "", "ok": true }"#.to_string(), - ); - } - - #[test] - fn test_write_theme_pre_object_whitespace() { - assert_new_settings( - r#" { "a": "", "ok": true }"#.to_string(), - |settings| settings.theme = Some("summerfruit-light".to_string()), - r#" { "theme": "summerfruit-light", "a": "", "ok": true }"#.unindent(), - ); - } - - #[test] - fn test_write_theme_into_multi_line_settings_without_theme() { - assert_new_settings( - r#" - { - "a": "b" - } - "# - .unindent(), - |settings| settings.theme = Some("summerfruit-light".to_string()), - r#" - { - "theme": "summerfruit-light", - "a": "b" - } - "# - .unindent(), - ); +pub fn initial_user_settings_content(assets: &'static impl AssetSource) -> Cow<'static, str> { + match assets.load(INITIAL_USER_SETTINGS_ASSET_PATH).unwrap() { + Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()), + Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()), } } diff --git a/crates/settings/src/settings_file.rs b/crates/settings/src/settings_file.rs index 6402a07f5e..cca2909da2 100644 --- a/crates/settings/src/settings_file.rs +++ b/crates/settings/src/settings_file.rs @@ -1,367 +1,137 @@ -use crate::{update_settings_file, watched_json::WatchedJsonFile, Settings, SettingsFileContent}; +use crate::{settings_store::SettingsStore, Setting, DEFAULT_SETTINGS_ASSET_PATH}; use anyhow::Result; use assets::Assets; use fs::Fs; -use gpui::AppContext; -use std::{io::ErrorKind, ops::Range, path::Path, sync::Arc}; +use futures::{channel::mpsc, StreamExt}; +use gpui::{executor::Background, AppContext, AssetSource}; +use std::{borrow::Cow, io::ErrorKind, path::PathBuf, str, sync::Arc, time::Duration}; +use util::{paths, ResultExt}; -// TODO: Switch SettingsFile to open a worktree and buffer for synchronization -// And instant updates in the Zed editor -#[derive(Clone)] -pub struct SettingsFile { - path: &'static Path, - settings_file_content: WatchedJsonFile, - fs: Arc, +pub fn register(cx: &mut AppContext) { + cx.update_global::(|store, cx| { + store.register_setting::(cx); + }); } -impl SettingsFile { - pub fn new( - path: &'static Path, - settings_file_content: WatchedJsonFile, - fs: Arc, - ) -> Self { - SettingsFile { - path, - settings_file_content, - fs, - } - } +pub fn get<'a, T: Setting>(cx: &'a AppContext) -> &'a T { + cx.global::().get(None) +} - async fn load_settings(path: &Path, fs: &Arc) -> Result { - match fs.load(path).await { - result @ Ok(_) => result, - Err(err) => { - if let Some(e) = err.downcast_ref::() { - if e.kind() == ErrorKind::NotFound { - return Ok(Settings::initial_user_settings_content(&Assets).to_string()); +pub fn default_settings() -> Cow<'static, str> { + match Assets.load(DEFAULT_SETTINGS_ASSET_PATH).unwrap() { + Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()), + Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()), + } +} + +pub const EMPTY_THEME_NAME: &'static str = "empty-theme"; + +#[cfg(any(test, feature = "test-support"))] +pub fn test_settings() -> String { + let mut value = crate::settings_store::parse_json_with_comments::( + default_settings().as_ref(), + ) + .unwrap(); + util::merge_non_null_json_value_into( + serde_json::json!({ + "buffer_font_family": "Courier", + "buffer_font_features": {}, + "buffer_font_size": 14, + "theme": EMPTY_THEME_NAME, + }), + &mut value, + ); + value.as_object_mut().unwrap().remove("languages"); + serde_json::to_string(&value).unwrap() +} + +pub fn watch_config_file( + executor: Arc, + fs: Arc, + path: PathBuf, +) -> mpsc::UnboundedReceiver { + let (tx, rx) = mpsc::unbounded(); + executor + .spawn(async move { + let events = fs.watch(&path, Duration::from_millis(100)).await; + futures::pin_mut!(events); + loop { + if let Ok(contents) = fs.load(&path).await { + if !tx.unbounded_send(contents).is_ok() { + break; } } - return Err(err); + if events.next().await.is_none() { + break; + } } + }) + .detach(); + rx +} + +pub fn handle_settings_file_changes( + mut user_settings_file_rx: mpsc::UnboundedReceiver, + cx: &mut AppContext, +) { + let user_settings_content = cx.background().block(user_settings_file_rx.next()).unwrap(); + cx.update_global::(|store, cx| { + store + .set_user_settings(&user_settings_content, cx) + .log_err(); + }); + cx.spawn(move |mut cx| async move { + while let Some(user_settings_content) = user_settings_file_rx.next().await { + cx.update(|cx| { + cx.update_global::(|store, cx| { + store + .set_user_settings(&user_settings_content, cx) + .log_err(); + }); + cx.refresh_windows(); + }); + } + }) + .detach(); +} + +async fn load_settings(fs: &Arc) -> Result { + match fs.load(&paths::SETTINGS).await { + result @ Ok(_) => result, + Err(err) => { + if let Some(e) = err.downcast_ref::() { + if e.kind() == ErrorKind::NotFound { + return Ok(crate::initial_user_settings_content(&Assets).to_string()); + } + } + return Err(err); } } +} - pub fn update_unsaved( - text: &str, - cx: &AppContext, - update: impl FnOnce(&mut SettingsFileContent), - ) -> Vec<(Range, String)> { - let this = cx.global::(); - let tab_size = cx.global::().tab_size(Some("JSON")); - let current_file_content = this.settings_file_content.current(); - update_settings_file(&text, current_file_content, tab_size, update) - } +pub fn update_settings_file( + fs: Arc, + cx: &mut AppContext, + update: impl 'static + Send + FnOnce(&mut T::FileContent), +) { + cx.spawn(|cx| async move { + let old_text = cx + .background() + .spawn({ + let fs = fs.clone(); + async move { load_settings(&fs).await } + }) + .await?; - pub fn update( - cx: &mut AppContext, - update: impl 'static + Send + FnOnce(&mut SettingsFileContent), - ) { - let this = cx.global::(); - let tab_size = cx.global::().tab_size(Some("JSON")); - let current_file_content = this.settings_file_content.current(); - let fs = this.fs.clone(); - let path = this.path.clone(); + let new_text = cx.read(|cx| { + cx.global::() + .new_text_for_update::(old_text, update) + }); cx.background() - .spawn(async move { - let old_text = SettingsFile::load_settings(path, &fs).await?; - let edits = update_settings_file(&old_text, current_file_content, tab_size, update); - let mut new_text = old_text; - for (range, replacement) in edits.into_iter().rev() { - new_text.replace_range(range, &replacement); - } - fs.atomic_write(path.to_path_buf(), new_text).await?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - watch_files, watched_json::watch_settings_file, EditorSettings, Settings, SoftWrap, - }; - use fs::FakeFs; - use gpui::{actions, elements::*, Action, Entity, TestAppContext, View, ViewContext}; - use theme::ThemeRegistry; - - struct TestView; - - impl Entity for TestView { - type Event = (); - } - - impl View for TestView { - fn ui_name() -> &'static str { - "TestView" - } - - fn render(&mut self, _: &mut ViewContext) -> AnyElement { - Empty::new().into_any() - } - } - - #[gpui::test] - async fn test_base_keymap(cx: &mut gpui::TestAppContext) { - let executor = cx.background(); - let fs = FakeFs::new(executor.clone()); - let font_cache = cx.font_cache(); - - actions!(test, [A, B]); - // From the Atom keymap - actions!(workspace, [ActivatePreviousPane]); - // From the JetBrains keymap - actions!(pane, [ActivatePrevItem]); - - fs.save( - "/settings.json".as_ref(), - &r#" - { - "base_keymap": "Atom" - } - "# - .into(), - Default::default(), - ) - .await - .unwrap(); - - fs.save( - "/keymap.json".as_ref(), - &r#" - [ - { - "bindings": { - "backspace": "test::A" - } - } - ] - "# - .into(), - Default::default(), - ) - .await - .unwrap(); - - let settings_file = - WatchedJsonFile::new(fs.clone(), &executor, "/settings.json".as_ref()).await; - let keymaps_file = - WatchedJsonFile::new(fs.clone(), &executor, "/keymap.json".as_ref()).await; - - let default_settings = cx.read(Settings::test); - - cx.update(|cx| { - cx.add_global_action(|_: &A, _cx| {}); - cx.add_global_action(|_: &B, _cx| {}); - cx.add_global_action(|_: &ActivatePreviousPane, _cx| {}); - cx.add_global_action(|_: &ActivatePrevItem, _cx| {}); - watch_files( - default_settings, - settings_file, - ThemeRegistry::new((), font_cache), - keymaps_file, - cx, - ) - }); - - cx.foreground().run_until_parked(); - - let (window_id, _view) = cx.add_window(|_| TestView); - - // Test loading the keymap base at all - assert_key_bindings_for( - window_id, - cx, - vec![("backspace", &A), ("k", &ActivatePreviousPane)], - line!(), - ); - - // Test modifying the users keymap, while retaining the base keymap - fs.save( - "/keymap.json".as_ref(), - &r#" - [ - { - "bindings": { - "backspace": "test::B" - } - } - ] - "# - .into(), - Default::default(), - ) - .await - .unwrap(); - - cx.foreground().run_until_parked(); - - assert_key_bindings_for( - window_id, - cx, - vec![("backspace", &B), ("k", &ActivatePreviousPane)], - line!(), - ); - - // Test modifying the base, while retaining the users keymap - fs.save( - "/settings.json".as_ref(), - &r#" - { - "base_keymap": "JetBrains" - } - "# - .into(), - Default::default(), - ) - .await - .unwrap(); - - cx.foreground().run_until_parked(); - - assert_key_bindings_for( - window_id, - cx, - vec![("backspace", &B), ("[", &ActivatePrevItem)], - line!(), - ); - } - - fn assert_key_bindings_for<'a>( - window_id: usize, - cx: &TestAppContext, - actions: Vec<(&'static str, &'a dyn Action)>, - line: u32, - ) { - for (key, action) in actions { - // assert that... - assert!( - cx.available_actions(window_id, 0) - .into_iter() - .any(|(_, bound_action, b)| { - // action names match... - bound_action.name() == action.name() - && bound_action.namespace() == action.namespace() - // and key strokes contain the given key - && b.iter() - .any(|binding| binding.keystrokes().iter().any(|k| k.key == key)) - }), - "On {} Failed to find {} with key binding {}", - line, - action.name(), - key - ); - } - } - - #[gpui::test] - async fn test_watch_settings_files(cx: &mut gpui::TestAppContext) { - let executor = cx.background(); - let fs = FakeFs::new(executor.clone()); - let font_cache = cx.font_cache(); - - fs.save( - "/settings.json".as_ref(), - &r#" - { - "buffer_font_size": 24, - "soft_wrap": "editor_width", - "tab_size": 8, - "language_overrides": { - "Markdown": { - "tab_size": 2, - "preferred_line_length": 100, - "soft_wrap": "preferred_line_length" - } - } - } - "# - .into(), - Default::default(), - ) - .await - .unwrap(); - - let source = WatchedJsonFile::new(fs.clone(), &executor, "/settings.json".as_ref()).await; - - let default_settings = cx.read(Settings::test).with_language_defaults( - "JavaScript", - EditorSettings { - tab_size: Some(2.try_into().unwrap()), - ..Default::default() - }, - ); - cx.update(|cx| { - watch_settings_file( - default_settings.clone(), - source, - ThemeRegistry::new((), font_cache), - cx, - ) - }); - - cx.foreground().run_until_parked(); - let settings = cx.read(|cx| cx.global::().clone()); - assert_eq!(settings.buffer_font_size, 24.0); - - assert_eq!(settings.soft_wrap(None), SoftWrap::EditorWidth); - assert_eq!( - settings.soft_wrap(Some("Markdown")), - SoftWrap::PreferredLineLength - ); - assert_eq!( - settings.soft_wrap(Some("JavaScript")), - SoftWrap::EditorWidth - ); - - assert_eq!(settings.preferred_line_length(None), 80); - assert_eq!(settings.preferred_line_length(Some("Markdown")), 100); - assert_eq!(settings.preferred_line_length(Some("JavaScript")), 80); - - assert_eq!(settings.tab_size(None).get(), 8); - assert_eq!(settings.tab_size(Some("Markdown")).get(), 2); - assert_eq!(settings.tab_size(Some("JavaScript")).get(), 8); - - fs.save( - "/settings.json".as_ref(), - &"(garbage)".into(), - Default::default(), - ) - .await - .unwrap(); - // fs.remove_file("/settings.json".as_ref(), Default::default()) - // .await - // .unwrap(); - - cx.foreground().run_until_parked(); - let settings = cx.read(|cx| cx.global::().clone()); - assert_eq!(settings.buffer_font_size, 24.0); - - assert_eq!(settings.soft_wrap(None), SoftWrap::EditorWidth); - assert_eq!( - settings.soft_wrap(Some("Markdown")), - SoftWrap::PreferredLineLength - ); - assert_eq!( - settings.soft_wrap(Some("JavaScript")), - SoftWrap::EditorWidth - ); - - assert_eq!(settings.preferred_line_length(None), 80); - assert_eq!(settings.preferred_line_length(Some("Markdown")), 100); - assert_eq!(settings.preferred_line_length(Some("JavaScript")), 80); - - assert_eq!(settings.tab_size(None).get(), 8); - assert_eq!(settings.tab_size(Some("Markdown")).get(), 2); - assert_eq!(settings.tab_size(Some("JavaScript")).get(), 8); - - fs.remove_file("/settings.json".as_ref(), Default::default()) - .await - .unwrap(); - cx.foreground().run_until_parked(); - let settings = cx.read(|cx| cx.global::().clone()); - assert_eq!(settings.buffer_font_size, default_settings.buffer_font_size); - } + .spawn(async move { fs.atomic_write(paths::SETTINGS.clone(), new_text).await }) + .await?; + anyhow::Ok(()) + }) + .detach_and_log_err(cx); } diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs new file mode 100644 index 0000000000..dd81b05434 --- /dev/null +++ b/crates/settings/src/settings_store.rs @@ -0,0 +1,1246 @@ +use anyhow::Result; +use collections::{btree_map, hash_map, BTreeMap, HashMap}; +use gpui::AppContext; +use lazy_static::lazy_static; +use schemars::{gen::SchemaGenerator, schema::RootSchema, JsonSchema}; +use serde::{de::DeserializeOwned, Deserialize as _, Serialize}; +use smallvec::SmallVec; +use std::{ + any::{type_name, Any, TypeId}, + fmt::Debug, + ops::Range, + path::Path, + str, + sync::Arc, +}; +use util::{merge_non_null_json_value_into, RangeExt, ResultExt as _}; + +/// A value that can be defined as a user setting. +/// +/// Settings can be loaded from a combination of multiple JSON files. +pub trait Setting: 'static { + /// The name of a key within the JSON file from which this setting should + /// be deserialized. If this is `None`, then the setting will be deserialized + /// from the root object. + const KEY: Option<&'static str>; + + /// The type that is stored in an individual JSON file. + type FileContent: Clone + Serialize + DeserializeOwned + JsonSchema; + + /// The logic for combining together values from one or more JSON files into the + /// final value for this setting. + /// + /// The user values are ordered from least specific (the global settings file) + /// to most specific (the innermost local settings file). + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + cx: &AppContext, + ) -> Result + where + Self: Sized; + + fn json_schema( + generator: &mut SchemaGenerator, + _: &SettingsJsonSchemaParams, + _: &AppContext, + ) -> RootSchema { + generator.root_schema_for::() + } + + fn json_merge( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + ) -> Result { + let mut merged = serde_json::Value::Null; + for value in [default_value].iter().chain(user_values) { + merge_non_null_json_value_into(serde_json::to_value(value).unwrap(), &mut merged); + } + Ok(serde_json::from_value(merged)?) + } + + fn load_via_json_merge( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + ) -> Result + where + Self: DeserializeOwned, + { + let mut merged = serde_json::Value::Null; + for value in [default_value].iter().chain(user_values) { + merge_non_null_json_value_into(serde_json::to_value(value).unwrap(), &mut merged); + } + Ok(serde_json::from_value(merged)?) + } + + fn missing_default() -> anyhow::Error { + anyhow::anyhow!("missing default") + } +} + +pub struct SettingsJsonSchemaParams<'a> { + pub staff_mode: bool, + pub language_names: &'a [String], +} + +/// A set of strongly-typed setting values defined via multiple JSON files. +#[derive(Default)] +pub struct SettingsStore { + setting_values: HashMap>, + default_deserialized_settings: Option, + user_deserialized_settings: Option, + local_deserialized_settings: BTreeMap, serde_json::Value>, + tab_size_callback: Option<(TypeId, Box Option>)>, +} + +#[derive(Debug)] +struct SettingValue { + global_value: Option, + local_values: Vec<(Arc, T)>, +} + +trait AnySettingValue { + fn key(&self) -> Option<&'static str>; + fn setting_type_name(&self) -> &'static str; + fn deserialize_setting(&self, json: &serde_json::Value) -> Result; + fn load_setting( + &self, + default_value: &DeserializedSetting, + custom: &[DeserializedSetting], + cx: &AppContext, + ) -> Result>; + fn value_for_path(&self, path: Option<&Path>) -> &dyn Any; + fn set_global_value(&mut self, value: Box); + fn set_local_value(&mut self, path: Arc, value: Box); + fn json_schema( + &self, + generator: &mut SchemaGenerator, + _: &SettingsJsonSchemaParams, + cx: &AppContext, + ) -> RootSchema; +} + +struct DeserializedSetting(Box); + +impl SettingsStore { + /// Add a new type of setting to the store. + pub fn register_setting(&mut self, cx: &AppContext) { + let setting_type_id = TypeId::of::(); + let entry = self.setting_values.entry(setting_type_id); + if matches!(entry, hash_map::Entry::Occupied(_)) { + return; + } + + let setting_value = entry.or_insert(Box::new(SettingValue:: { + global_value: None, + local_values: Vec::new(), + })); + + if let Some(default_settings) = &self.default_deserialized_settings { + if let Some(default_settings) = setting_value + .deserialize_setting(default_settings) + .log_err() + { + let mut user_values_stack = Vec::new(); + + if let Some(user_settings) = &self.user_deserialized_settings { + if let Some(user_settings) = + setting_value.deserialize_setting(user_settings).log_err() + { + user_values_stack = vec![user_settings]; + } + } + + if let Some(setting) = setting_value + .load_setting(&default_settings, &user_values_stack, cx) + .log_err() + { + setting_value.set_global_value(setting); + } + } + } + } + + /// Get the value of a setting. + /// + /// Panics if the given setting type has not been registered, or if there is no + /// value for this setting. + pub fn get(&self, path: Option<&Path>) -> &T { + self.setting_values + .get(&TypeId::of::()) + .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::())) + .value_for_path(path) + .downcast_ref::() + .expect("no default value for setting type") + } + + /// Override the global value for a setting. + /// + /// The given value will be overwritten if the user settings file changes. + pub fn override_global(&mut self, value: T) { + self.setting_values + .get_mut(&TypeId::of::()) + .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::())) + .set_global_value(Box::new(value)) + } + + /// Get the user's settings as a raw JSON value. + /// + /// This is only for debugging and reporting. For user-facing functionality, + /// use the typed setting interface. + pub fn untyped_user_settings(&self) -> &serde_json::Value { + self.user_deserialized_settings + .as_ref() + .unwrap_or(&serde_json::Value::Null) + } + + #[cfg(any(test, feature = "test-support"))] + pub fn test(cx: &AppContext) -> Self { + let mut this = Self::default(); + this.set_default_settings(&crate::test_settings(), cx) + .unwrap(); + this.set_user_settings("{}", cx).unwrap(); + this + } + + /// Update the value of a setting in the user's global configuration. + /// + /// This is only for tests. Normally, settings are only loaded from + /// JSON files. + #[cfg(any(test, feature = "test-support"))] + pub fn update_user_settings( + &mut self, + cx: &AppContext, + update: impl FnOnce(&mut T::FileContent), + ) { + if self.user_deserialized_settings.is_none() { + self.set_user_settings("{}", cx).unwrap(); + } + let old_text = + serde_json::to_string(self.user_deserialized_settings.as_ref().unwrap()).unwrap(); + let new_text = self.new_text_for_update::(old_text, update); + self.set_user_settings(&new_text, cx).unwrap(); + } + + /// Update the value of a setting in a JSON file, returning the new text + /// for that JSON file. + pub fn new_text_for_update( + &self, + old_text: String, + update: impl FnOnce(&mut T::FileContent), + ) -> String { + let edits = self.edits_for_update::(&old_text, update); + let mut new_text = old_text; + for (range, replacement) in edits.into_iter() { + new_text.replace_range(range, &replacement); + } + new_text + } + + /// Update the value of a setting in a JSON file, returning a list + /// of edits to apply to the JSON file. + pub fn edits_for_update( + &self, + text: &str, + update: impl FnOnce(&mut T::FileContent), + ) -> Vec<(Range, String)> { + let setting_type_id = TypeId::of::(); + + let old_content = self + .setting_values + .get(&setting_type_id) + .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::())) + .deserialize_setting( + self.user_deserialized_settings + .as_ref() + .expect("no user settings loaded"), + ) + .unwrap_or_else(|e| { + panic!( + "could not deserialize setting type {} from user settings: {}", + type_name::(), + e + ) + }) + .0 + .downcast::() + .unwrap(); + let mut new_content = old_content.clone(); + update(&mut new_content); + + let old_value = &serde_json::to_value(&old_content).unwrap(); + let new_value = serde_json::to_value(new_content).unwrap(); + + let mut key_path = Vec::new(); + if let Some(key) = T::KEY { + key_path.push(key); + } + + let mut edits = Vec::new(); + let tab_size = self.json_tab_size(); + let mut text = text.to_string(); + update_value_in_json_text( + &mut text, + &mut key_path, + tab_size, + &old_value, + &new_value, + &mut edits, + ); + return edits; + } + + /// Configure the tab sized when updating JSON files. + pub fn set_json_tab_size_callback( + &mut self, + get_tab_size: fn(&T) -> Option, + ) { + self.tab_size_callback = Some(( + TypeId::of::(), + Box::new(move |value| get_tab_size(value.downcast_ref::().unwrap())), + )); + } + + fn json_tab_size(&self) -> usize { + const DEFAULT_JSON_TAB_SIZE: usize = 2; + + if let Some((setting_type_id, callback)) = &self.tab_size_callback { + let setting_value = self.setting_values.get(setting_type_id).unwrap(); + let value = setting_value.value_for_path(None); + if let Some(value) = callback(value) { + return value; + } + } + + DEFAULT_JSON_TAB_SIZE + } + + /// Set the default settings via a JSON string. + /// + /// The string should contain a JSON object with a default value for every setting. + pub fn set_default_settings( + &mut self, + default_settings_content: &str, + cx: &AppContext, + ) -> Result<()> { + self.default_deserialized_settings = + Some(parse_json_with_comments(default_settings_content)?); + self.recompute_values(None, cx)?; + Ok(()) + } + + /// Set the user settings via a JSON string. + pub fn set_user_settings( + &mut self, + user_settings_content: &str, + cx: &AppContext, + ) -> Result<()> { + self.user_deserialized_settings = Some(parse_json_with_comments(user_settings_content)?); + self.recompute_values(None, cx)?; + Ok(()) + } + + /// Add or remove a set of local settings via a JSON string. + pub fn set_local_settings( + &mut self, + path: Arc, + settings_content: Option<&str>, + cx: &AppContext, + ) -> Result<()> { + if let Some(content) = settings_content { + self.local_deserialized_settings + .insert(path.clone(), parse_json_with_comments(content)?); + } else { + self.local_deserialized_settings.remove(&path); + } + self.recompute_values(Some(&path), cx)?; + Ok(()) + } + + pub fn json_schema( + &self, + schema_params: &SettingsJsonSchemaParams, + cx: &AppContext, + ) -> serde_json::Value { + use schemars::{ + gen::SchemaSettings, + schema::{Schema, SchemaObject}, + }; + + let settings = SchemaSettings::draft07().with(|settings| { + settings.option_add_null_type = false; + }); + let mut generator = SchemaGenerator::new(settings); + let mut combined_schema = RootSchema::default(); + + for setting_value in self.setting_values.values() { + let setting_schema = setting_value.json_schema(&mut generator, schema_params, cx); + combined_schema + .definitions + .extend(setting_schema.definitions); + + let target_schema = if let Some(key) = setting_value.key() { + let key_schema = combined_schema + .schema + .object() + .properties + .entry(key.to_string()) + .or_insert_with(|| Schema::Object(SchemaObject::default())); + if let Schema::Object(key_schema) = key_schema { + key_schema + } else { + continue; + } + } else { + &mut combined_schema.schema + }; + + merge_schema(target_schema, setting_schema.schema); + } + + fn merge_schema(target: &mut SchemaObject, source: SchemaObject) { + if let Some(source) = source.object { + let target_properties = &mut target.object().properties; + for (key, value) in source.properties { + match target_properties.entry(key) { + btree_map::Entry::Vacant(e) => { + e.insert(value); + } + btree_map::Entry::Occupied(e) => { + if let (Schema::Object(target), Schema::Object(src)) = + (e.into_mut(), value) + { + merge_schema(target, src); + } + } + } + } + } + + overwrite(&mut target.instance_type, source.instance_type); + overwrite(&mut target.string, source.string); + overwrite(&mut target.number, source.number); + overwrite(&mut target.reference, source.reference); + overwrite(&mut target.array, source.array); + overwrite(&mut target.enum_values, source.enum_values); + + fn overwrite(target: &mut Option, source: Option) { + if let Some(source) = source { + *target = Some(source); + } + } + } + + serde_json::to_value(&combined_schema).unwrap() + } + + fn recompute_values( + &mut self, + changed_local_path: Option<&Path>, + cx: &AppContext, + ) -> Result<()> { + // Reload the global and local values for every setting. + let mut user_settings_stack = Vec::::new(); + let mut paths_stack = Vec::>::new(); + for setting_value in self.setting_values.values_mut() { + if let Some(default_settings) = &self.default_deserialized_settings { + let default_settings = setting_value.deserialize_setting(default_settings)?; + + user_settings_stack.clear(); + paths_stack.clear(); + + if let Some(user_settings) = &self.user_deserialized_settings { + if let Some(user_settings) = + setting_value.deserialize_setting(user_settings).log_err() + { + user_settings_stack.push(user_settings); + paths_stack.push(None); + } + } + + // If the global settings file changed, reload the global value for the field. + if changed_local_path.is_none() { + setting_value.set_global_value(setting_value.load_setting( + &default_settings, + &user_settings_stack, + cx, + )?); + } + + // Reload the local values for the setting. + for (path, local_settings) in &self.local_deserialized_settings { + // Build a stack of all of the local values for that setting. + while let Some(prev_path) = paths_stack.last() { + if let Some(prev_path) = prev_path { + if !path.starts_with(prev_path) { + paths_stack.pop(); + user_settings_stack.pop(); + continue; + } + } + break; + } + + if let Some(local_settings) = + setting_value.deserialize_setting(&local_settings).log_err() + { + paths_stack.push(Some(path.as_ref())); + user_settings_stack.push(local_settings); + + // If a local settings file changed, then avoid recomputing local + // settings for any path outside of that directory. + if changed_local_path.map_or(false, |changed_local_path| { + !path.starts_with(changed_local_path) + }) { + continue; + } + + setting_value.set_local_value( + path.clone(), + setting_value.load_setting( + &default_settings, + &user_settings_stack, + cx, + )?, + ); + } + } + } + } + Ok(()) + } +} + +impl AnySettingValue for SettingValue { + fn key(&self) -> Option<&'static str> { + T::KEY + } + + fn setting_type_name(&self) -> &'static str { + type_name::() + } + + fn load_setting( + &self, + default_value: &DeserializedSetting, + user_values: &[DeserializedSetting], + cx: &AppContext, + ) -> Result> { + let default_value = default_value.0.downcast_ref::().unwrap(); + let values: SmallVec<[&T::FileContent; 6]> = user_values + .iter() + .map(|value| value.0.downcast_ref().unwrap()) + .collect(); + Ok(Box::new(T::load(default_value, &values, cx)?)) + } + + fn deserialize_setting(&self, mut json: &serde_json::Value) -> Result { + if let Some(key) = T::KEY { + json = json.get(key).unwrap_or(&serde_json::Value::Null); + } + let value = T::FileContent::deserialize(json)?; + Ok(DeserializedSetting(Box::new(value))) + } + + fn value_for_path(&self, path: Option<&Path>) -> &dyn Any { + if let Some(path) = path { + for (settings_path, value) in self.local_values.iter().rev() { + if path.starts_with(&settings_path) { + return value; + } + } + } + self.global_value + .as_ref() + .unwrap_or_else(|| panic!("no default value for setting {}", self.setting_type_name())) + } + + fn set_global_value(&mut self, value: Box) { + self.global_value = Some(*value.downcast().unwrap()); + } + + fn set_local_value(&mut self, path: Arc, value: Box) { + let value = *value.downcast().unwrap(); + match self.local_values.binary_search_by_key(&&path, |e| &e.0) { + Ok(ix) => self.local_values[ix].1 = value, + Err(ix) => self.local_values.insert(ix, (path, value)), + } + } + + fn json_schema( + &self, + generator: &mut SchemaGenerator, + params: &SettingsJsonSchemaParams, + cx: &AppContext, + ) -> RootSchema { + T::json_schema(generator, params, cx) + } +} + +// impl Debug for SettingsStore { +// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +// return f +// .debug_struct("SettingsStore") +// .field( +// "setting_value_sets_by_type", +// &self +// .setting_values +// .values() +// .map(|set| (set.setting_type_name(), set)) +// .collect::>(), +// ) +// .finish_non_exhaustive(); +// } +// } + +fn update_value_in_json_text<'a>( + text: &mut String, + key_path: &mut Vec<&'a str>, + tab_size: usize, + old_value: &'a serde_json::Value, + new_value: &'a serde_json::Value, + edits: &mut Vec<(Range, String)>, +) { + // If the old and new values are both objects, then compare them key by key, + // preserving the comments and formatting of the unchanged parts. Otherwise, + // replace the old value with the new value. + if let (serde_json::Value::Object(old_object), serde_json::Value::Object(new_object)) = + (old_value, new_value) + { + for (key, old_sub_value) in old_object.iter() { + key_path.push(key); + let new_sub_value = new_object.get(key).unwrap_or(&serde_json::Value::Null); + update_value_in_json_text( + text, + key_path, + tab_size, + old_sub_value, + new_sub_value, + edits, + ); + key_path.pop(); + } + for (key, new_sub_value) in new_object.iter() { + key_path.push(key); + if !old_object.contains_key(key) { + update_value_in_json_text( + text, + key_path, + tab_size, + &serde_json::Value::Null, + new_sub_value, + edits, + ); + } + key_path.pop(); + } + } else if old_value != new_value { + let (range, replacement) = + replace_value_in_json_text(text, &key_path, tab_size, &new_value); + text.replace_range(range.clone(), &replacement); + edits.push((range, replacement)); + } +} + +fn replace_value_in_json_text( + text: &str, + key_path: &[&str], + tab_size: usize, + new_value: impl Serialize, +) -> (Range, String) { + const LANGUAGE_OVERRIDES: &'static str = "language_overrides"; + const LANGUAGES: &'static str = "languages"; + + lazy_static! { + static ref PAIR_QUERY: tree_sitter::Query = tree_sitter::Query::new( + tree_sitter_json::language(), + "(pair key: (string) @key value: (_) @value)", + ) + .unwrap(); + } + + let mut parser = tree_sitter::Parser::new(); + parser.set_language(tree_sitter_json::language()).unwrap(); + let syntax_tree = parser.parse(text, None).unwrap(); + + let mut cursor = tree_sitter::QueryCursor::new(); + + let has_language_overrides = text.contains(LANGUAGE_OVERRIDES); + + let mut depth = 0; + let mut last_value_range = 0..0; + let mut first_key_start = None; + let mut existing_value_range = 0..text.len(); + let matches = cursor.matches(&PAIR_QUERY, syntax_tree.root_node(), text.as_bytes()); + for mat in matches { + if mat.captures.len() != 2 { + continue; + } + + let key_range = mat.captures[0].node.byte_range(); + let value_range = mat.captures[1].node.byte_range(); + + // Don't enter sub objects until we find an exact + // match for the current keypath + if last_value_range.contains_inclusive(&value_range) { + continue; + } + + last_value_range = value_range.clone(); + + if key_range.start > existing_value_range.end { + break; + } + + first_key_start.get_or_insert_with(|| key_range.start); + + let found_key = text + .get(key_range.clone()) + .map(|key_text| { + if key_path[depth] == LANGUAGES && has_language_overrides { + return key_text == format!("\"{}\"", LANGUAGE_OVERRIDES); + } else { + return key_text == format!("\"{}\"", key_path[depth]); + } + }) + .unwrap_or(false); + + if found_key { + existing_value_range = value_range; + // Reset last value range when increasing in depth + last_value_range = existing_value_range.start..existing_value_range.start; + depth += 1; + + if depth == key_path.len() { + break; + } else { + first_key_start = None; + } + } + } + + // We found the exact key we want, insert the new value + if depth == key_path.len() { + let new_val = to_pretty_json(&new_value, tab_size, tab_size * depth); + (existing_value_range, new_val) + } else { + // We have key paths, construct the sub objects + let new_key = if has_language_overrides && key_path[depth] == LANGUAGES { + LANGUAGE_OVERRIDES + } else { + key_path[depth] + }; + + // We don't have the key, construct the nested objects + let mut new_value = serde_json::to_value(new_value).unwrap(); + for key in key_path[(depth + 1)..].iter().rev() { + if has_language_overrides && key == &LANGUAGES { + new_value = serde_json::json!({ LANGUAGE_OVERRIDES.to_string(): new_value }); + } else { + new_value = serde_json::json!({ key.to_string(): new_value }); + } + } + + if let Some(first_key_start) = first_key_start { + let mut row = 0; + let mut column = 0; + for (ix, char) in text.char_indices() { + if ix == first_key_start { + break; + } + if char == '\n' { + row += 1; + column = 0; + } else { + column += char.len_utf8(); + } + } + + if row > 0 { + // depth is 0 based, but division needs to be 1 based. + let new_val = to_pretty_json(&new_value, column / (depth + 1), column); + let space = ' '; + let content = format!("\"{new_key}\": {new_val},\n{space:width$}", width = column); + (first_key_start..first_key_start, content) + } else { + let new_val = serde_json::to_string(&new_value).unwrap(); + let mut content = format!(r#""{new_key}": {new_val},"#); + content.push(' '); + (first_key_start..first_key_start, content) + } + } else { + new_value = serde_json::json!({ new_key.to_string(): new_value }); + let indent_prefix_len = 4 * depth; + let mut new_val = to_pretty_json(&new_value, 4, indent_prefix_len); + if depth == 0 { + new_val.push('\n'); + } + + (existing_value_range, new_val) + } + } +} + +fn to_pretty_json(value: &impl Serialize, indent_size: usize, indent_prefix_len: usize) -> String { + const SPACES: [u8; 32] = [b' '; 32]; + + debug_assert!(indent_size <= SPACES.len()); + debug_assert!(indent_prefix_len <= SPACES.len()); + + let mut output = Vec::new(); + let mut ser = serde_json::Serializer::with_formatter( + &mut output, + serde_json::ser::PrettyFormatter::with_indent(&SPACES[0..indent_size.min(SPACES.len())]), + ); + + value.serialize(&mut ser).unwrap(); + let text = String::from_utf8(output).unwrap(); + + let mut adjusted_text = String::new(); + for (i, line) in text.split('\n').enumerate() { + if i > 0 { + adjusted_text.push_str(str::from_utf8(&SPACES[0..indent_prefix_len]).unwrap()); + } + adjusted_text.push_str(line); + adjusted_text.push('\n'); + } + adjusted_text.pop(); + adjusted_text +} + +pub fn parse_json_with_comments(content: &str) -> Result { + Ok(serde_json::from_reader( + json_comments::CommentSettings::c_style().strip_comments(content.as_bytes()), + )?) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_derive::Deserialize; + use unindent::Unindent; + + #[gpui::test] + fn test_settings_store_basic(cx: &mut AppContext) { + let mut store = SettingsStore::default(); + store.register_setting::(cx); + store.register_setting::(cx); + store.register_setting::(cx); + + // error - missing required field in default settings + store + .set_default_settings( + r#"{ + "user": { + "name": "John Doe", + "age": 30, + "staff": false + } + }"#, + cx, + ) + .unwrap_err(); + + // error - type error in default settings + store + .set_default_settings( + r#"{ + "turbo": "the-wrong-type", + "user": { + "name": "John Doe", + "age": 30, + "staff": false + } + }"#, + cx, + ) + .unwrap_err(); + + // valid default settings. + store + .set_default_settings( + r#"{ + "turbo": false, + "user": { + "name": "John Doe", + "age": 30, + "staff": false + } + }"#, + cx, + ) + .unwrap(); + + assert_eq!(store.get::(None), &TurboSetting(false)); + assert_eq!( + store.get::(None), + &UserSettings { + name: "John Doe".to_string(), + age: 30, + staff: false, + } + ); + assert_eq!( + store.get::(None), + &MultiKeySettings { + key1: String::new(), + key2: String::new(), + } + ); + + store + .set_user_settings( + r#"{ + "turbo": true, + "user": { "age": 31 }, + "key1": "a" + }"#, + cx, + ) + .unwrap(); + + assert_eq!(store.get::(None), &TurboSetting(true)); + assert_eq!( + store.get::(None), + &UserSettings { + name: "John Doe".to_string(), + age: 31, + staff: false + } + ); + + store + .set_local_settings( + Path::new("/root1").into(), + Some(r#"{ "user": { "staff": true } }"#), + cx, + ) + .unwrap(); + store + .set_local_settings( + Path::new("/root1/subdir").into(), + Some(r#"{ "user": { "name": "Jane Doe" } }"#), + cx, + ) + .unwrap(); + + store + .set_local_settings( + Path::new("/root2").into(), + Some(r#"{ "user": { "age": 42 }, "key2": "b" }"#), + cx, + ) + .unwrap(); + + assert_eq!( + store.get::(Some(Path::new("/root1/something"))), + &UserSettings { + name: "John Doe".to_string(), + age: 31, + staff: true + } + ); + assert_eq!( + store.get::(Some(Path::new("/root1/subdir/something"))), + &UserSettings { + name: "Jane Doe".to_string(), + age: 31, + staff: true + } + ); + assert_eq!( + store.get::(Some(Path::new("/root2/something"))), + &UserSettings { + name: "John Doe".to_string(), + age: 42, + staff: false + } + ); + assert_eq!( + store.get::(Some(Path::new("/root2/something"))), + &MultiKeySettings { + key1: "a".to_string(), + key2: "b".to_string(), + } + ); + } + + #[gpui::test] + fn test_setting_store_assign_json_before_register(cx: &mut AppContext) { + let mut store = SettingsStore::default(); + store + .set_default_settings( + r#"{ + "turbo": true, + "user": { + "name": "John Doe", + "age": 30, + "staff": false + }, + "key1": "x" + }"#, + cx, + ) + .unwrap(); + store + .set_user_settings(r#"{ "turbo": false }"#, cx) + .unwrap(); + store.register_setting::(cx); + store.register_setting::(cx); + + assert_eq!(store.get::(None), &TurboSetting(false)); + assert_eq!( + store.get::(None), + &UserSettings { + name: "John Doe".to_string(), + age: 30, + staff: false, + } + ); + + store.register_setting::(cx); + assert_eq!( + store.get::(None), + &MultiKeySettings { + key1: "x".into(), + key2: String::new(), + } + ); + } + + #[gpui::test] + fn test_setting_store_update(cx: &mut AppContext) { + let mut store = SettingsStore::default(); + store.register_setting::(cx); + store.register_setting::(cx); + store.register_setting::(cx); + + // entries added and updated + check_settings_update::( + &mut store, + r#"{ + "languages": { + "JSON": { + "is_enabled": true + } + } + }"# + .unindent(), + |settings| { + settings.languages.get_mut("JSON").unwrap().is_enabled = false; + settings + .languages + .insert("Rust".into(), LanguageSettingEntry { is_enabled: true }); + }, + r#"{ + "languages": { + "Rust": { + "is_enabled": true + }, + "JSON": { + "is_enabled": false + } + } + }"# + .unindent(), + cx, + ); + + // weird formatting + check_settings_update::( + &mut store, + r#"{ + "user": { "age": 36, "name": "Max", "staff": true } + }"# + .unindent(), + |settings| settings.age = Some(37), + r#"{ + "user": { "age": 37, "name": "Max", "staff": true } + }"# + .unindent(), + cx, + ); + + // single-line formatting, other keys + check_settings_update::( + &mut store, + r#"{ "one": 1, "two": 2 }"#.unindent(), + |settings| settings.key1 = Some("x".into()), + r#"{ "key1": "x", "one": 1, "two": 2 }"#.unindent(), + cx, + ); + + // empty object + check_settings_update::( + &mut store, + r#"{ + "user": {} + }"# + .unindent(), + |settings| settings.age = Some(37), + r#"{ + "user": { + "age": 37 + } + }"# + .unindent(), + cx, + ); + + // no content + check_settings_update::( + &mut store, + r#""#.unindent(), + |settings| settings.age = Some(37), + r#"{ + "user": { + "age": 37 + } + } + "# + .unindent(), + cx, + ); + } + + fn check_settings_update( + store: &mut SettingsStore, + old_json: String, + update: fn(&mut T::FileContent), + expected_new_json: String, + cx: &mut AppContext, + ) { + store.set_user_settings(&old_json, cx).ok(); + let edits = store.edits_for_update::(&old_json, update); + let mut new_json = old_json; + for (range, replacement) in edits.into_iter() { + new_json.replace_range(range, &replacement); + } + pretty_assertions::assert_eq!(new_json, expected_new_json); + } + + #[derive(Debug, PartialEq, Deserialize)] + struct UserSettings { + name: String, + age: u32, + staff: bool, + } + + #[derive(Clone, Serialize, Deserialize, JsonSchema)] + struct UserSettingsJson { + name: Option, + age: Option, + staff: Option, + } + + impl Setting for UserSettings { + const KEY: Option<&'static str> = Some("user"); + type FileContent = UserSettingsJson; + + fn load( + default_value: &UserSettingsJson, + user_values: &[&UserSettingsJson], + _: &AppContext, + ) -> Result { + Self::load_via_json_merge(default_value, user_values) + } + } + + #[derive(Debug, Deserialize, PartialEq)] + struct TurboSetting(bool); + + impl Setting for TurboSetting { + const KEY: Option<&'static str> = Some("turbo"); + type FileContent = Option; + + fn load( + default_value: &Option, + user_values: &[&Option], + _: &AppContext, + ) -> Result { + Self::load_via_json_merge(default_value, user_values) + } + } + + #[derive(Clone, Debug, PartialEq, Deserialize)] + struct MultiKeySettings { + #[serde(default)] + key1: String, + #[serde(default)] + key2: String, + } + + #[derive(Clone, Serialize, Deserialize, JsonSchema)] + struct MultiKeySettingsJson { + key1: Option, + key2: Option, + } + + impl Setting for MultiKeySettings { + const KEY: Option<&'static str> = None; + + type FileContent = MultiKeySettingsJson; + + fn load( + default_value: &MultiKeySettingsJson, + user_values: &[&MultiKeySettingsJson], + _: &AppContext, + ) -> Result { + Self::load_via_json_merge(default_value, user_values) + } + } + + #[derive(Debug, Deserialize)] + struct JournalSettings { + pub path: String, + pub hour_format: HourFormat, + } + + #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] + #[serde(rename_all = "snake_case")] + enum HourFormat { + Hour12, + Hour24, + } + + #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] + struct JournalSettingsJson { + pub path: Option, + pub hour_format: Option, + } + + impl Setting for JournalSettings { + const KEY: Option<&'static str> = Some("journal"); + + type FileContent = JournalSettingsJson; + + fn load( + default_value: &JournalSettingsJson, + user_values: &[&JournalSettingsJson], + _: &AppContext, + ) -> Result { + Self::load_via_json_merge(default_value, user_values) + } + } + + #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] + struct LanguageSettings { + #[serde(default)] + languages: HashMap, + } + + #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] + struct LanguageSettingEntry { + is_enabled: bool, + } + + impl Setting for LanguageSettings { + const KEY: Option<&'static str> = None; + + type FileContent = Self; + + fn load(default_value: &Self, user_values: &[&Self], _: &AppContext) -> Result { + Self::load_via_json_merge(default_value, user_values) + } + } +} diff --git a/crates/settings/src/watched_json.rs b/crates/settings/src/watched_json.rs deleted file mode 100644 index 16be82fa35..0000000000 --- a/crates/settings/src/watched_json.rs +++ /dev/null @@ -1,126 +0,0 @@ -use fs::Fs; -use futures::StreamExt; -use gpui::{executor, AppContext}; -use postage::sink::Sink as _; -use postage::{prelude::Stream, watch}; -use serde::Deserialize; - -use std::{path::Path, sync::Arc, time::Duration}; -use theme::ThemeRegistry; -use util::ResultExt; - -use crate::{parse_json_with_comments, KeymapFileContent, Settings, SettingsFileContent}; - -#[derive(Clone)] -pub struct WatchedJsonFile(pub watch::Receiver); - -impl WatchedJsonFile -where - T: 'static + for<'de> Deserialize<'de> + Clone + Default + Send + Sync, -{ - pub async fn new( - fs: Arc, - executor: &executor::Background, - path: impl Into>, - ) -> Self { - let path = path.into(); - let settings = Self::load(fs.clone(), &path).await.unwrap_or_default(); - let mut events = fs.watch(&path, Duration::from_millis(500)).await; - let (mut tx, rx) = watch::channel_with(settings); - executor - .spawn(async move { - while events.next().await.is_some() { - if let Some(settings) = Self::load(fs.clone(), &path).await { - if tx.send(settings).await.is_err() { - break; - } - } - } - }) - .detach(); - Self(rx) - } - - ///Loads the given watched JSON file. In the special case that the file is - ///empty (ignoring whitespace) or is not a file, this will return T::default() - async fn load(fs: Arc, path: &Path) -> Option { - if !fs.is_file(path).await { - return Some(T::default()); - } - - fs.load(path).await.log_err().and_then(|data| { - if data.trim().is_empty() { - Some(T::default()) - } else { - parse_json_with_comments(&data).log_err() - } - }) - } - - pub fn current(&self) -> T { - self.0.borrow().clone() - } -} - -pub fn watch_files( - defaults: Settings, - settings_file: WatchedJsonFile, - theme_registry: Arc, - keymap_file: WatchedJsonFile, - cx: &mut AppContext, -) { - watch_settings_file(defaults, settings_file, theme_registry, cx); - watch_keymap_file(keymap_file, cx); -} - -pub(crate) fn watch_settings_file( - defaults: Settings, - mut file: WatchedJsonFile, - theme_registry: Arc, - cx: &mut AppContext, -) { - settings_updated(&defaults, file.0.borrow().clone(), &theme_registry, cx); - cx.spawn(|mut cx| async move { - while let Some(content) = file.0.recv().await { - cx.update(|cx| settings_updated(&defaults, content, &theme_registry, cx)); - } - }) - .detach(); -} - -fn keymap_updated(content: KeymapFileContent, cx: &mut AppContext) { - cx.clear_bindings(); - KeymapFileContent::load_defaults(cx); - content.add_to_cx(cx).log_err(); -} - -fn settings_updated( - defaults: &Settings, - content: SettingsFileContent, - theme_registry: &Arc, - cx: &mut AppContext, -) { - let mut settings = defaults.clone(); - settings.set_user_settings(content, theme_registry, cx.font_cache()); - cx.set_global(settings); - cx.refresh_windows(); -} - -fn watch_keymap_file(mut file: WatchedJsonFile, cx: &mut AppContext) { - cx.spawn(|mut cx| async move { - let mut settings_subscription = None; - while let Some(content) = file.0.recv().await { - cx.update(|cx| { - let old_base_keymap = cx.global::().base_keymap; - keymap_updated(content.clone(), cx); - settings_subscription = Some(cx.observe_global::(move |cx| { - let settings = cx.global::(); - if settings.base_keymap != old_base_keymap { - keymap_updated(content.clone(), cx); - } - })); - }); - } - }) - .detach(); -} diff --git a/crates/sum_tree/src/sum_tree.rs b/crates/sum_tree/src/sum_tree.rs index 3e916ccd1b..36f0f926cd 100644 --- a/crates/sum_tree/src/sum_tree.rs +++ b/crates/sum_tree/src/sum_tree.rs @@ -5,7 +5,7 @@ use arrayvec::ArrayVec; pub use cursor::{Cursor, FilterCursor, Iter}; use std::marker::PhantomData; use std::{cmp::Ordering, fmt, iter::FromIterator, sync::Arc}; -pub use tree_map::{TreeMap, TreeSet}; +pub use tree_map::{MapSeekTarget, TreeMap, TreeSet}; #[cfg(test)] const TREE_BASE: usize = 2; diff --git a/crates/sum_tree/src/tree_map.rs b/crates/sum_tree/src/tree_map.rs index 1b97cbec9f..ea69fb0dca 100644 --- a/crates/sum_tree/src/tree_map.rs +++ b/crates/sum_tree/src/tree_map.rs @@ -1,14 +1,14 @@ use std::{cmp::Ordering, fmt::Debug}; -use crate::{Bias, Dimension, Item, KeyedItem, SeekTarget, SumTree, Summary}; +use crate::{Bias, Dimension, Edit, Item, KeyedItem, SeekTarget, SumTree, Summary}; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct TreeMap(SumTree>) where K: Clone + Debug + Default + Ord, V: Clone + Debug; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct MapEntry { key: K, value: V, @@ -73,6 +73,17 @@ impl TreeMap { removed } + pub fn remove_range(&mut self, start: &impl MapSeekTarget, end: &impl MapSeekTarget) { + let start = MapSeekTargetAdaptor(start); + let end = MapSeekTargetAdaptor(end); + let mut cursor = self.0.cursor::>(); + let mut new_tree = cursor.slice(&start, Bias::Left, &()); + cursor.seek(&end, Bias::Left, &()); + new_tree.push_tree(cursor.suffix(&()), &()); + drop(cursor); + self.0 = new_tree; + } + /// Returns the key-value pair with the greatest key less than or equal to the given key. pub fn closest(&self, key: &K) -> Option<(&K, &V)> { let mut cursor = self.0.cursor::>(); @@ -82,6 +93,16 @@ impl TreeMap { cursor.item().map(|item| (&item.key, &item.value)) } + pub fn iter_from<'a>(&'a self, from: &'a K) -> impl Iterator + '_ { + let mut cursor = self.0.cursor::>(); + let from_key = MapKeyRef(Some(from)); + cursor.seek(&from_key, Bias::Left, &()); + + cursor + .into_iter() + .map(|map_entry| (&map_entry.key, &map_entry.value)) + } + pub fn update(&mut self, key: &K, f: F) -> Option where F: FnOnce(&mut V) -> T, @@ -125,6 +146,45 @@ impl TreeMap { pub fn values(&self) -> impl Iterator + '_ { self.0.iter().map(|entry| &entry.value) } + + pub fn insert_tree(&mut self, other: TreeMap) { + let edits = other + .iter() + .map(|(key, value)| { + Edit::Insert(MapEntry { + key: key.to_owned(), + value: value.to_owned(), + }) + }) + .collect(); + + self.0.edit(edits, &()); + } +} + +#[derive(Debug)] +struct MapSeekTargetAdaptor<'a, T>(&'a T); + +impl<'a, K: Debug + Clone + Default + Ord, T: MapSeekTarget> + SeekTarget<'a, MapKey, MapKeyRef<'a, K>> for MapSeekTargetAdaptor<'_, T> +{ + fn cmp(&self, cursor_location: &MapKeyRef, _: &()) -> Ordering { + if let Some(key) = &cursor_location.0 { + MapSeekTarget::cmp_cursor(self.0, key) + } else { + Ordering::Greater + } + } +} + +pub trait MapSeekTarget: Debug { + fn cmp_cursor(&self, cursor_location: &K) -> Ordering; +} + +impl MapSeekTarget for K { + fn cmp_cursor(&self, cursor_location: &K) -> Ordering { + self.cmp(cursor_location) + } } impl Default for TreeMap @@ -186,7 +246,7 @@ where K: Clone + Debug + Default + Ord, { fn cmp(&self, cursor_location: &MapKeyRef, _: &()) -> Ordering { - self.0.cmp(&cursor_location.0) + Ord::cmp(&self.0, &cursor_location.0) } } @@ -272,4 +332,112 @@ mod tests { map.retain(|key, _| *key % 2 == 0); assert_eq!(map.iter().collect::>(), vec![(&4, &"d"), (&6, &"f")]); } + + #[test] + fn test_iter_from() { + let mut map = TreeMap::default(); + + map.insert("a", 1); + map.insert("b", 2); + map.insert("baa", 3); + map.insert("baaab", 4); + map.insert("c", 5); + + let result = map + .iter_from(&"ba") + .take_while(|(key, _)| key.starts_with(&"ba")) + .collect::>(); + + assert_eq!(result.len(), 2); + assert!(result.iter().find(|(k, _)| k == &&"baa").is_some()); + assert!(result.iter().find(|(k, _)| k == &&"baaab").is_some()); + + let result = map + .iter_from(&"c") + .take_while(|(key, _)| key.starts_with(&"c")) + .collect::>(); + + assert_eq!(result.len(), 1); + assert!(result.iter().find(|(k, _)| k == &&"c").is_some()); + } + + #[test] + fn test_insert_tree() { + let mut map = TreeMap::default(); + map.insert("a", 1); + map.insert("b", 2); + map.insert("c", 3); + + let mut other = TreeMap::default(); + other.insert("a", 2); + other.insert("b", 2); + other.insert("d", 4); + + map.insert_tree(other); + + assert_eq!(map.iter().count(), 4); + assert_eq!(map.get(&"a"), Some(&2)); + assert_eq!(map.get(&"b"), Some(&2)); + assert_eq!(map.get(&"c"), Some(&3)); + assert_eq!(map.get(&"d"), Some(&4)); + } + + #[test] + fn test_remove_between_and_path_successor() { + use std::path::{Path, PathBuf}; + + #[derive(Debug)] + pub struct PathDescendants<'a>(&'a Path); + + impl MapSeekTarget for PathDescendants<'_> { + fn cmp_cursor(&self, key: &PathBuf) -> Ordering { + if key.starts_with(&self.0) { + Ordering::Greater + } else { + self.0.cmp(key) + } + } + } + + let mut map = TreeMap::default(); + + map.insert(PathBuf::from("a"), 1); + map.insert(PathBuf::from("a/a"), 1); + map.insert(PathBuf::from("b"), 2); + map.insert(PathBuf::from("b/a/a"), 3); + map.insert(PathBuf::from("b/a/a/a/b"), 4); + map.insert(PathBuf::from("c"), 5); + map.insert(PathBuf::from("c/a"), 6); + + map.remove_range( + &PathBuf::from("b/a"), + &PathDescendants(&PathBuf::from("b/a")), + ); + + assert_eq!(map.get(&PathBuf::from("a")), Some(&1)); + assert_eq!(map.get(&PathBuf::from("a/a")), Some(&1)); + assert_eq!(map.get(&PathBuf::from("b")), Some(&2)); + assert_eq!(map.get(&PathBuf::from("b/a/a")), None); + assert_eq!(map.get(&PathBuf::from("b/a/a/a/b")), None); + assert_eq!(map.get(&PathBuf::from("c")), Some(&5)); + assert_eq!(map.get(&PathBuf::from("c/a")), Some(&6)); + + map.remove_range(&PathBuf::from("c"), &PathDescendants(&PathBuf::from("c"))); + + assert_eq!(map.get(&PathBuf::from("a")), Some(&1)); + assert_eq!(map.get(&PathBuf::from("a/a")), Some(&1)); + assert_eq!(map.get(&PathBuf::from("b")), Some(&2)); + assert_eq!(map.get(&PathBuf::from("c")), None); + assert_eq!(map.get(&PathBuf::from("c/a")), None); + + map.remove_range(&PathBuf::from("a"), &PathDescendants(&PathBuf::from("a"))); + + assert_eq!(map.get(&PathBuf::from("a")), None); + assert_eq!(map.get(&PathBuf::from("a/a")), None); + assert_eq!(map.get(&PathBuf::from("b")), Some(&2)); + + map.remove_range(&PathBuf::from("b"), &PathDescendants(&PathBuf::from("b"))); + + assert_eq!(map.get(&PathBuf::from("b")), None); + } } diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index 725b102c04..a2902234c5 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -15,6 +15,7 @@ settings = { path = "../settings" } db = { path = "../db" } theme = { path = "../theme" } util = { path = "../util" } + alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "a51dbe25d67e84d6ed4261e640d3954fbdd9be45" } procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false } smallvec.workspace = true @@ -27,6 +28,7 @@ dirs = "4.0.0" shellexpand = "2.1.0" libc = "0.2" anyhow.workspace = true +schemars.workspace = true thiserror.workspace = true lazy_static.workspace = true serde.workspace = true diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 25852875c3..9cf3f52924 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -31,8 +31,8 @@ use mappings::mouse::{ }; use procinfo::LocalProcessInfo; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{AlternateScroll, Settings, Shell, TerminalBlink}; use util::truncate_and_trailoff; use std::{ @@ -48,11 +48,12 @@ use std::{ use thiserror::Error; use gpui::{ + fonts, geometry::vector::{vec2f, Vector2F}, keymap_matcher::Keystroke, platform::{MouseButton, MouseMovedEvent, TouchPhase}, scene::{MouseDown, MouseDrag, MouseScrollWheel, MouseUp}, - ClipboardItem, Entity, ModelContext, Task, + AppContext, ClipboardItem, Entity, ModelContext, Task, }; use crate::mappings::{ @@ -114,6 +115,125 @@ impl EventListener for ZedListener { } } +pub fn init(cx: &mut AppContext) { + settings::register::(cx); +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +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) => *line_height, + } + } +} + +#[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, @@ -599,7 +719,7 @@ impl Terminal { match event { InternalEvent::ColorRequest(index, format) => { let color = term.colors()[*index].unwrap_or_else(|| { - let term_style = &cx.global::().theme.terminal; + let term_style = &theme::current(cx).terminal; to_alac_rgb(get_color_at_index(index, &term_style)) }); self.write_to_pty(format(color)) @@ -1049,16 +1169,7 @@ impl Terminal { } pub fn mouse_up(&mut self, e: &MouseUp, origin: Vector2F, cx: &mut ModelContext) { - let settings = cx.global::(); - let copy_on_select = settings - .terminal_overrides - .copy_on_select - .unwrap_or_else(|| { - settings - .terminal_defaults - .copy_on_select - .expect("Should be set in defaults") - }); + let setting = settings::get::(cx); let position = e.position.sub(origin); if self.mouse_mode(e.shift) { @@ -1072,7 +1183,7 @@ impl Terminal { self.pty_tx.notify(bytes); } } else { - if e.button == MouseButton::Left && copy_on_select { + if e.button == MouseButton::Left && setting.copy_on_select { self.copy(); } diff --git a/crates/terminal_view/Cargo.toml b/crates/terminal_view/Cargo.toml index 3a25317870..a42d6c550e 100644 --- a/crates/terminal_view/Cargo.toml +++ b/crates/terminal_view/Cargo.toml @@ -39,6 +39,7 @@ serde_derive.workspace = true [dev-dependencies] +editor = { path = "../editor", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } client = { path = "../client", features = ["test-support"]} project = { path = "../project", features = ["test-support"]} diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index ae2342cd97..18c85db980 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -16,7 +16,6 @@ use gpui::{ use itertools::Itertools; use language::CursorShape; use ordered_float::OrderedFloat; -use settings::Settings; use terminal::{ alacritty_terminal::{ ansi::{Color as AnsiColor, Color::Named, CursorShape as AlacCursorShape, NamedColor}, @@ -25,9 +24,9 @@ use terminal::{ term::{cell::Flags, TermMode}, }, mappings::colors::convert_color, - IndexedCell, Terminal, TerminalContent, TerminalSize, + IndexedCell, Terminal, TerminalContent, TerminalSettings, TerminalSize, }; -use theme::TerminalStyle; +use theme::{TerminalStyle, ThemeSettings}; use util::ResultExt; use std::{fmt::Debug, ops::RangeInclusive}; @@ -510,47 +509,6 @@ impl TerminalElement { scene.push_mouse_region(region); } - - ///Configures a text style from the current settings. - pub fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle { - let font_family_name = settings - .terminal_overrides - .font_family - .as_ref() - .or(settings.terminal_defaults.font_family.as_ref()) - .unwrap_or(&settings.buffer_font_family_name); - let font_features = settings - .terminal_overrides - .font_features - .as_ref() - .or(settings.terminal_defaults.font_features.as_ref()) - .unwrap_or(&settings.buffer_font_features); - - let family_id = font_cache - .load_family(&[font_family_name], &font_features) - .log_err() - .unwrap_or(settings.buffer_font_family); - - let font_size = settings - .terminal_overrides - .font_size - .or(settings.terminal_defaults.font_size) - .unwrap_or(settings.buffer_font_size); - - let font_id = font_cache - .select_font(family_id, &Default::default()) - .unwrap(); - - TextStyle { - color: settings.theme.editor.text_color, - font_family_id: family_id, - font_family_name: font_cache.family_name(family_id).unwrap(), - font_id, - font_size, - font_properties: Default::default(), - underline: Default::default(), - } - } } impl Element for TerminalElement { @@ -563,20 +521,48 @@ impl Element for TerminalElement { view: &mut TerminalView, cx: &mut LayoutContext, ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) { - let settings = cx.global::(); - let font_cache = cx.font_cache(); + let settings = settings::get::(cx); + let terminal_settings = settings::get::(cx); //Setup layout information let terminal_theme = settings.theme.terminal.clone(); //TODO: Try to minimize this clone. let link_style = settings.theme.editor.link_definition; let tooltip_style = settings.theme.tooltip.clone(); - let text_style = TerminalElement::make_text_style(font_cache, settings); + let font_cache = cx.font_cache(); + let font_size = terminal_settings + .font_size(cx) + .unwrap_or(settings.buffer_font_size(cx)); + let font_family_name = terminal_settings + .font_family + .as_ref() + .unwrap_or(&settings.buffer_font_family_name); + let font_features = terminal_settings + .font_features + .as_ref() + .unwrap_or(&settings.buffer_font_features); + let family_id = font_cache + .load_family(&[font_family_name], &font_features) + .log_err() + .unwrap_or(settings.buffer_font_family); + let font_id = font_cache + .select_font(family_id, &Default::default()) + .unwrap(); + + let text_style = TextStyle { + color: settings.theme.editor.text_color, + font_family_id: family_id, + font_family_name: font_cache.family_name(family_id).unwrap(), + font_id, + font_size, + font_properties: Default::default(), + underline: Default::default(), + }; let selection_color = settings.theme.editor.selection.selection; let match_color = settings.theme.search.match_background; let gutter; let dimensions = { - let line_height = text_style.font_size * settings.terminal_line_height(); + let line_height = text_style.font_size * terminal_settings.line_height.value(); let cell_width = font_cache.em_advance(text_style.font_id, text_style.font_size); gutter = cell_width; diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 1f4096880c..afd564fb56 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -1,11 +1,15 @@ +use std::sync::Arc; + use crate::TerminalView; use db::kvp::KEY_VALUE_STORE; use gpui::{ actions, anyhow::Result, elements::*, serde_json, AppContext, AsyncAppContext, Entity, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, }; +use project::Fs; use serde::{Deserialize, Serialize}; -use settings::{settings_file::SettingsFile, Settings, TerminalDockPosition, WorkingDirectory}; +use settings::SettingsStore; +use terminal::{TerminalDockPosition, TerminalSettings}; use util::{ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel}, @@ -31,6 +35,7 @@ pub enum Event { pub struct TerminalPanel { pane: ViewHandle, + fs: Arc, workspace: WeakViewHandle, pending_serialization: Task>, _subscriptions: Vec, @@ -38,22 +43,13 @@ pub struct TerminalPanel { impl TerminalPanel { pub fn new(workspace: &Workspace, cx: &mut ViewContext) -> Self { - let mut old_dock_position = cx.global::().terminal_overrides.dock; - cx.observe_global::(move |_, cx| { - let new_dock_position = cx.global::().terminal_overrides.dock; - if new_dock_position != old_dock_position { - old_dock_position = new_dock_position; - cx.emit(Event::DockPositionChanged); - } - }) - .detach(); - - let this = cx.weak_handle(); + let weak_self = cx.weak_handle(); let pane = cx.add_view(|cx| { let window_id = cx.window_id(); let mut pane = Pane::new( workspace.weak_handle(), workspace.app_state().background_actions, + Default::default(), cx, ); pane.set_can_split(false, cx); @@ -65,7 +61,7 @@ impl TerminalPanel { }) }); pane.set_render_tab_bar_buttons(cx, move |_, cx| { - let this = this.clone(); + let this = weak_self.clone(); Pane::render_tab_bar_button( 0, "icons/plus_12.svg", @@ -89,12 +85,23 @@ impl TerminalPanel { cx.observe(&pane, |_, _, cx| cx.notify()), cx.subscribe(&pane, Self::handle_pane_event), ]; - Self { + let this = Self { pane, + fs: workspace.app_state().fs.clone(), workspace: workspace.weak_handle(), pending_serialization: Task::ready(None), _subscriptions: subscriptions, - } + }; + let mut old_dock_position = this.position(cx); + cx.observe_global::(move |this, cx| { + let new_dock_position = this.position(cx); + if new_dock_position != old_dock_position { + old_dock_position = new_dock_position; + cx.emit(Event::DockPositionChanged); + } + }) + .detach(); + this } pub fn load( @@ -187,12 +194,9 @@ impl TerminalPanel { cx.spawn(|this, mut cx| async move { let pane = this.read_with(&cx, |this, _| this.pane.clone())?; workspace.update(&mut cx, |workspace, cx| { - let working_directory_strategy = cx - .global::() - .terminal_overrides + let working_directory_strategy = settings::get::(cx) .working_directory - .clone() - .unwrap_or(WorkingDirectory::CurrentProjectDirectory); + .clone(); let working_directory = crate::get_working_directory(workspace, cx, working_directory_strategy); let window_id = cx.window_id(); @@ -262,17 +266,10 @@ impl View for TerminalPanel { impl Panel for TerminalPanel { fn position(&self, cx: &WindowContext) -> DockPosition { - let settings = cx.global::(); - let dock = settings - .terminal_overrides - .dock - .or(settings.terminal_defaults.dock) - .unwrap() - .into(); - match dock { - settings::TerminalDockPosition::Left => DockPosition::Left, - settings::TerminalDockPosition::Bottom => DockPosition::Bottom, - settings::TerminalDockPosition::Right => DockPosition::Right, + match settings::get::(cx).dock { + TerminalDockPosition::Left => DockPosition::Left, + TerminalDockPosition::Bottom => DockPosition::Bottom, + TerminalDockPosition::Right => DockPosition::Right, } } @@ -281,21 +278,21 @@ impl Panel for TerminalPanel { } fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { - SettingsFile::update(cx, move |settings| { + settings::update_settings_file::(self.fs.clone(), cx, move |settings| { let dock = match position { DockPosition::Left => TerminalDockPosition::Left, DockPosition::Bottom => TerminalDockPosition::Bottom, DockPosition::Right => TerminalDockPosition::Right, }; - settings.terminal.dock = Some(dock); + settings.dock = Some(dock); }); } fn default_size(&self, cx: &WindowContext) -> f32 { - let settings = &cx.global::().terminal_overrides; + let settings = settings::get::(cx); match self.position(cx) { - DockPosition::Left | DockPosition::Right => settings.default_width.unwrap_or(640.), - DockPosition::Bottom => settings.default_height.unwrap_or(320.), + DockPosition::Left | DockPosition::Right => settings.default_width, + DockPosition::Bottom => settings.default_height, } } diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 2c33cc8f39..767e3bf4db 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -2,6 +2,7 @@ mod persistence; pub mod terminal_element; pub mod terminal_panel; +use crate::{persistence::TERMINAL_DB, terminal_element::TerminalElement}; use context_menu::{ContextMenu, ContextMenuItem}; use dirs::home_dir; use gpui::{ @@ -16,7 +17,6 @@ use gpui::{ }; use project::{LocalWorktree, Project}; use serde::Deserialize; -use settings::{Settings, TerminalBlink, WorkingDirectory}; use smallvec::{smallvec, SmallVec}; use smol::Timer; use std::{ @@ -30,7 +30,7 @@ use terminal::{ index::Point, term::{search::RegexSearch, TermMode}, }, - Event, Terminal, + Event, Terminal, TerminalBlink, WorkingDirectory, }; use util::ResultExt; use workspace::{ @@ -41,7 +41,7 @@ use workspace::{ Pane, ToolbarItemLocation, Workspace, WorkspaceId, }; -use crate::{persistence::TERMINAL_DB, terminal_element::TerminalElement}; +pub use terminal::TerminalSettings; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); @@ -64,6 +64,8 @@ impl_actions!(terminal, [SendText, SendKeystroke]); pub fn init(cx: &mut AppContext) { terminal_panel::init(cx); + terminal::init(cx); + cx.add_action(TerminalView::deploy); register_deserializable_item::(cx); @@ -102,9 +104,9 @@ impl TerminalView { _: &workspace::NewTerminal, cx: &mut ViewContext, ) { - let strategy = cx.global::().terminal_strategy(); - - let working_directory = get_working_directory(workspace, cx, strategy); + let strategy = settings::get::(cx); + let working_directory = + get_working_directory(workspace, cx, strategy.working_directory.clone()); let window_id = cx.window_id(); let terminal = workspace @@ -216,10 +218,7 @@ impl TerminalView { self.terminal.update(cx, |term, cx| { term.try_keystroke( &Keystroke::parse("ctrl-cmd-space").unwrap(), - cx.global::() - .terminal_overrides - .option_as_meta - .unwrap_or(false), + settings::get::(cx).option_as_meta, ) }); } @@ -245,16 +244,7 @@ impl TerminalView { return true; } - let setting = { - let settings = cx.global::(); - settings - .terminal_overrides - .blinking - .clone() - .unwrap_or(TerminalBlink::TerminalControlled) - }; - - match setting { + match settings::get::(cx).blinking { //If the user requested to never blink, don't blink it. TerminalBlink::Off => true, //If the terminal is controlling it, check terminal mode @@ -347,10 +337,7 @@ impl TerminalView { self.terminal.update(cx, |term, cx| { term.try_keystroke( &keystroke, - cx.global::() - .terminal_overrides - .option_as_meta - .unwrap_or(false), + settings::get::(cx).option_as_meta, ); }); } @@ -413,10 +400,7 @@ impl View for TerminalView { self.terminal.update(cx, |term, cx| { term.try_keystroke( &event.keystroke, - cx.global::() - .terminal_overrides - .option_as_meta - .unwrap_or(false), + settings::get::(cx).option_as_meta, ) }) } @@ -618,7 +602,9 @@ impl Item for TerminalView { .flatten() .or_else(|| { cx.read(|cx| { - let strategy = cx.global::().terminal_strategy(); + let strategy = settings::get::(cx) + .working_directory + .clone(); workspace .upgrade(cx) .map(|workspace| { @@ -802,22 +788,18 @@ fn get_path_from_wt(wt: &LocalWorktree) -> Option { #[cfg(test)] mod tests { - use super::*; use gpui::TestAppContext; use project::{Entry, Project, ProjectPath, Worktree}; + use std::path::Path; use workspace::AppState; - use std::path::Path; + // Working directory calculation tests - ///Working directory calculation tests - - ///No Worktrees in project -> home_dir() + // No Worktrees in project -> home_dir() #[gpui::test] async fn no_worktree(cx: &mut TestAppContext) { - //Setup variables - let (project, workspace) = blank_workspace(cx).await; - //Test + let (project, workspace) = init_test(cx).await; cx.read(|cx| { let workspace = workspace.read(cx); let active_entry = project.read(cx).active_entry(); @@ -833,14 +815,12 @@ mod tests { }); } - ///No active entry, but a worktree, worktree is a file -> home_dir() + // No active entry, but a worktree, worktree is a file -> home_dir() #[gpui::test] async fn no_active_entry_worktree_is_file(cx: &mut TestAppContext) { - //Setup variables + let (project, workspace) = init_test(cx).await; - let (project, workspace) = blank_workspace(cx).await; create_file_wt(project.clone(), "/root.txt", cx).await; - cx.read(|cx| { let workspace = workspace.read(cx); let active_entry = project.read(cx).active_entry(); @@ -856,14 +836,12 @@ mod tests { }); } - //No active entry, but a worktree, worktree is a folder -> worktree_folder + // No active entry, but a worktree, worktree is a folder -> worktree_folder #[gpui::test] async fn no_active_entry_worktree_is_dir(cx: &mut TestAppContext) { - //Setup variables - let (project, workspace) = blank_workspace(cx).await; - let (_wt, _entry) = create_folder_wt(project.clone(), "/root/", cx).await; + let (project, workspace) = init_test(cx).await; - //Test + let (_wt, _entry) = create_folder_wt(project.clone(), "/root/", cx).await; cx.update(|cx| { let workspace = workspace.read(cx); let active_entry = project.read(cx).active_entry(); @@ -878,17 +856,15 @@ mod tests { }); } - //Active entry with a work tree, worktree is a file -> home_dir() + // Active entry with a work tree, worktree is a file -> home_dir() #[gpui::test] async fn active_entry_worktree_is_file(cx: &mut TestAppContext) { - //Setup variables + let (project, workspace) = init_test(cx).await; - let (project, workspace) = blank_workspace(cx).await; let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await; let (wt2, entry2) = create_file_wt(project.clone(), "/root2.txt", cx).await; insert_active_entry_for(wt2, entry2, project.clone(), cx); - //Test cx.update(|cx| { let workspace = workspace.read(cx); let active_entry = project.read(cx).active_entry(); @@ -902,16 +878,15 @@ mod tests { }); } - //Active entry, with a worktree, worktree is a folder -> worktree_folder + // Active entry, with a worktree, worktree is a folder -> worktree_folder #[gpui::test] async fn active_entry_worktree_is_dir(cx: &mut TestAppContext) { - //Setup variables - let (project, workspace) = blank_workspace(cx).await; + let (project, workspace) = init_test(cx).await; + let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await; let (wt2, entry2) = create_folder_wt(project.clone(), "/root2/", cx).await; insert_active_entry_for(wt2, entry2, project.clone(), cx); - //Test cx.update(|cx| { let workspace = workspace.read(cx); let active_entry = project.read(cx).active_entry(); @@ -925,11 +900,12 @@ mod tests { }); } - ///Creates a worktree with 1 file: /root.txt - pub async fn blank_workspace( + /// Creates a worktree with 1 file: /root.txt + pub async fn init_test( cx: &mut TestAppContext, ) -> (ModelHandle, ViewHandle) { let params = cx.update(AppState::test); + cx.update(|cx| theme::init((), cx)); let project = Project::test(params.fs.clone(), [], cx).await; let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); @@ -937,7 +913,7 @@ mod tests { (project, workspace) } - ///Creates a worktree with 1 folder: /root{suffix}/ + /// Creates a worktree with 1 folder: /root{suffix}/ async fn create_folder_wt( project: ModelHandle, path: impl AsRef, @@ -946,7 +922,7 @@ mod tests { create_wt(project, true, path, cx).await } - ///Creates a worktree with 1 file: /root{suffix}.txt + /// Creates a worktree with 1 file: /root{suffix}.txt async fn create_file_wt( project: ModelHandle, path: impl AsRef, diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 86bb7f4a26..dcfaf818d1 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -1783,6 +1783,19 @@ impl BufferSnapshot { where D: 'a + TextDimension, A: 'a + IntoIterator, + { + let anchors = anchors.into_iter(); + self.summaries_for_anchors_with_payload::(anchors.map(|a| (a, ()))) + .map(|d| d.0) + } + + pub fn summaries_for_anchors_with_payload<'a, D, A, T>( + &'a self, + anchors: A, + ) -> impl 'a + Iterator + where + D: 'a + TextDimension, + A: 'a + IntoIterator, { let anchors = anchors.into_iter(); let mut insertion_cursor = self.insertions.cursor::(); @@ -1790,11 +1803,11 @@ impl BufferSnapshot { let mut text_cursor = self.visible_text.cursor(0); let mut position = D::default(); - anchors.map(move |anchor| { + anchors.map(move |(anchor, payload)| { if *anchor == Anchor::MIN { - return D::default(); + return (D::default(), payload); } else if *anchor == Anchor::MAX { - return D::from_text_summary(&self.visible_text.summary()); + return (D::from_text_summary(&self.visible_text.summary()), payload); } let anchor_key = InsertionFragmentKey { @@ -1825,7 +1838,7 @@ impl BufferSnapshot { } position.add_assign(&text_cursor.summary(fragment_offset)); - position.clone() + (position.clone(), payload) }) } diff --git a/crates/theme/Cargo.toml b/crates/theme/Cargo.toml index 67a28397e2..b213cc9c1e 100644 --- a/crates/theme/Cargo.toml +++ b/crates/theme/Cargo.toml @@ -4,16 +4,33 @@ version = "0.1.0" edition = "2021" publish = false +[features] +test-support = [ + "gpui/test-support", + "fs/test-support", + "settings/test-support" +] + [lib] path = "src/theme.rs" doctest = false [dependencies] gpui = { path = "../gpui" } +fs = { path = "../fs" } +settings = { path = "../settings" } +util = { path = "../util" } + anyhow.workspace = true indexmap = "1.6.2" parking_lot.workspace = true +schemars.workspace = true serde.workspace = true serde_derive.workspace = true serde_json.workspace = true -toml = "0.5" +toml.workspace = true + +[dev-dependencies] +gpui = { path = "../gpui", features = ["test-support"] } +fs = { path = "../fs", features = ["test-support"] } +settings = { path = "../settings", features = ["test-support"] } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 50f4607d66..a62694ea35 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -1,19 +1,40 @@ mod theme_registry; +mod theme_settings; +pub mod ui; use gpui::{ color::Color, elements::{ContainerStyle, ImageStyle, LabelStyle, Shadow, TooltipStyle}, fonts::{HighlightStyle, TextStyle}, - platform, Border, MouseState, + platform, AppContext, AssetSource, Border, MouseState, }; use serde::{de::DeserializeOwned, Deserialize}; use serde_json::Value; +use settings::SettingsStore; use std::{collections::HashMap, sync::Arc}; use ui::{ButtonStyle, CheckboxStyle, IconStyle, ModalStyle, SvgStyle}; -pub mod ui; - pub use theme_registry::*; +pub use theme_settings::*; + +pub fn current(cx: &AppContext) -> Arc { + settings::get::(cx).theme.clone() +} + +pub fn init(source: impl AssetSource, cx: &mut AppContext) { + cx.set_global(ThemeRegistry::new(source, cx.font_cache().clone())); + settings::register::(cx); + + let mut prev_buffer_font_size = settings::get::(cx).buffer_font_size; + cx.observe_global::(move |cx| { + let buffer_font_size = settings::get::(cx).buffer_font_size; + if buffer_font_size != prev_buffer_font_size { + prev_buffer_font_size = buffer_font_size; + reset_font_size(cx); + } + }) + .detach(); +} #[derive(Deserialize, Default)] pub struct Theme { diff --git a/crates/theme/src/theme_registry.rs b/crates/theme/src/theme_registry.rs index f9f89b7adc..8565bc3b56 100644 --- a/crates/theme/src/theme_registry.rs +++ b/crates/theme/src/theme_registry.rs @@ -22,13 +22,25 @@ pub struct ThemeRegistry { impl ThemeRegistry { pub fn new(source: impl AssetSource, font_cache: Arc) -> Arc { - Arc::new(Self { + let this = Arc::new(Self { assets: Box::new(source), themes: Default::default(), theme_data: Default::default(), next_theme_id: Default::default(), font_cache, - }) + }); + + this.themes.lock().insert( + settings::EMPTY_THEME_NAME.to_string(), + gpui::fonts::with_font_cache(this.font_cache.clone(), || { + let mut theme = Theme::default(); + theme.meta.id = this.next_theme_id.fetch_add(1, SeqCst); + theme.meta.name = settings::EMPTY_THEME_NAME.into(); + Arc::new(theme) + }), + ); + + this } pub fn list(&self, staff: bool) -> impl Iterator + '_ { diff --git a/crates/theme/src/theme_settings.rs b/crates/theme/src/theme_settings.rs new file mode 100644 index 0000000000..f86d3fd8dd --- /dev/null +++ b/crates/theme/src/theme_settings.rs @@ -0,0 +1,184 @@ +use crate::{Theme, ThemeRegistry}; +use anyhow::Result; +use gpui::{font_cache::FamilyId, fonts, AppContext}; +use schemars::{ + gen::SchemaGenerator, + schema::{InstanceType, Schema, SchemaObject}, + JsonSchema, +}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use settings::SettingsJsonSchemaParams; +use std::sync::Arc; +use util::ResultExt as _; + +const MIN_FONT_SIZE: f32 = 6.0; + +#[derive(Clone)] +pub struct ThemeSettings { + pub buffer_font_family_name: String, + pub buffer_font_features: fonts::Features, + pub buffer_font_family: FamilyId, + pub(crate) buffer_font_size: f32, + pub theme: Arc, +} + +pub struct AdjustedBufferFontSize(pub f32); + +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +pub struct ThemeSettingsContent { + #[serde(default)] + pub buffer_font_family: Option, + #[serde(default)] + pub buffer_font_size: Option, + #[serde(default)] + pub buffer_font_features: Option, + #[serde(default)] + pub theme: Option, +} + +impl ThemeSettings { + pub fn buffer_font_size(&self, cx: &AppContext) -> f32 { + if cx.has_global::() { + cx.global::().0 + } else { + self.buffer_font_size + } + .max(MIN_FONT_SIZE) + } +} + +pub fn adjusted_font_size(size: f32, cx: &AppContext) -> f32 { + if cx.has_global::() { + let buffer_font_size = settings::get::(cx).buffer_font_size; + let delta = cx.global::().0 - buffer_font_size; + size + delta + } else { + size + } + .max(MIN_FONT_SIZE) +} + +pub fn adjust_font_size(cx: &mut AppContext, f: fn(&mut f32)) { + if !cx.has_global::() { + let buffer_font_size = settings::get::(cx).buffer_font_size; + cx.set_global(AdjustedBufferFontSize(buffer_font_size)); + } + + cx.update_global::(|delta, cx| { + f(&mut delta.0); + delta.0 = delta + .0 + .max(MIN_FONT_SIZE - settings::get::(cx).buffer_font_size); + }); + cx.refresh_windows(); +} + +pub fn reset_font_size(cx: &mut AppContext) { + if cx.has_global::() { + cx.remove_global::(); + cx.refresh_windows(); + } +} + +impl settings::Setting for ThemeSettings { + const KEY: Option<&'static str> = None; + + type FileContent = ThemeSettingsContent; + + fn load( + defaults: &Self::FileContent, + user_values: &[&Self::FileContent], + cx: &AppContext, + ) -> Result { + let buffer_font_features = defaults.buffer_font_features.clone().unwrap(); + let themes = cx.global::>(); + + let mut this = Self { + buffer_font_family: cx + .font_cache() + .load_family( + &[defaults.buffer_font_family.as_ref().unwrap()], + &buffer_font_features, + ) + .unwrap(), + buffer_font_family_name: defaults.buffer_font_family.clone().unwrap(), + buffer_font_features, + buffer_font_size: defaults.buffer_font_size.unwrap(), + theme: themes.get(defaults.theme.as_ref().unwrap()).unwrap(), + }; + + for value in user_values.into_iter().copied().cloned() { + let font_cache = cx.font_cache(); + let mut family_changed = false; + if let Some(value) = value.buffer_font_family { + this.buffer_font_family_name = value; + family_changed = true; + } + if let Some(value) = value.buffer_font_features { + this.buffer_font_features = value; + family_changed = true; + } + if family_changed { + if let Some(id) = font_cache + .load_family(&[&this.buffer_font_family_name], &this.buffer_font_features) + .log_err() + { + this.buffer_font_family = id; + } + } + + if let Some(value) = &value.theme { + if let Some(theme) = themes.get(value).log_err() { + this.theme = theme; + } + } + + merge(&mut this.buffer_font_size, value.buffer_font_size); + } + + Ok(this) + } + + fn json_schema( + generator: &mut SchemaGenerator, + params: &SettingsJsonSchemaParams, + cx: &AppContext, + ) -> schemars::schema::RootSchema { + let mut root_schema = generator.root_schema_for::(); + let theme_names = cx + .global::>() + .list(params.staff_mode) + .map(|theme| Value::String(theme.name.clone())) + .collect(); + + let theme_name_schema = SchemaObject { + instance_type: Some(InstanceType::String.into()), + enum_values: Some(theme_names), + ..Default::default() + }; + + root_schema + .definitions + .extend([("ThemeName".into(), theme_name_schema.into())]); + + root_schema + .schema + .object + .as_mut() + .unwrap() + .properties + .extend([( + "theme".to_owned(), + Schema::new_ref("#/definitions/ThemeName".into()), + )]); + + root_schema + } +} + +fn merge(target: &mut T, value: Option) { + if let Some(value) = value { + *target = value; + } +} diff --git a/crates/theme/src/ui.rs b/crates/theme/src/ui.rs index b86bfca8c4..e4df24c89f 100644 --- a/crates/theme/src/ui.rs +++ b/crates/theme/src/ui.rs @@ -1,9 +1,10 @@ use std::borrow::Cow; +use fs::repository::GitFileStatus; use gpui::{ color::Color, elements::{ - ConstrainedBox, Container, ContainerStyle, Empty, Flex, KeystrokeLabel, Label, + ConstrainedBox, Container, ContainerStyle, Empty, Flex, KeystrokeLabel, Label, LabelStyle, MouseEventHandler, ParentElement, Stack, Svg, }, fonts::TextStyle, @@ -11,11 +12,11 @@ use gpui::{ platform, platform::MouseButton, scene::MouseClick, - Action, Element, EventContext, MouseState, View, ViewContext, + Action, AnyElement, Element, EventContext, MouseState, View, ViewContext, }; use serde::Deserialize; -use crate::{ContainedText, Interactive}; +use crate::{ContainedText, Interactive, Theme}; #[derive(Clone, Deserialize, Default)] pub struct CheckboxStyle { @@ -252,3 +253,53 @@ where .constrained() .with_height(style.dimensions().y()) } + +pub struct FileName { + filename: String, + git_status: Option, + style: FileNameStyle, +} + +pub struct FileNameStyle { + template_style: LabelStyle, + git_inserted: Color, + git_modified: Color, + git_deleted: Color, +} + +impl FileName { + pub fn new(filename: String, git_status: Option, style: FileNameStyle) -> Self { + FileName { + filename, + git_status, + style, + } + } + + pub fn style>(style: I, theme: &Theme) -> FileNameStyle { + FileNameStyle { + template_style: style.into(), + git_inserted: theme.editor.diff.inserted, + git_modified: theme.editor.diff.modified, + git_deleted: theme.editor.diff.deleted, + } + } +} + +impl gpui::elements::Component for FileName { + fn render(&self, _: &mut V, _: &mut ViewContext) -> AnyElement { + // Prepare colors for git statuses + let mut filename_text_style = self.style.template_style.text.clone(); + filename_text_style.color = self + .git_status + .as_ref() + .map(|status| match status { + GitFileStatus::Added => self.style.git_inserted, + GitFileStatus::Modified => self.style.git_modified, + GitFileStatus::Conflict => self.style.git_deleted, + }) + .unwrap_or(self.style.template_style.text.color); + + Label::new(self.filename.clone(), filename_text_style).into_any() + } +} diff --git a/crates/theme_selector/Cargo.toml b/crates/theme_selector/Cargo.toml index a404e43f29..377f64aad6 100644 --- a/crates/theme_selector/Cargo.toml +++ b/crates/theme_selector/Cargo.toml @@ -11,6 +11,7 @@ doctest = false [dependencies] editor = { path = "../editor" } fuzzy = { path = "../fuzzy" } +fs = { path = "../fs" } gpui = { path = "../gpui" } picker = { path = "../picker" } theme = { path = "../theme" } @@ -22,3 +23,6 @@ log.workspace = true parking_lot.workspace = true postage.workspace = true smol.workspace = true + +[dev-dependencies] +editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index 21332114e2..a6c84d1d91 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -1,10 +1,11 @@ +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::{settings_file::SettingsFile, Settings}; +use settings::{update_settings_file, SettingsStore}; use staff_mode::StaffMode; use std::sync::Arc; -use theme::{Theme, ThemeMeta, ThemeRegistry}; +use theme::{Theme, ThemeMeta, ThemeRegistry, ThemeSettings}; use util::ResultExt; use workspace::Workspace; @@ -17,16 +18,17 @@ pub fn init(cx: &mut AppContext) { pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { workspace.toggle_modal(cx, |workspace, cx| { - let themes = workspace.app_state().themes.clone(); - cx.add_view(|cx| ThemeSelector::new(ThemeSelectorDelegate::new(themes, cx), cx)) + let fs = workspace.app_state().fs.clone(); + cx.add_view(|cx| ThemeSelector::new(ThemeSelectorDelegate::new(fs, cx), cx)) }); } #[cfg(debug_assertions)] -pub fn reload(themes: Arc, cx: &mut AppContext) { - let current_theme_name = cx.global::().theme.meta.name.clone(); - themes.clear(); - match themes.get(¤t_theme_name) { +pub fn reload(cx: &mut AppContext) { + let current_theme_name = theme::current(cx).meta.name.clone(); + let registry = cx.global::>(); + registry.clear(); + match registry.get(¤t_theme_name) { Ok(theme) => { ThemeSelectorDelegate::set_theme(theme, cx); log::info!("reloaded theme {}", current_theme_name); @@ -40,7 +42,7 @@ pub fn reload(themes: Arc, cx: &mut AppContext) { pub type ThemeSelector = Picker; pub struct ThemeSelectorDelegate { - registry: Arc, + fs: Arc, theme_data: Vec, matches: Vec, original_theme: Arc, @@ -49,14 +51,12 @@ pub struct ThemeSelectorDelegate { } impl ThemeSelectorDelegate { - fn new(registry: Arc, cx: &mut ViewContext) -> Self { - let settings = cx.global::(); + fn new(fs: Arc, cx: &mut ViewContext) -> Self { + let original_theme = theme::current(cx).clone(); - let original_theme = settings.theme.clone(); - - let mut theme_names = registry - .list(**cx.default_global::()) - .collect::>(); + let staff_mode = **cx.default_global::(); + 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))); let matches = theme_names .iter() @@ -68,7 +68,7 @@ impl ThemeSelectorDelegate { }) .collect(); let mut this = Self { - registry, + fs, theme_data: theme_names, matches, original_theme: original_theme.clone(), @@ -81,7 +81,8 @@ impl ThemeSelectorDelegate { fn show_selected_theme(&mut self, cx: &mut ViewContext) { if let Some(mat) = self.matches.get(self.selected_index) { - match self.registry.get(&mat.string) { + let registry = cx.global::>(); + match registry.get(&mat.string) { Ok(theme) => { Self::set_theme(theme, cx); } @@ -101,8 +102,10 @@ impl ThemeSelectorDelegate { } fn set_theme(theme: Arc, cx: &mut AppContext) { - cx.update_global::(|settings, cx| { - settings.theme = theme; + cx.update_global::(|store, cx| { + let mut theme_settings = store.get::(None).clone(); + theme_settings.theme = theme; + store.override_global(theme_settings); cx.refresh_windows(); }); } @@ -120,9 +123,9 @@ impl PickerDelegate for ThemeSelectorDelegate { fn confirm(&mut self, cx: &mut ViewContext) { self.selection_completed = true; - let theme_name = cx.global::().theme.meta.name.clone(); - SettingsFile::update(cx, |settings_content| { - settings_content.theme = Some(theme_name); + let theme_name = theme::current(cx).meta.name.clone(); + update_settings_file::(self.fs.clone(), cx, |settings| { + settings.theme = Some(theme_name); }); cx.emit(PickerEvent::Dismiss); @@ -204,11 +207,10 @@ impl PickerDelegate for ThemeSelectorDelegate { selected: bool, cx: &AppContext, ) -> AnyElement> { - let settings = cx.global::(); - let theme = &settings.theme; - let theme_match = &self.matches[ix]; + let theme = theme::current(cx); let style = theme.picker.item.style_for(mouse_state, selected); + let theme_match = &self.matches[ix]; Label::new(theme_match.string.clone(), style.label.clone()) .with_highlights(theme_match.positions.clone()) .contained() diff --git a/crates/theme_testbench/src/theme_testbench.rs b/crates/theme_testbench/src/theme_testbench.rs index c18a580d07..258249b599 100644 --- a/crates/theme_testbench/src/theme_testbench.rs +++ b/crates/theme_testbench/src/theme_testbench.rs @@ -10,8 +10,7 @@ use gpui::{ WeakViewHandle, }; use project::Project; -use settings::Settings; -use theme::{ColorScheme, Layer, Style, StyleSet}; +use theme::{ColorScheme, Layer, Style, StyleSet, ThemeSettings}; use workspace::{item::Item, register_deserializable_item, Pane, Workspace}; actions!(theme, [DeployThemeTestbench]); @@ -220,10 +219,10 @@ impl ThemeTestbench { } fn render_label(text: String, style: &Style, cx: &mut ViewContext) -> Label { - let settings = cx.global::(); + let settings = settings::get::(cx); let font_cache = cx.font_cache(); let family_id = settings.buffer_font_family; - let font_size = settings.buffer_font_size; + let font_size = settings.buffer_font_size(cx); let font_id = font_cache .select_font(family_id, &Default::default()) .unwrap(); @@ -252,7 +251,7 @@ impl View for ThemeTestbench { } fn render(&mut self, cx: &mut gpui::ViewContext) -> AnyElement { - let color_scheme = &cx.global::().theme.clone().color_scheme; + let color_scheme = &theme::current(cx).clone().color_scheme; Flex::row() .with_child( diff --git a/crates/util/Cargo.toml b/crates/util/Cargo.toml index 319d815d17..4ec8f7553c 100644 --- a/crates/util/Cargo.toml +++ b/crates/util/Cargo.toml @@ -26,6 +26,7 @@ serde.workspace = true serde_json.workspace = true git2 = { version = "0.15", default-features = false, optional = true } dirs = "3.0" +take-until = "0.2.0" [dev-dependencies] tempdir.workspace = true diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index a324b21a31..f998fc319f 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -1,5 +1,7 @@ use std::path::{Path, PathBuf}; +use serde::{Deserialize, Serialize}; + lazy_static::lazy_static! { pub static ref HOME: PathBuf = dirs::home_dir().expect("failed to determine home directory"); pub static ref CONFIG_DIR: PathBuf = HOME.join(".config").join("zed"); @@ -70,3 +72,208 @@ pub fn compact(path: &Path) -> PathBuf { path.to_path_buf() } } + +/// A delimiter to use in `path_query:row_number:column_number` strings parsing. +pub const FILE_ROW_COLUMN_DELIMITER: char = ':'; + +/// A representation of a path-like string with optional row and column numbers. +/// Matching values example: `te`, `test.rs:22`, `te:22:5`, etc. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PathLikeWithPosition

{ + pub path_like: P, + pub row: Option, + // Absent if row is absent. + pub column: Option, +} + +impl

PathLikeWithPosition

{ + /// Parses a string that possibly has `:row:column` suffix. + /// Ignores trailing `:`s, so `test.rs:22:` is parsed as `test.rs:22`. + /// If any of the row/column component parsing fails, the whole string is then parsed as a path like. + pub fn parse_str( + s: &str, + parse_path_like_str: impl Fn(&str) -> Result, + ) -> Result { + let fallback = |fallback_str| { + Ok(Self { + path_like: parse_path_like_str(fallback_str)?, + row: None, + column: None, + }) + }; + + match s.trim().split_once(FILE_ROW_COLUMN_DELIMITER) { + Some((path_like_str, maybe_row_and_col_str)) => { + let path_like_str = path_like_str.trim(); + let maybe_row_and_col_str = maybe_row_and_col_str.trim(); + if path_like_str.is_empty() { + fallback(s) + } else if maybe_row_and_col_str.is_empty() { + fallback(path_like_str) + } else { + let (row_parse_result, maybe_col_str) = + match maybe_row_and_col_str.split_once(FILE_ROW_COLUMN_DELIMITER) { + Some((maybe_row_str, maybe_col_str)) => { + (maybe_row_str.parse::(), maybe_col_str.trim()) + } + None => (maybe_row_and_col_str.parse::(), ""), + }; + + match row_parse_result { + Ok(row) => { + if maybe_col_str.is_empty() { + Ok(Self { + path_like: parse_path_like_str(path_like_str)?, + row: Some(row), + column: None, + }) + } else { + match maybe_col_str.parse::() { + Ok(col) => Ok(Self { + path_like: parse_path_like_str(path_like_str)?, + row: Some(row), + column: Some(col), + }), + Err(_) => fallback(s), + } + } + } + Err(_) => fallback(s), + } + } + } + None => fallback(s), + } + } + + pub fn map_path_like( + self, + mapping: impl FnOnce(P) -> Result, + ) -> Result, E> { + Ok(PathLikeWithPosition { + path_like: mapping(self.path_like)?, + row: self.row, + column: self.column, + }) + } + + pub fn to_string(&self, path_like_to_string: impl Fn(&P) -> String) -> String { + let path_like_string = path_like_to_string(&self.path_like); + if let Some(row) = self.row { + if let Some(column) = self.column { + format!("{path_like_string}:{row}:{column}") + } else { + format!("{path_like_string}:{row}") + } + } else { + path_like_string + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + type TestPath = PathLikeWithPosition; + + fn parse_str(s: &str) -> TestPath { + TestPath::parse_str(s, |s| Ok::<_, std::convert::Infallible>(s.to_string())) + .expect("infallible") + } + + #[test] + fn path_with_position_parsing_positive() { + let input_and_expected = [ + ( + "test_file.rs", + PathLikeWithPosition { + path_like: "test_file.rs".to_string(), + row: None, + column: None, + }, + ), + ( + "test_file.rs:1", + PathLikeWithPosition { + path_like: "test_file.rs".to_string(), + row: Some(1), + column: None, + }, + ), + ( + "test_file.rs:1:2", + PathLikeWithPosition { + path_like: "test_file.rs".to_string(), + row: Some(1), + column: Some(2), + }, + ), + ]; + + for (input, expected) in input_and_expected { + let actual = parse_str(input); + assert_eq!( + actual, expected, + "For positive case input str '{input}', got a parse mismatch" + ); + } + } + + #[test] + fn path_with_position_parsing_negative() { + for input in [ + "test_file.rs:a", + "test_file.rs:a:b", + "test_file.rs::", + "test_file.rs::1", + "test_file.rs:1::", + "test_file.rs::1:2", + "test_file.rs:1::2", + "test_file.rs:1:2:", + "test_file.rs:1:2:3", + ] { + let actual = parse_str(input); + assert_eq!( + actual, + PathLikeWithPosition { + path_like: input.to_string(), + row: None, + column: None, + }, + "For negative case input str '{input}', got a parse mismatch" + ); + } + } + + // Trim off trailing `:`s for otherwise valid input. + #[test] + fn path_with_position_parsing_special() { + let input_and_expected = [ + ( + "test_file.rs:", + PathLikeWithPosition { + path_like: "test_file.rs".to_string(), + row: None, + column: None, + }, + ), + ( + "test_file.rs:1:", + PathLikeWithPosition { + path_like: "test_file.rs".to_string(), + row: Some(1), + column: None, + }, + ), + ]; + + for (input, expected) in input_and_expected { + let actual = parse_str(input); + assert_eq!( + actual, expected, + "For special case input str '{input}', got a parse mismatch" + ); + } + } +} diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 903b0eec59..9d787e1389 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -17,6 +17,8 @@ pub use backtrace::Backtrace; use futures::Future; use rand::{seq::SliceRandom, Rng}; +pub use take_until::*; + #[macro_export] macro_rules! debug_panic { ( $($fmt_arg:tt)* ) => { @@ -93,6 +95,27 @@ pub fn merge_json_value_into(source: serde_json::Value, target: &mut serde_json: } } +pub fn merge_non_null_json_value_into(source: serde_json::Value, target: &mut serde_json::Value) { + use serde_json::Value; + if let Value::Object(source_object) = source { + let target_object = if let Value::Object(target) = target { + target + } else { + *target = Value::Object(Default::default()); + target.as_object_mut().unwrap() + }; + for (key, value) in source_object { + if let Some(target) = target_object.get_mut(&key) { + merge_non_null_json_value_into(value, target); + } else if !value.is_null() { + target_object.insert(key.clone(), value); + } + } + } else if !source.is_null() { + *target = source + } +} + pub trait ResultExt { type Ok; diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index 5f7cf0a5a3..c34a5b469b 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -12,6 +12,7 @@ doctest = false neovim = ["nvim-rs", "async-compat", "async-trait", "tokio"] [dependencies] +anyhow.workspace = true serde.workspace = true serde_derive.workspace = true itertools = "0.10" diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index 69227a0e45..531fbf0bba 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -17,14 +17,17 @@ pub struct VimTestContext<'a> { impl<'a> VimTestContext<'a> { pub async fn new(cx: &'a mut gpui::TestAppContext, enabled: bool) -> VimTestContext<'a> { let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await; + cx.update(|cx| { - cx.update_global(|settings: &mut Settings, _| { - settings.vim_mode = enabled; - }); search::init(cx); crate::init(cx); + }); - settings::KeymapFileContent::load("keymaps/vim.json", cx).unwrap(); + cx.update(|cx| { + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |s| *s = Some(enabled)); + }); + settings::KeymapFileContent::load_asset("keymaps/vim.json", cx).unwrap(); }); // Setup search toolbars and keypress hook @@ -52,16 +55,16 @@ impl<'a> VimTestContext<'a> { pub fn enable_vim(&mut self) { self.cx.update(|cx| { - cx.update_global(|settings: &mut Settings, _| { - settings.vim_mode = true; + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |s| *s = Some(true)); }); }) } pub fn disable_vim(&mut self) { self.cx.update(|cx| { - cx.update_global(|settings: &mut Settings, _| { - settings.vim_mode = false; + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |s| *s = Some(false)); }); }) } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index cc686f851f..d10ec5e824 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -10,8 +10,7 @@ mod state; mod utils; mod visual; -use std::sync::Arc; - +use anyhow::Result; use collections::CommandPaletteFilter; use editor::{Bias, Cancel, Editor, EditorMode, Event}; use gpui::{ @@ -22,11 +21,14 @@ use language::CursorShape; use motion::Motion; use normal::normal_replace; use serde::Deserialize; -use settings::Settings; +use settings::{Setting, SettingsStore}; use state::{Mode, Operator, VimState}; +use std::sync::Arc; use visual::visual_replace; use workspace::{self, Workspace}; +struct VimModeSetting(bool); + #[derive(Clone, Deserialize, PartialEq)] pub struct SwitchMode(pub Mode); @@ -40,6 +42,8 @@ actions!(vim, [Tab, Enter]); impl_actions!(vim, [Number, SwitchMode, PushOperator]); pub fn init(cx: &mut AppContext) { + settings::register::(cx); + editor_events::init(cx); normal::init(cx); visual::init(cx); @@ -91,11 +95,11 @@ pub fn init(cx: &mut AppContext) { filter.filtered_namespaces.insert("vim"); }); cx.update_default_global(|vim: &mut Vim, cx: &mut AppContext| { - vim.set_enabled(cx.global::().vim_mode, cx) + vim.set_enabled(settings::get::(cx).0, cx) }); - cx.observe_global::(|cx| { + cx.observe_global::(|cx| { cx.update_default_global(|vim: &mut Vim, cx: &mut AppContext| { - vim.set_enabled(cx.global::().vim_mode, cx) + vim.set_enabled(settings::get::(cx).0, cx) }); }) .detach(); @@ -330,6 +334,22 @@ impl Vim { } } +impl Setting for VimModeSetting { + const KEY: Option<&'static str> = Some("vim_mode"); + + type FileContent = Option; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &AppContext, + ) -> Result { + Ok(Self(user_values.iter().rev().find_map(|v| **v).unwrap_or( + default_value.ok_or_else(Self::missing_default)?, + ))) + } +} + fn local_selections_changed(newest_empty: bool, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { if vim.enabled && vim.state.mode == Mode::Normal && !newest_empty { diff --git a/crates/welcome/Cargo.toml b/crates/welcome/Cargo.toml index d35cced642..ea01f822a7 100644 --- a/crates/welcome/Cargo.toml +++ b/crates/welcome/Cargo.toml @@ -11,9 +11,9 @@ path = "src/welcome.rs" test-support = [] [dependencies] -anyhow.workspace = true -log.workspace = true +client = { path = "../client" } editor = { path = "../editor" } +fs = { path = "../fs" } fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } db = { path = "../db" } @@ -25,3 +25,11 @@ theme_selector = { path = "../theme_selector" } util = { path = "../util" } picker = { path = "../picker" } workspace = { path = "../workspace" } + +anyhow.workspace = true +log.workspace = true +schemars.workspace = true +serde.workspace = true + +[dev-dependencies] +editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/welcome/src/base_keymap_picker.rs b/crates/welcome/src/base_keymap_picker.rs index 260c279e18..e44b391d84 100644 --- a/crates/welcome/src/base_keymap_picker.rs +++ b/crates/welcome/src/base_keymap_picker.rs @@ -1,5 +1,4 @@ -use std::sync::Arc; - +use super::base_keymap_setting::BaseKeymap; use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; use gpui::{ actions, @@ -7,7 +6,9 @@ use gpui::{ AppContext, Task, ViewContext, }; use picker::{Picker, PickerDelegate, PickerEvent}; -use settings::{settings_file::SettingsFile, BaseKeymap, Settings}; +use project::Fs; +use settings::update_settings_file; +use std::sync::Arc; use util::ResultExt; use workspace::Workspace; @@ -23,8 +24,9 @@ pub fn toggle( _: &ToggleBaseKeymapSelector, cx: &mut ViewContext, ) { - workspace.toggle_modal(cx, |_, cx| { - cx.add_view(|cx| BaseKeymapSelector::new(BaseKeymapSelectorDelegate::new(cx), cx)) + workspace.toggle_modal(cx, |workspace, cx| { + let fs = workspace.app_state().fs.clone(); + cx.add_view(|cx| BaseKeymapSelector::new(BaseKeymapSelectorDelegate::new(fs, cx), cx)) }); } @@ -33,18 +35,20 @@ pub type BaseKeymapSelector = Picker; pub struct BaseKeymapSelectorDelegate { matches: Vec, selected_index: usize, + fs: Arc, } impl BaseKeymapSelectorDelegate { - fn new(cx: &mut ViewContext) -> Self { - let base = cx.global::().base_keymap; + fn new(fs: Arc, cx: &mut ViewContext) -> Self { + let base = settings::get::(cx); let selected_index = BaseKeymap::OPTIONS .iter() - .position(|(_, value)| *value == base) + .position(|(_, value)| value == base) .unwrap_or(0); Self { matches: Vec::new(), selected_index, + fs, } } } @@ -119,7 +123,9 @@ impl PickerDelegate for BaseKeymapSelectorDelegate { fn confirm(&mut self, cx: &mut ViewContext) { if let Some(selection) = self.matches.get(self.selected_index) { let base_keymap = BaseKeymap::from_names(&selection.string); - SettingsFile::update(cx, move |settings| settings.base_keymap = Some(base_keymap)); + update_settings_file::(self.fs.clone(), cx, move |setting| { + *setting = Some(base_keymap) + }); } cx.emit(PickerEvent::Dismiss); } @@ -133,7 +139,7 @@ impl PickerDelegate for BaseKeymapSelectorDelegate { selected: bool, cx: &gpui::AppContext, ) -> gpui::AnyElement> { - let theme = &cx.global::().theme; + let theme = &theme::current(cx); let keymap_match = &self.matches[ix]; let style = theme.picker.item.style_for(mouse_state, selected); diff --git a/crates/welcome/src/base_keymap_setting.rs b/crates/welcome/src/base_keymap_setting.rs new file mode 100644 index 0000000000..c5b6171f9b --- /dev/null +++ b/crates/welcome/src/base_keymap_setting.rs @@ -0,0 +1,65 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::Setting; + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)] +pub enum BaseKeymap { + #[default] + VSCode, + JetBrains, + SublimeText, + Atom, + TextMate, +} + +impl BaseKeymap { + pub const OPTIONS: [(&'static str, Self); 5] = [ + ("VSCode (Default)", Self::VSCode), + ("Atom", Self::Atom), + ("JetBrains", Self::JetBrains), + ("Sublime Text", Self::SublimeText), + ("TextMate", Self::TextMate), + ]; + + pub fn asset_path(&self) -> Option<&'static str> { + match self { + BaseKeymap::JetBrains => Some("keymaps/jetbrains.json"), + BaseKeymap::SublimeText => Some("keymaps/sublime_text.json"), + BaseKeymap::Atom => Some("keymaps/atom.json"), + BaseKeymap::TextMate => Some("keymaps/textmate.json"), + BaseKeymap::VSCode => None, + } + } + + pub fn names() -> impl Iterator { + Self::OPTIONS.iter().map(|(name, _)| *name) + } + + pub fn from_names(option: &str) -> BaseKeymap { + Self::OPTIONS + .iter() + .copied() + .find_map(|(name, value)| (name == option).then(|| value)) + .unwrap_or_default() + } +} + +impl Setting for BaseKeymap { + const KEY: Option<&'static str> = Some("base_keymap"); + + type FileContent = Option; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &gpui::AppContext, + ) -> anyhow::Result + where + Self: Sized, + { + Ok(user_values + .first() + .and_then(|v| **v) + .unwrap_or(default_value.unwrap())) + } +} diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index d78b230dd9..b7460c4c46 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -1,24 +1,27 @@ mod base_keymap_picker; +mod base_keymap_setting; -use std::{borrow::Cow, sync::Arc}; - +use crate::base_keymap_picker::ToggleBaseKeymapSelector; +use client::TelemetrySettings; use db::kvp::KEY_VALUE_STORE; use gpui::{ elements::{Flex, Label, ParentElement}, AnyElement, AppContext, Element, Entity, Subscription, View, ViewContext, WeakViewHandle, }; -use settings::{settings_file::SettingsFile, Settings}; - +use settings::{update_settings_file, SettingsStore}; +use std::{borrow::Cow, sync::Arc}; use workspace::{ dock::DockPosition, item::Item, open_new, AppState, PaneBackdrop, Welcome, Workspace, WorkspaceId, }; -use crate::base_keymap_picker::ToggleBaseKeymapSelector; +pub use base_keymap_setting::BaseKeymap; pub const FIRST_OPEN: &str = "first_open"; pub fn init(cx: &mut AppContext) { + settings::register::(cx); + cx.add_action(|workspace: &mut Workspace, _: &Welcome, cx| { let welcome_page = cx.add_view(|cx| WelcomePage::new(workspace, cx)); workspace.add_item(Box::new(welcome_page), cx) @@ -58,15 +61,10 @@ impl View for WelcomePage { fn render(&mut self, cx: &mut gpui::ViewContext) -> AnyElement { let self_handle = cx.handle(); - let settings = cx.global::(); - let theme = settings.theme.clone(); - + let theme = theme::current(cx); let width = theme.welcome.page_width; - let (diagnostics, metrics) = { - let telemetry = settings.telemetry(); - (telemetry.diagnostics(), telemetry.metrics()) - }; + let telemetry_settings = *settings::get::(cx); enum Metrics {} enum Diagnostics {} @@ -166,13 +164,18 @@ impl View for WelcomePage { .with_style(theme.welcome.usage_note.container), ), &theme.welcome.checkbox, - metrics, + telemetry_settings.metrics, 0, cx, - |_, checked, cx| { - SettingsFile::update(cx, move |file| { - file.telemetry.set_metrics(checked) - }) + |this, checked, cx| { + if let Some(workspace) = this.workspace.upgrade(cx) { + let fs = workspace.read(cx).app_state().fs.clone(); + update_settings_file::( + fs, + cx, + move |setting| setting.metrics = Some(checked), + ) + } }, ) .contained() @@ -182,13 +185,18 @@ impl View for WelcomePage { theme::ui::checkbox::( "Send crash reports", &theme.welcome.checkbox, - diagnostics, + telemetry_settings.diagnostics, 0, cx, - |_, checked, cx| { - SettingsFile::update(cx, move |file| { - file.telemetry.set_diagnostics(checked) - }) + |this, checked, cx| { + if let Some(workspace) = this.workspace.upgrade(cx) { + let fs = workspace.read(cx).app_state().fs.clone(); + update_settings_file::( + fs, + cx, + move |setting| setting.diagnostics = Some(checked), + ) + } }, ) .contained() @@ -214,7 +222,7 @@ impl WelcomePage { pub fn new(workspace: &Workspace, cx: &mut ViewContext) -> Self { WelcomePage { workspace: workspace.weak_handle(), - _settings_subscription: cx.observe_global::(move |_, cx| cx.notify()), + _settings_subscription: cx.observe_global::(move |_, cx| cx.notify()), } } } @@ -250,7 +258,7 @@ impl Item for WelcomePage { ) -> Option { Some(WelcomePage { workspace: self.workspace.clone(), - _settings_subscription: cx.observe_global::(move |_, cx| cx.notify()), + _settings_subscription: cx.observe_global::(move |_, cx| cx.notify()), }) } } diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 177dc0a292..33e5e7aefe 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -38,6 +38,7 @@ theme = { path = "../theme" } util = { path = "../util" } async-recursion = "1.0.0" +itertools = "0.10" bincode = "1.2.1" anyhow.workspace = true futures.workspace = true @@ -45,6 +46,7 @@ lazy_static.workspace = true log.workspace = true parking_lot.workspace = true postage.workspace = true +schemars.workspace = true serde.workspace = true serde_derive.workspace = true serde_json.workspace = true diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 83bb54d3be..c327c4ded8 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -6,8 +6,8 @@ use gpui::{ WindowContext, }; use serde::Deserialize; -use settings::Settings; use std::rc::Rc; +use theme::ThemeSettings; pub trait Panel: View { fn position(&self, cx: &WindowContext) -> DockPosition; @@ -379,7 +379,7 @@ impl Dock { pub fn render_placeholder(&self, cx: &WindowContext) -> AnyElement { if let Some(active_entry) = self.active_entry() { - let style = &cx.global::().theme.workspace.dock; + let style = &settings::get::(cx).theme.workspace.dock; Empty::new() .into_any() .contained() @@ -407,7 +407,7 @@ impl View for Dock { fn render(&mut self, cx: &mut ViewContext) -> AnyElement { if let Some(active_entry) = self.active_entry() { - let style = &cx.global::().theme.workspace.dock; + let style = &settings::get::(cx).theme.workspace.dock; ChildView::new(active_entry.panel.as_any(), cx) .contained() .with_style(style.container) @@ -444,7 +444,7 @@ impl View for PanelButtons { } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let theme = &cx.global::().theme; + let theme = &settings::get::(cx).theme; let tooltip_style = theme.tooltip.clone(); let theme = &theme.workspace.status_bar.panel_buttons; let button_style = theme.button.clone(); @@ -578,7 +578,7 @@ impl StatusItemView for PanelButtons { #[cfg(test)] pub(crate) mod test { use super::*; - use gpui::Entity; + use gpui::{ViewContext, WindowContext}; pub enum TestPanelEvent { PositionChanged, diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 2adbff51fe..c947078015 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -3,6 +3,7 @@ use crate::{ FollowableItemBuilders, ItemNavHistory, Pane, ToolbarItemLocation, ViewId, Workspace, WorkspaceId, }; +use crate::{AutosaveSetting, WorkspaceSettings}; use anyhow::Result; use client::{proto, Client}; use gpui::{ @@ -10,7 +11,6 @@ use gpui::{ ViewContext, ViewHandle, WeakViewHandle, WindowContext, }; use project::{Project, ProjectEntryId, ProjectPath}; -use settings::{Autosave, Settings}; use smallvec::SmallVec; use std::{ any::{Any, TypeId}, @@ -450,8 +450,11 @@ impl ItemHandle for ViewHandle { } ItemEvent::Edit => { - if let Autosave::AfterDelay { milliseconds } = - cx.global::().autosave + let settings = settings::get::(cx); + let debounce_delay = settings.git.gutter_debounce; + + if let AutosaveSetting::AfterDelay { milliseconds } = + settings.autosave { let delay = Duration::from_millis(milliseconds); let item = item.clone(); @@ -460,9 +463,6 @@ impl ItemHandle for ViewHandle { }); } - let settings = cx.global::(); - let debounce_delay = settings.git_overrides.gutter_debounce; - let item = item.clone(); if let Some(delay) = debounce_delay { @@ -500,7 +500,10 @@ impl ItemHandle for ViewHandle { })); cx.observe_focus(self, move |workspace, item, focused, cx| { - if !focused && cx.global::().autosave == Autosave::OnFocusChange { + if !focused + && settings::get::(cx).autosave + == AutosaveSetting::OnFocusChange + { Pane::autosave_item(&item, workspace.project.clone(), cx) .detach_and_log_err(cx); } diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 7881603bbc..21b3be09d0 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -149,6 +149,8 @@ impl Workspace { } pub mod simple_message_notification { + use super::Notification; + use crate::Workspace; use gpui::{ actions, elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text}, @@ -158,13 +160,8 @@ pub mod simple_message_notification { }; use menu::Cancel; use serde::Deserialize; - use settings::Settings; use std::{borrow::Cow, sync::Arc}; - use crate::Workspace; - - use super::Notification; - actions!(message_notifications, [CancelMessageNotification]); #[derive(Clone, Default, Deserialize, PartialEq)] @@ -240,7 +237,7 @@ pub mod simple_message_notification { } fn render(&mut self, cx: &mut gpui::ViewContext) -> gpui::AnyElement { - let theme = cx.global::().theme.clone(); + let theme = theme::current(cx).clone(); let theme = &theme.simple_message_notification; enum MessageNotificationTag {} diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 681a7eda27..5f698d89a5 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2,8 +2,8 @@ mod dragged_item_receiver; use super::{ItemHandle, SplitDirection}; use crate::{ - item::WeakItemHandle, toolbar::Toolbar, Item, NewFile, NewSearch, NewTerminal, ToggleZoom, - Workspace, + item::WeakItemHandle, toolbar::Toolbar, AutosaveSetting, Item, NewFile, NewSearch, NewTerminal, + ToggleZoom, Workspace, WorkspaceSettings, }; use anyhow::{anyhow, Result}; use collections::{HashMap, HashSet, VecDeque}; @@ -27,9 +27,18 @@ use gpui::{ }; use project::{Project, ProjectEntryId, ProjectPath}; use serde::Deserialize; -use settings::{Autosave, Settings}; -use std::{any::Any, cell::RefCell, cmp, mem, path::Path, rc::Rc}; -use theme::Theme; +use std::{ + any::Any, + cell::RefCell, + cmp, mem, + path::Path, + rc::Rc, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, +}; +use theme::{Theme, ThemeSettings}; use util::ResultExt; #[derive(Clone, Deserialize, PartialEq)] @@ -163,6 +172,8 @@ pub struct ItemNavHistory { item: Rc, } +pub struct PaneNavHistory(Rc>); + struct NavHistory { mode: NavigationMode, backward_stack: VecDeque, @@ -170,6 +181,7 @@ struct NavHistory { closed_stack: VecDeque, paths_by_item: HashMap, pane: WeakViewHandle, + next_timestamp: Arc, } #[derive(Copy, Clone)] @@ -191,6 +203,7 @@ impl Default for NavigationMode { pub struct NavigationEntry { pub item: Rc, pub data: Option>, + pub timestamp: usize, } pub struct DraggedItem { @@ -228,6 +241,7 @@ impl Pane { pub fn new( workspace: WeakViewHandle, background_actions: BackgroundActions, + next_timestamp: Arc, cx: &mut ViewContext, ) -> Self { let pane_view_id = cx.view_id(); @@ -252,6 +266,7 @@ impl Pane { closed_stack: Default::default(), paths_by_item: Default::default(), pane: handle.clone(), + next_timestamp, })), toolbar: cx.add_view(|_| Toolbar::new(handle)), tab_bar_context_menu: TabBarContextMenu { @@ -332,6 +347,10 @@ impl Pane { } } + pub fn nav_history(&self) -> PaneNavHistory { + PaneNavHistory(self.nav_history.clone()) + } + pub fn go_back( workspace: &mut Workspace, pane: Option>, @@ -756,6 +775,10 @@ impl Pane { _: &CloseInactiveItems, cx: &mut ViewContext, ) -> Option>> { + if self.items.is_empty() { + return None; + } + let active_item_id = self.items[self.active_item_index].id(); Some(self.close_items(cx, move |item_id| item_id != active_item_id)) } @@ -778,6 +801,9 @@ impl Pane { _: &CloseItemsToTheLeft, cx: &mut ViewContext, ) -> Option>> { + if self.items.is_empty() { + return None; + } let active_item_id = self.items[self.active_item_index].id(); Some(self.close_items_to_the_left_by_id(active_item_id, cx)) } @@ -800,6 +826,9 @@ impl Pane { _: &CloseItemsToTheRight, cx: &mut ViewContext, ) -> Option>> { + if self.items.is_empty() { + return None; + } let active_item_id = self.items[self.active_item_index].id(); Some(self.close_items_to_the_right_by_id(active_item_id, cx)) } @@ -995,8 +1024,8 @@ impl Pane { } else if is_dirty && (can_save || is_singleton) { let will_autosave = cx.read(|cx| { matches!( - cx.global::().autosave, - Autosave::OnFocusChange | Autosave::OnWindowChange + settings::get::(cx).autosave, + AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange ) && Self::can_autosave_item(&*item, cx) }); let should_save = if should_prompt_for_save && !will_autosave { @@ -1220,6 +1249,25 @@ impl Pane { &self.toolbar } + pub fn handle_deleted_project_item( + &mut self, + entry_id: ProjectEntryId, + cx: &mut ViewContext, + ) -> Option<()> { + let (item_index_to_delete, item_id) = self.items().enumerate().find_map(|(i, item)| { + if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] { + Some((i, item.id())) + } else { + None + } + })?; + + self.remove_item(item_index_to_delete, false, cx); + self.nav_history.borrow_mut().remove_item(item_id); + + Some(()) + } + fn update_toolbar(&mut self, cx: &mut ViewContext) { let active_item = self .items @@ -1231,7 +1279,7 @@ impl Pane { } fn render_tabs(&mut self, cx: &mut ViewContext) -> impl Element { - let theme = cx.global::().theme.clone(); + let theme = theme::current(cx).clone(); let pane = cx.handle().downgrade(); let autoscroll = if mem::take(&mut self.autoscroll) { @@ -1262,7 +1310,7 @@ impl Pane { let pane = pane.clone(); let detail = detail.clone(); - let theme = cx.global::().theme.clone(); + let theme = theme::current(cx).clone(); let mut tooltip_theme = theme.tooltip.clone(); tooltip_theme.max_text_width = None; let tab_tooltip_text = item.tab_tooltip_text(cx).map(|a| a.to_string()); @@ -1327,7 +1375,7 @@ impl Pane { pane: pane.clone(), }, { - let theme = cx.global::().theme.clone(); + let theme = theme::current(cx).clone(); let detail = detail.clone(); move |dragged_item: &DraggedItem, cx: &mut ViewContext| { @@ -1532,7 +1580,7 @@ impl Pane { Stack::new() .with_child( MouseEventHandler::::new(index, cx, |mouse_state, cx| { - let theme = &cx.global::().theme.workspace.tab_bar; + let theme = &settings::get::(cx).theme.workspace.tab_bar; let style = theme.pane_button.style_for(mouse_state, false); Svg::new(icon) .with_color(style.color) @@ -1589,7 +1637,7 @@ impl View for Pane { if let Some(active_item) = self.active_item() { Flex::column() .with_child({ - let theme = cx.global::().theme.clone(); + let theme = theme::current(cx).clone(); let mut stack = Stack::new(); @@ -1659,7 +1707,7 @@ impl View for Pane { .into_any() } else { enum EmptyPane {} - let theme = cx.global::().theme.clone(); + let theme = theme::current(cx).clone(); dragged_item_receiver::(self, 0, 0, false, None, cx, |_, cx| { self.render_blank_pane(&theme, cx) @@ -1803,6 +1851,7 @@ impl NavHistory { self.backward_stack.push_back(NavigationEntry { item, data: data.map(|data| Box::new(data) as Box), + timestamp: self.next_timestamp.fetch_add(1, Ordering::SeqCst), }); self.forward_stack.clear(); } @@ -1813,6 +1862,7 @@ impl NavHistory { self.forward_stack.push_back(NavigationEntry { item, data: data.map(|data| Box::new(data) as Box), + timestamp: self.next_timestamp.fetch_add(1, Ordering::SeqCst), }); } NavigationMode::GoingForward => { @@ -1822,6 +1872,7 @@ impl NavHistory { self.backward_stack.push_back(NavigationEntry { item, data: data.map(|data| Box::new(data) as Box), + timestamp: self.next_timestamp.fetch_add(1, Ordering::SeqCst), }); } NavigationMode::ClosingItem => { @@ -1831,6 +1882,7 @@ impl NavHistory { self.closed_stack.push_back(NavigationEntry { item, data: data.map(|data| Box::new(data) as Box), + timestamp: self.next_timestamp.fetch_add(1, Ordering::SeqCst), }); } } @@ -1844,6 +1896,40 @@ impl NavHistory { }); } } + + fn remove_item(&mut self, item_id: usize) { + self.paths_by_item.remove(&item_id); + self.backward_stack + .retain(|entry| entry.item.id() != item_id); + self.forward_stack + .retain(|entry| entry.item.id() != item_id); + self.closed_stack.retain(|entry| entry.item.id() != item_id); + } +} + +impl PaneNavHistory { + pub fn for_each_entry( + &self, + cx: &AppContext, + mut f: impl FnMut(&NavigationEntry, ProjectPath), + ) { + let borrowed_history = self.0.borrow(); + borrowed_history + .forward_stack + .iter() + .chain(borrowed_history.backward_stack.iter()) + .chain(borrowed_history.closed_stack.iter()) + .for_each(|entry| { + if let Some(path) = borrowed_history.paths_by_item.get(&entry.item.id()) { + f(entry, path.clone()); + } else if let Some(item) = entry.item.upgrade(cx) { + let path = item.project_path(cx); + if let Some(path) = path { + f(entry, path); + } + } + }) + } } pub struct PaneBackdrop { @@ -1884,7 +1970,7 @@ impl Element for PaneBackdrop { view: &mut V, cx: &mut ViewContext, ) -> Self::PaintState { - let background = cx.global::().theme.editor.background; + let background = theme::current(cx).editor.background; let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default(); @@ -1948,10 +2034,11 @@ mod tests { use crate::item::test::{TestItem, TestProjectItem}; use gpui::{executor::Deterministic, TestAppContext}; use project::FakeFs; + use settings::SettingsStore; #[gpui::test] async fn test_remove_active_empty(cx: &mut TestAppContext) { - Settings::test_async(cx); + init_test(cx); let fs = FakeFs::new(cx.background()); let project = Project::test(fs, None, cx).await; @@ -1966,7 +2053,7 @@ mod tests { #[gpui::test] async fn test_add_item_with_new_item(cx: &mut TestAppContext) { cx.foreground().forbid_parking(); - Settings::test_async(cx); + init_test(cx); let fs = FakeFs::new(cx.background()); let project = Project::test(fs, None, cx).await; @@ -2054,7 +2141,7 @@ mod tests { #[gpui::test] async fn test_add_item_with_existing_item(cx: &mut TestAppContext) { cx.foreground().forbid_parking(); - Settings::test_async(cx); + init_test(cx); let fs = FakeFs::new(cx.background()); let project = Project::test(fs, None, cx).await; @@ -2130,7 +2217,7 @@ mod tests { #[gpui::test] async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) { cx.foreground().forbid_parking(); - Settings::test_async(cx); + init_test(cx); let fs = FakeFs::new(cx.background()); let project = Project::test(fs, None, cx).await; @@ -2239,7 +2326,7 @@ mod tests { #[gpui::test] async fn test_remove_item_ordering(deterministic: Arc, cx: &mut TestAppContext) { - Settings::test_async(cx); + init_test(cx); let fs = FakeFs::new(cx.background()); let project = Project::test(fs, None, cx).await; @@ -2278,7 +2365,7 @@ mod tests { #[gpui::test] async fn test_close_inactive_items(deterministic: Arc, cx: &mut TestAppContext) { - Settings::test_async(cx); + init_test(cx); let fs = FakeFs::new(cx.background()); let project = Project::test(fs, None, cx).await; @@ -2297,7 +2384,7 @@ mod tests { #[gpui::test] async fn test_close_clean_items(deterministic: Arc, cx: &mut TestAppContext) { - Settings::test_async(cx); + init_test(cx); let fs = FakeFs::new(cx.background()); let project = Project::test(fs, None, cx).await; @@ -2322,7 +2409,7 @@ mod tests { deterministic: Arc, cx: &mut TestAppContext, ) { - Settings::test_async(cx); + init_test(cx); let fs = FakeFs::new(cx.background()); let project = Project::test(fs, None, cx).await; @@ -2344,7 +2431,7 @@ mod tests { deterministic: Arc, cx: &mut TestAppContext, ) { - Settings::test_async(cx); + init_test(cx); let fs = FakeFs::new(cx.background()); let project = Project::test(fs, None, cx).await; @@ -2363,7 +2450,7 @@ mod tests { #[gpui::test] async fn test_close_all_items(deterministic: Arc, cx: &mut TestAppContext) { - Settings::test_async(cx); + init_test(cx); let fs = FakeFs::new(cx.background()); let project = Project::test(fs, None, cx).await; @@ -2381,6 +2468,14 @@ mod tests { assert_item_labels(&pane, [], cx); } + fn init_test(cx: &mut TestAppContext) { + cx.update(|cx| { + cx.set_global(SettingsStore::test(cx)); + theme::init((), cx); + crate::init_settings(cx); + }); + } + fn add_labeled_item( workspace: &ViewHandle, pane: &ViewHandle, diff --git a/crates/workspace/src/pane/dragged_item_receiver.rs b/crates/workspace/src/pane/dragged_item_receiver.rs index 003cf1e4ef..bb5d3a2464 100644 --- a/crates/workspace/src/pane/dragged_item_receiver.rs +++ b/crates/workspace/src/pane/dragged_item_receiver.rs @@ -1,3 +1,5 @@ +use super::DraggedItem; +use crate::{Pane, SplitDirection, Workspace}; use drag_and_drop::DragAndDrop; use gpui::{ color::Color, @@ -8,11 +10,6 @@ use gpui::{ AppContext, Element, EventContext, MouseState, Quad, View, ViewContext, WeakViewHandle, }; use project::ProjectEntryId; -use settings::Settings; - -use crate::{Pane, SplitDirection, Workspace}; - -use super::DraggedItem; pub fn dragged_item_receiver( pane: &Pane, @@ -234,8 +231,5 @@ fn drop_split_direction( } fn overlay_color(cx: &AppContext) -> Color { - cx.global::() - .theme - .workspace - .drop_target_overlay_color + theme::current(cx).workspace.drop_target_overlay_color } diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 5f6d46aa46..5e5a5a98ba 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use crate::{AppState, FollowerStatesByLeader, Pane, Workspace}; +use crate::{AppState, FollowerStatesByLeader, Pane, Workspace, WorkspaceSettings}; use anyhow::{anyhow, Result}; use call::{ActiveCall, ParticipantLocation}; use gpui::{ @@ -11,7 +11,6 @@ use gpui::{ }; use project::Project; use serde::Deserialize; -use settings::Settings; use theme::Theme; #[derive(Clone, Debug, Eq, PartialEq)] @@ -391,7 +390,7 @@ impl PaneAxis { .with_children(self.members.iter().enumerate().map(|(ix, member)| { let mut flex = 1.0; if member.contains(active_pane) { - flex = cx.global::().active_pane_magnification; + flex = settings::get::(cx).active_pane_magnification; } let mut member = member.render( diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 9cf13af681..fbefd3984e 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -497,10 +497,8 @@ impl WorkspaceDb { #[cfg(test)] mod tests { - - use db::open_test_db; - use super::*; + use db::open_test_db; #[gpui::test] async fn test_next_id_stability() { diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index bc10c931bb..fe7735753f 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -1,4 +1,4 @@ -use crate::{ItemDeserializers, Member, Pane, PaneAxis, Workspace, WorkspaceId}; +use crate::{item::ItemHandle, ItemDeserializers, Member, Pane, PaneAxis, Workspace, WorkspaceId}; use anyhow::{anyhow, Context, Result}; use async_recursion::async_recursion; use db::sqlez::{ @@ -150,17 +150,23 @@ impl SerializedPaneGroup { workspace_id: WorkspaceId, workspace: &WeakViewHandle, cx: &mut AsyncAppContext, - ) -> Option<(Member, Option>)> { + ) -> Option<( + Member, + Option>, + Vec>>, + )> { match self { SerializedPaneGroup::Group { axis, children } => { let mut current_active_pane = None; let mut members = Vec::new(); + let mut items = Vec::new(); for child in children { - if let Some((new_member, active_pane)) = child + if let Some((new_member, active_pane, new_items)) = child .deserialize(project, workspace_id, workspace, cx) .await { members.push(new_member); + items.extend(new_items); current_active_pane = current_active_pane.or(active_pane); } } @@ -170,7 +176,7 @@ impl SerializedPaneGroup { } if members.len() == 1 { - return Some((members.remove(0), current_active_pane)); + return Some((members.remove(0), current_active_pane, items)); } Some(( @@ -179,6 +185,7 @@ impl SerializedPaneGroup { members, }), current_active_pane, + items, )) } SerializedPaneGroup::Pane(serialized_pane) => { @@ -186,7 +193,7 @@ impl SerializedPaneGroup { .update(cx, |workspace, cx| workspace.add_pane(cx).downgrade()) .log_err()?; let active = serialized_pane.active; - serialized_pane + let new_items = serialized_pane .deserialize_to(project, &pane, workspace_id, workspace, cx) .await .log_err()?; @@ -196,7 +203,7 @@ impl SerializedPaneGroup { .log_err()? { let pane = pane.upgrade(cx)?; - Some((Member::Pane(pane.clone()), active.then(|| pane))) + Some((Member::Pane(pane.clone()), active.then(|| pane), new_items)) } else { let pane = pane.upgrade(cx)?; workspace @@ -227,7 +234,8 @@ impl SerializedPane { workspace_id: WorkspaceId, workspace: &WeakViewHandle, cx: &mut AsyncAppContext, - ) -> Result<()> { + ) -> Result>>> { + let mut items = Vec::new(); let mut active_item_index = None; for (index, item) in self.children.iter().enumerate() { let project = project.clone(); @@ -245,6 +253,8 @@ impl SerializedPane { .await .log_err(); + items.push(item_handle.clone()); + if let Some(item_handle) = item_handle { workspace.update(cx, |workspace, cx| { let pane_handle = pane_handle @@ -266,7 +276,7 @@ impl SerializedPane { })?; } - anyhow::Ok(()) + anyhow::Ok(items) } } @@ -330,40 +340,3 @@ impl Column for SerializedItem { )) } } - -#[cfg(test)] -mod tests { - use db::sqlez::connection::Connection; - - // use super::WorkspaceLocation; - - #[test] - fn test_workspace_round_trips() { - let _db = Connection::open_memory(Some("workspace_id_round_trips")); - - todo!(); - // db.exec(indoc::indoc! {" - // CREATE TABLE workspace_id_test( - // workspace_id INTEGER, - // dock_anchor TEXT - // );"}) - // .unwrap()() - // .unwrap(); - - // let workspace_id: WorkspaceLocation = WorkspaceLocation::from(&["\test2", "\test1"]); - - // db.exec_bound("INSERT INTO workspace_id_test(workspace_id, dock_anchor) VALUES (?,?)") - // .unwrap()((&workspace_id, DockAnchor::Bottom)) - // .unwrap(); - - // assert_eq!( - // db.select_row("SELECT workspace_id, dock_anchor FROM workspace_id_test LIMIT 1") - // .unwrap()() - // .unwrap(), - // Some(( - // WorkspaceLocation::from(&["\test1", "\test2"]), - // DockAnchor::Bottom - // )) - // ); - } -} diff --git a/crates/workspace/src/shared_screen.rs b/crates/workspace/src/shared_screen.rs index 5cc54a6a7f..9a2e0bc5d2 100644 --- a/crates/workspace/src/shared_screen.rs +++ b/crates/workspace/src/shared_screen.rs @@ -12,7 +12,6 @@ use gpui::{ platform::MouseButton, AppContext, Entity, Task, View, ViewContext, }; -use settings::Settings; use smallvec::SmallVec; use std::{ borrow::Cow, @@ -88,7 +87,7 @@ impl View for SharedScreen { } }) .contained() - .with_style(cx.global::().theme.shared_screen) + .with_style(theme::current(cx).shared_screen) }) .on_down(MouseButton::Left, |_, _, cx| cx.focus_parent()) .into_any() diff --git a/crates/workspace/src/status_bar.rs b/crates/workspace/src/status_bar.rs index b4de6b3575..6fc1467566 100644 --- a/crates/workspace/src/status_bar.rs +++ b/crates/workspace/src/status_bar.rs @@ -11,7 +11,6 @@ use gpui::{ AnyElement, AnyViewHandle, Entity, LayoutContext, SceneBuilder, SizeConstraint, Subscription, View, ViewContext, ViewHandle, WindowContext, }; -use settings::Settings; pub trait StatusItemView: View { fn set_active_pane_item( @@ -47,7 +46,7 @@ impl View for StatusBar { } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let theme = &cx.global::().theme.workspace.status_bar; + let theme = &theme::current(cx).workspace.status_bar; StatusBarElement { left: Flex::row() diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs index b2832aa1e8..30890ed5d2 100644 --- a/crates/workspace/src/toolbar.rs +++ b/crates/workspace/src/toolbar.rs @@ -3,7 +3,6 @@ use gpui::{ elements::*, platform::CursorStyle, platform::MouseButton, Action, AnyElement, AnyViewHandle, AppContext, Entity, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, }; -use settings::Settings; pub trait ToolbarItemView: View { fn set_active_pane_item( @@ -68,7 +67,7 @@ impl View for Toolbar { } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let theme = &cx.global::().theme.workspace.toolbar; + let theme = &theme::current(cx).workspace.toolbar; let mut primary_left_items = Vec::new(); let mut primary_right_items = Vec::new(); @@ -131,7 +130,7 @@ impl View for Toolbar { let height = theme.height * primary_items_row_count as f32; let nav_button_height = theme.height; let button_style = theme.nav_button; - let tooltip_style = cx.global::().theme.tooltip.clone(); + let tooltip_style = theme::current(cx).tooltip.clone(); Flex::column() .with_child( diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index dfb8560843..78900eb002 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -12,6 +12,7 @@ pub mod searchable; pub mod shared_screen; mod status_bar; mod toolbar; +mod workspace_settings; use anyhow::{anyhow, Context, Result}; use assets::Assets; @@ -44,6 +45,7 @@ use gpui::{ WindowContext, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem}; +use itertools::Itertools; use language::{LanguageRegistry, Rope}; use std::{ any::TypeId, @@ -52,7 +54,7 @@ use std::{ future::Future, path::{Path, PathBuf}, str, - sync::Arc, + sync::{atomic::AtomicUsize, Arc}, time::Duration, }; @@ -75,13 +77,13 @@ pub use persistence::{ use postage::prelude::Stream; use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; use serde::Deserialize; -use settings::{Autosave, Settings}; use shared_screen::SharedScreen; use status_bar::StatusBar; pub use status_bar::StatusItemView; -use theme::{Theme, ThemeRegistry}; +use theme::Theme; pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; -use util::{paths, ResultExt}; +use util::{async_iife, paths, ResultExt}; +pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings}; lazy_static! { static ref ZED_WINDOW_SIZE: Option = env::var("ZED_WINDOW_SIZE") @@ -184,7 +186,12 @@ pub type WorkspaceId = i64; impl_actions!(workspace, [ActivatePane]); +pub fn init_settings(cx: &mut AppContext) { + settings::register::(cx); +} + pub fn init(app_state: Arc, cx: &mut AppContext) { + init_settings(cx); pane::init(cx); notifications::init(cx); @@ -234,7 +241,6 @@ pub fn init(app_state: Arc, cx: &mut AppContext) { }, ); cx.add_action(Workspace::toggle_panel); - cx.add_action(Workspace::focus_center); cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| { workspace.activate_previous_pane(cx) }); @@ -270,7 +276,7 @@ pub fn init(app_state: Arc, cx: &mut AppContext) { cx.add_action( move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext| { create_and_open_local_file(&paths::SETTINGS, cx, || { - Settings::initial_user_settings_content(&Assets) + settings::initial_user_settings_content(&Assets) .as_ref() .into() }) @@ -355,7 +361,6 @@ pub fn register_deserializable_item(cx: &mut AppContext) { pub struct AppState { pub languages: Arc, - pub themes: Arc, pub client: Arc, pub user_store: ModelHandle, pub fs: Arc, @@ -369,18 +374,24 @@ pub struct AppState { impl AppState { #[cfg(any(test, feature = "test-support"))] pub fn test(cx: &mut AppContext) -> Arc { - let settings = Settings::test(cx); - cx.set_global(settings); + use settings::SettingsStore; + + if !cx.has_global::() { + cx.set_global(SettingsStore::test(cx)); + } let fs = fs::FakeFs::new(cx.background().clone()); let languages = Arc::new(LanguageRegistry::test()); let http_client = util::http::FakeHttpClient::with_404_response(); let client = Client::new(http_client.clone(), cx); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); - let themes = ThemeRegistry::new((), cx.font_cache().clone()); + + theme::init((), cx); + client::init(&client, cx); + crate::init_settings(cx); + Arc::new(Self { client, - themes, fs, languages, user_store, @@ -469,6 +480,7 @@ pub struct Workspace { _subscriptions: Vec, _apply_leader_updates: Task>, _observe_current_user: Task>, + pane_history_timestamp: Arc, } #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] @@ -492,7 +504,6 @@ struct FollowerState { impl Workspace { pub fn new( - serialized_workspace: Option, workspace_id: WorkspaceId, project: ModelHandle, app_state: Arc, @@ -524,6 +535,14 @@ impl Workspace { cx.remove_window(); } + project::Event::DeletedEntry(entry_id) => { + for pane in this.panes.iter() { + pane.update(cx, |pane, cx| { + pane.handle_deleted_project_item(*entry_id, cx) + }); + } + } + _ => {} } cx.notify() @@ -531,9 +550,16 @@ impl Workspace { .detach(); let weak_handle = cx.weak_handle(); + let pane_history_timestamp = Arc::new(AtomicUsize::new(0)); - let center_pane = - cx.add_view(|cx| Pane::new(weak_handle.clone(), app_state.background_actions, cx)); + let center_pane = cx.add_view(|cx| { + Pane::new( + weak_handle.clone(), + app_state.background_actions, + pane_history_timestamp.clone(), + cx, + ) + }); cx.subscribe(¢er_pane, Self::handle_pane_event).detach(); cx.focus(¢er_pane); cx.emit(Event::PaneAdded(center_pane.clone())); @@ -658,16 +684,10 @@ impl Workspace { _apply_leader_updates, leader_updates_tx, _subscriptions: subscriptions, + pane_history_timestamp, }; this.project_remote_id_changed(project.read(cx).remote_id(), cx); cx.defer(|this, cx| this.update_window_title(cx)); - - if let Some(serialized_workspace) = serialized_workspace { - cx.defer(move |_, cx| { - Self::load_from_serialized_workspace(weak_handle, serialized_workspace, cx) - }); - } - this } @@ -689,18 +709,15 @@ impl Workspace { ); cx.spawn(|mut cx| async move { - let mut serialized_workspace = - persistence::DB.workspace_for_roots(&abs_paths.as_slice()); + let serialized_workspace = persistence::DB.workspace_for_roots(&abs_paths.as_slice()); - let paths_to_open = serialized_workspace - .as_ref() - .map(|workspace| workspace.location.paths()) - .unwrap_or(Arc::new(abs_paths)); + let paths_to_open = Arc::new(abs_paths); // Get project paths for all of the abs_paths let mut worktree_roots: HashSet> = Default::default(); - let mut project_paths = Vec::new(); - for path in paths_to_open.iter() { + let mut project_paths: Vec<(PathBuf, Option)> = + Vec::with_capacity(paths_to_open.len()); + for path in paths_to_open.iter().cloned() { if let Some((worktree, project_entry)) = cx .update(|cx| { Workspace::project_path_for_path(project_handle.clone(), &path, true, cx) @@ -709,9 +726,9 @@ impl Workspace { .log_err() { worktree_roots.insert(worktree.read_with(&mut cx, |tree, _| tree.abs_path())); - project_paths.push(Some(project_entry)); + project_paths.push((path, Some(project_entry))); } else { - project_paths.push(None); + project_paths.push((path, None)); } } @@ -731,21 +748,13 @@ impl Workspace { )) }); - let was_deserialized = serialized_workspace.is_some(); + let build_workspace = |cx: &mut ViewContext| { + Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx) + }; let workspace = requesting_window_id .and_then(|window_id| { - cx.update(|cx| { - cx.replace_root_view(window_id, |cx| { - Workspace::new( - serialized_workspace.take(), - workspace_id, - project_handle.clone(), - app_state.clone(), - cx, - ) - }) - }) + cx.update(|cx| cx.replace_root_view(window_id, |cx| build_workspace(cx))) }) .unwrap_or_else(|| { let (bounds, display) = if let Some(bounds) = window_bounds_override { @@ -783,22 +792,14 @@ impl Workspace { // Use the serialized workspace to construct the new window cx.add_window( (app_state.build_window_options)(bounds, display, cx.platform().as_ref()), - |cx| { - Workspace::new( - serialized_workspace, - workspace_id, - project_handle.clone(), - app_state.clone(), - cx, - ) - }, + |cx| build_workspace(cx), ) .1 }); (app_state.initialize_workspace)( workspace.downgrade(), - was_deserialized, + serialized_workspace.is_some(), app_state.clone(), cx.clone(), ) @@ -809,37 +810,14 @@ impl Workspace { let workspace = workspace.downgrade(); notify_if_database_failed(&workspace, &mut cx); - - // Call open path for each of the project paths - // (this will bring them to the front if they were in the serialized workspace) - debug_assert!(paths_to_open.len() == project_paths.len()); - let tasks = paths_to_open - .iter() - .cloned() - .zip(project_paths.into_iter()) - .map(|(abs_path, project_path)| { - let workspace = workspace.clone(); - cx.spawn(|mut cx| { - let fs = app_state.fs.clone(); - async move { - let project_path = project_path?; - if fs.is_file(&abs_path).await { - Some( - workspace - .update(&mut cx, |workspace, cx| { - workspace.open_path(project_path, None, true, cx) - }) - .log_err()? - .await, - ) - } else { - None - } - } - }) - }); - - let opened_items = futures::future::join_all(tasks.into_iter()).await; + let opened_items = open_items( + serialized_workspace, + &workspace, + project_paths, + app_state, + cx, + ) + .await; (workspace, opened_items) }) @@ -936,6 +914,39 @@ impl Workspace { &self.project } + pub fn recent_navigation_history( + &self, + limit: Option, + cx: &AppContext, + ) -> Vec { + let mut history: HashMap = HashMap::default(); + for pane in &self.panes { + let pane = pane.read(cx); + pane.nav_history() + .for_each_entry(cx, |entry, project_path| { + let timestamp = entry.timestamp; + match history.entry(project_path) { + hash_map::Entry::Occupied(mut entry) => { + if ×tamp > entry.get() { + entry.insert(timestamp); + } + } + hash_map::Entry::Vacant(entry) => { + entry.insert(timestamp); + } + } + }); + } + + history + .into_iter() + .sorted_by_key(|(_, timestamp)| *timestamp) + .map(|(project_path, _)| project_path) + .rev() + .take(limit.unwrap_or(usize::MAX)) + .collect() + } + pub fn client(&self) -> &Client { &self.app_state.client } @@ -1193,6 +1204,8 @@ impl Workspace { visible: bool, cx: &mut ViewContext, ) -> Task, anyhow::Error>>>> { + log::info!("open paths {:?}", abs_paths); + let fs = self.app_state.fs.clone(); // Sort the paths to ensure we add worktrees for parents before their children. @@ -1527,14 +1540,15 @@ impl Workspace { cx.notify(); } - pub fn focus_center(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { - cx.focus_self(); - cx.notify(); - } - fn add_pane(&mut self, cx: &mut ViewContext) -> ViewHandle { - let pane = - cx.add_view(|cx| Pane::new(self.weak_handle(), self.app_state.background_actions, cx)); + let pane = cx.add_view(|cx| { + Pane::new( + self.weak_handle(), + self.app_state.background_actions, + self.pane_history_timestamp.clone(), + cx, + ) + }); cx.subscribe(&pane, Self::handle_pane_event).detach(); self.panes.push(pane.clone()); cx.focus(&pane); @@ -2103,7 +2117,7 @@ impl Workspace { enum DisconnectedOverlay {} Some( MouseEventHandler::::new(0, cx, |_, cx| { - let theme = &cx.global::().theme; + let theme = &theme::current(cx); Label::new( "Your connection to the remote project has been lost.", theme.workspace.disconnected_overlay.text.clone(), @@ -2474,8 +2488,8 @@ impl Workspace { item.workspace_deactivated(cx); } if matches!( - cx.global::().autosave, - Autosave::OnWindowChange | Autosave::OnFocusChange + settings::get::(cx).autosave, + AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange ) { for item in pane.items() { Pane::autosave_item(item.as_ref(), self.project.clone(), cx) @@ -2659,91 +2673,125 @@ impl Workspace { } } - fn load_from_serialized_workspace( + pub(crate) fn load_workspace( workspace: WeakViewHandle, serialized_workspace: SerializedWorkspace, + paths_to_open: Vec>, cx: &mut AppContext, - ) { + ) -> Task, anyhow::Error>>>> { cx.spawn(|mut cx| async move { - let (project, old_center_pane) = workspace.read_with(&cx, |workspace, _| { - ( - workspace.project().clone(), - workspace.last_active_center_pane.clone(), - ) - })?; + let result = async_iife! {{ + let (project, old_center_pane) = + workspace.read_with(&cx, |workspace, _| { + ( + workspace.project().clone(), + workspace.last_active_center_pane.clone(), + ) + })?; - // Traverse the splits tree and add to things - let center_group = serialized_workspace - .center_group - .deserialize(&project, serialized_workspace.id, &workspace, &mut cx) - .await; - - // Remove old panes from workspace panes list - workspace.update(&mut cx, |workspace, cx| { - if let Some((center_group, active_pane)) = center_group { - workspace.remove_panes(workspace.center.root.clone(), cx); - - // Swap workspace center group - workspace.center = PaneGroup::with_root(center_group); - - // Change the focus to the workspace first so that we retrigger focus in on the pane. - cx.focus_self(); - - if let Some(active_pane) = active_pane { - cx.focus(&active_pane); - } else { - cx.focus(workspace.panes.last().unwrap()); - } - } else { - let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx)); - if let Some(old_center_handle) = old_center_handle { - cx.focus(&old_center_handle) - } else { - cx.focus_self() - } + let mut center_items = None; + let mut center_group = None; + // Traverse the splits tree and add to things + if let Some((group, active_pane, items)) = serialized_workspace + .center_group + .deserialize(&project, serialized_workspace.id, &workspace, &mut cx) + .await { + center_items = Some(items); + center_group = Some((group, active_pane)) } - let docks = serialized_workspace.docks; - workspace.left_dock.update(cx, |dock, cx| { - dock.set_open(docks.left.visible, cx); - if let Some(active_panel) = docks.left.active_panel { - if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) { - dock.activate_panel(ix, cx); - } - } - }); - workspace.right_dock.update(cx, |dock, cx| { - dock.set_open(docks.right.visible, cx); - if let Some(active_panel) = docks.right.active_panel { - if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) { - dock.activate_panel(ix, cx); - } - } - }); - workspace.bottom_dock.update(cx, |dock, cx| { - dock.set_open(docks.bottom.visible, cx); - if let Some(active_panel) = docks.bottom.active_panel { - if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) { - dock.activate_panel(ix, cx); - } - } + let resulting_list = cx.read(|cx| { + let mut opened_items = center_items + .unwrap_or_default() + .into_iter() + .filter_map(|item| { + let item = item?; + let project_path = item.project_path(cx)?; + Some((project_path, item)) + }) + .collect::>(); + + paths_to_open + .into_iter() + .map(|path_to_open| { + path_to_open.map(|path_to_open| { + Ok(opened_items.remove(&path_to_open)) + }) + .transpose() + .map(|item| item.flatten()) + .transpose() + }) + .collect::>() }); - cx.notify(); - })?; + // Remove old panes from workspace panes list + workspace.update(&mut cx, |workspace, cx| { + if let Some((center_group, active_pane)) = center_group { + workspace.remove_panes(workspace.center.root.clone(), cx); - // Serialize ourself to make sure our timestamps and any pane / item changes are replicated - workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?; - anyhow::Ok(()) + // Swap workspace center group + workspace.center = PaneGroup::with_root(center_group); + + // Change the focus to the workspace first so that we retrigger focus in on the pane. + cx.focus_self(); + + if let Some(active_pane) = active_pane { + cx.focus(&active_pane); + } else { + cx.focus(workspace.panes.last().unwrap()); + } + } else { + let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx)); + if let Some(old_center_handle) = old_center_handle { + cx.focus(&old_center_handle) + } else { + cx.focus_self() + } + } + + let docks = serialized_workspace.docks; + workspace.left_dock.update(cx, |dock, cx| { + dock.set_open(docks.left.visible, cx); + if let Some(active_panel) = docks.left.active_panel { + if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) { + dock.activate_panel(ix, cx); + } + } + }); + workspace.right_dock.update(cx, |dock, cx| { + dock.set_open(docks.right.visible, cx); + if let Some(active_panel) = docks.right.active_panel { + if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) { + dock.activate_panel(ix, cx); + } + } + }); + workspace.bottom_dock.update(cx, |dock, cx| { + dock.set_open(docks.bottom.visible, cx); + if let Some(active_panel) = docks.bottom.active_panel { + if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) { + dock.activate_panel(ix, cx); + } + } + }); + + cx.notify(); + })?; + + // Serialize ourself to make sure our timestamps and any pane / item changes are replicated + workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?; + + Ok::<_, anyhow::Error>(resulting_list) + }}; + + result.await.unwrap_or_default() }) - .detach_and_log_err(cx); } #[cfg(any(test, feature = "test-support"))] pub fn test_new(project: ModelHandle, cx: &mut ViewContext) -> Self { let app_state = Arc::new(AppState { languages: project.read(cx).languages().clone(), - themes: ThemeRegistry::new((), cx.font_cache().clone()), client: project.read(cx).client(), user_store: project.read(cx).user_store(), fs: project.read(cx).fs().clone(), @@ -2751,7 +2799,7 @@ impl Workspace { initialize_workspace: |_, _, _, _| Task::ready(Ok(())), background_actions: || &[], }); - Self::new(None, 0, project, app_state, cx) + Self::new(0, project, app_state, cx) } fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option> { @@ -2782,6 +2830,95 @@ impl Workspace { } } +async fn open_items( + serialized_workspace: Option, + workspace: &WeakViewHandle, + mut project_paths_to_open: Vec<(PathBuf, Option)>, + app_state: Arc, + mut cx: AsyncAppContext, +) -> Vec>>> { + let mut opened_items = Vec::with_capacity(project_paths_to_open.len()); + + if let Some(serialized_workspace) = serialized_workspace { + let workspace = workspace.clone(); + let restored_items = cx + .update(|cx| { + Workspace::load_workspace( + workspace, + serialized_workspace, + project_paths_to_open + .iter() + .map(|(_, project_path)| project_path) + .cloned() + .collect(), + cx, + ) + }) + .await; + + let restored_project_paths = cx.read(|cx| { + restored_items + .iter() + .filter_map(|item| item.as_ref()?.as_ref().ok()?.project_path(cx)) + .collect::>() + }); + + opened_items = restored_items; + project_paths_to_open + .iter_mut() + .for_each(|(_, project_path)| { + if let Some(project_path_to_open) = project_path { + if restored_project_paths.contains(project_path_to_open) { + *project_path = None; + } + } + }); + } else { + for _ in 0..project_paths_to_open.len() { + opened_items.push(None); + } + } + assert!(opened_items.len() == project_paths_to_open.len()); + + let tasks = + project_paths_to_open + .into_iter() + .enumerate() + .map(|(i, (abs_path, project_path))| { + let workspace = workspace.clone(); + cx.spawn(|mut cx| { + let fs = app_state.fs.clone(); + async move { + let file_project_path = project_path?; + if fs.is_file(&abs_path).await { + Some(( + i, + workspace + .update(&mut cx, |workspace, cx| { + workspace.open_path(file_project_path, None, true, cx) + }) + .log_err()? + .await, + )) + } else { + None + } + } + }) + }); + + for maybe_opened_path in futures::future::join_all(tasks.into_iter()) + .await + .into_iter() + { + if let Some((i, path_open_result)) = maybe_opened_path { + opened_items[i] = Some(path_open_result); + } + } + + opened_items +} + fn notify_if_database_failed(workspace: &WeakViewHandle, cx: &mut AsyncAppContext) { const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml"; @@ -2826,7 +2963,7 @@ impl View for Workspace { } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let theme = cx.global::().theme.clone(); + let theme = theme::current(cx).clone(); Stack::new() .with_child( Flex::column() @@ -2992,8 +3129,6 @@ pub fn open_paths( Vec, anyhow::Error>>>, )>, > { - log::info!("open paths {:?}", abs_paths); - let app_state = app_state.clone(); let abs_paths = abs_paths.to_vec(); cx.spawn(|mut cx| async move { @@ -3105,7 +3240,7 @@ pub fn join_remote_project( let (_, workspace) = cx.add_window( (app_state.build_window_options)(None, None, cx.platform().as_ref()), - |cx| Workspace::new(None, 0, project, app_state.clone(), cx), + |cx| Workspace::new(0, project, app_state.clone(), cx), ); (app_state.initialize_workspace)( workspace.downgrade(), @@ -3156,7 +3291,7 @@ pub fn join_remote_project( } pub fn restart(_: &Restart, cx: &mut AppContext) { - let should_confirm = cx.global::().confirm_quit; + let should_confirm = settings::get::(cx).confirm_quit; cx.spawn(|mut cx| async move { let mut workspaces = cx .window_ids() @@ -3217,23 +3352,21 @@ fn parse_pixel_position_env_var(value: &str) -> Option { #[cfg(test)] mod tests { - use std::{cell::RefCell, rc::Rc}; - + use super::*; use crate::{ dock::test::{TestPanel, TestPanelEvent}, item::test::{TestItem, TestItemEvent, TestProjectItem}, }; - - use super::*; use fs::FakeFs; use gpui::{executor::Deterministic, TestAppContext}; use project::{Project, ProjectEntryId}; use serde_json::json; + use settings::SettingsStore; + use std::{cell::RefCell, rc::Rc}; #[gpui::test] async fn test_tab_disambiguation(cx: &mut TestAppContext) { - cx.foreground().forbid_parking(); - Settings::test_async(cx); + init_test(cx); let fs = FakeFs::new(cx.background()); let project = Project::test(fs, [], cx).await; @@ -3281,8 +3414,8 @@ mod tests { #[gpui::test] async fn test_tracking_active_path(cx: &mut TestAppContext) { - cx.foreground().forbid_parking(); - Settings::test_async(cx); + init_test(cx); + let fs = FakeFs::new(cx.background()); fs.insert_tree( "/root1", @@ -3385,8 +3518,8 @@ mod tests { #[gpui::test] async fn test_close_window(cx: &mut TestAppContext) { - cx.foreground().forbid_parking(); - Settings::test_async(cx); + init_test(cx); + let fs = FakeFs::new(cx.background()); fs.insert_tree("/root", json!({ "one": "" })).await; @@ -3421,8 +3554,8 @@ mod tests { #[gpui::test] async fn test_close_pane_items(cx: &mut TestAppContext) { - cx.foreground().forbid_parking(); - Settings::test_async(cx); + init_test(cx); + let fs = FakeFs::new(cx.background()); let project = Project::test(fs, None, cx).await; @@ -3523,8 +3656,8 @@ mod tests { #[gpui::test] async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) { - cx.foreground().forbid_parking(); - Settings::test_async(cx); + init_test(cx); + let fs = FakeFs::new(cx.background()); let project = Project::test(fs, [], cx).await; @@ -3626,9 +3759,8 @@ mod tests { #[gpui::test] async fn test_autosave(deterministic: Arc, cx: &mut gpui::TestAppContext) { - deterministic.forbid_parking(); + init_test(cx); - Settings::test_async(cx); let fs = FakeFs::new(cx.background()); let project = Project::test(fs, [], cx).await; @@ -3645,8 +3777,10 @@ mod tests { // Autosave on window change. item.update(cx, |item, cx| { - cx.update_global(|settings: &mut Settings, _| { - settings.autosave = Autosave::OnWindowChange; + cx.update_global(|settings: &mut SettingsStore, cx| { + settings.update_user_settings::(cx, |settings| { + settings.autosave = Some(AutosaveSetting::OnWindowChange); + }) }); item.is_dirty = true; }); @@ -3659,8 +3793,10 @@ mod tests { // Autosave on focus change. item.update(cx, |item, cx| { cx.focus_self(); - cx.update_global(|settings: &mut Settings, _| { - settings.autosave = Autosave::OnFocusChange; + cx.update_global(|settings: &mut SettingsStore, cx| { + settings.update_user_settings::(cx, |settings| { + settings.autosave = Some(AutosaveSetting::OnFocusChange); + }) }); item.is_dirty = true; }); @@ -3683,8 +3819,10 @@ mod tests { // Autosave after delay. item.update(cx, |item, cx| { - cx.update_global(|settings: &mut Settings, _| { - settings.autosave = Autosave::AfterDelay { milliseconds: 500 }; + cx.update_global(|settings: &mut SettingsStore, cx| { + settings.update_user_settings::(cx, |settings| { + settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 }); + }) }); item.is_dirty = true; cx.emit(TestItemEvent::Edit); @@ -3700,8 +3838,10 @@ mod tests { // Autosave on focus change, ensuring closing the tab counts as such. item.update(cx, |item, cx| { - cx.update_global(|settings: &mut Settings, _| { - settings.autosave = Autosave::OnFocusChange; + cx.update_global(|settings: &mut SettingsStore, cx| { + settings.update_user_settings::(cx, |settings| { + settings.autosave = Some(AutosaveSetting::OnFocusChange); + }) }); item.is_dirty = true; }); @@ -3735,12 +3875,9 @@ mod tests { } #[gpui::test] - async fn test_pane_navigation( - deterministic: Arc, - cx: &mut gpui::TestAppContext, - ) { - deterministic.forbid_parking(); - Settings::test_async(cx); + async fn test_pane_navigation(cx: &mut gpui::TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background()); let project = Project::test(fs, [], cx).await; @@ -3794,9 +3931,8 @@ mod tests { } #[gpui::test] - async fn test_panels(deterministic: Arc, cx: &mut gpui::TestAppContext) { - deterministic.forbid_parking(); - Settings::test_async(cx); + async fn test_panels(cx: &mut gpui::TestAppContext) { + init_test(cx); let fs = FakeFs::new(cx.background()); let project = Project::test(fs, [], cx).await; @@ -3942,4 +4078,14 @@ mod tests { assert!(!left_dock.read(cx).is_open()); }); } + + pub fn init_test(cx: &mut TestAppContext) { + cx.foreground().forbid_parking(); + cx.update(|cx| { + cx.set_global(SettingsStore::test(cx)); + theme::init((), cx); + language::init(cx); + crate::init_settings(cx); + }); + } } diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs new file mode 100644 index 0000000000..b21c687050 --- /dev/null +++ b/crates/workspace/src/workspace_settings.rs @@ -0,0 +1,58 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::Setting; + +#[derive(Deserialize)] +pub struct WorkspaceSettings { + pub active_pane_magnification: f32, + pub confirm_quit: bool, + pub show_call_status_icon: bool, + pub autosave: AutosaveSetting, + pub git: GitSettings, +} + +#[derive(Clone, Serialize, Deserialize, JsonSchema)] +pub struct WorkspaceSettingsContent { + pub active_pane_magnification: Option, + pub confirm_quit: Option, + pub show_call_status_icon: Option, + pub autosave: Option, + pub git: Option, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum AutosaveSetting { + Off, + AfterDelay { milliseconds: u64 }, + OnFocusChange, + OnWindowChange, +} + +#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +pub struct GitSettings { + pub git_gutter: Option, + pub gutter_debounce: Option, +} + +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum GitGutterSetting { + #[default] + TrackedFiles, + Hide, +} + +impl Setting for WorkspaceSettings { + const KEY: Option<&'static str> = None; + + type FileContent = WorkspaceSettingsContent; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &gpui::AppContext, + ) -> anyhow::Result { + Self::load_via_json_merge(default_value, user_values) + } +} diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 70c71cc18e..90dced65f5 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.86.0" +version = "0.88.0" publish = false [lib] @@ -101,7 +101,7 @@ smol.workspace = true tempdir.workspace = true thiserror.workspace = true tiny_http = "0.8" -toml = "0.5" +toml.workspace = true tree-sitter = "0.20" tree-sitter-c = "0.20.1" tree-sitter-cpp = "0.20.0" diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 91b58f634b..1f2b359af1 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -3,7 +3,6 @@ pub use language::*; use node_runtime::NodeRuntime; use rust_embed::RustEmbed; use std::{borrow::Cow, str, sync::Arc}; -use theme::ThemeRegistry; mod c; mod elixir; @@ -32,11 +31,7 @@ mod yaml; #[exclude = "*.rs"] struct LanguageDir; -pub fn init( - languages: Arc, - themes: Arc, - node_runtime: Arc, -) { +pub fn init(languages: Arc, node_runtime: Arc) { fn adapter_arc(adapter: impl LspAdapter) -> Arc { Arc::new(adapter) } @@ -69,7 +64,6 @@ pub fn init( vec![adapter_arc(json::JsonLspAdapter::new( node_runtime.clone(), languages.clone(), - themes.clone(), ))], ), ("markdown", tree_sitter_markdown::language(), vec![]), diff --git a/crates/zed/src/languages/c.rs b/crates/zed/src/languages/c.rs index 84c5798b07..7e4ddcef19 100644 --- a/crates/zed/src/languages/c.rs +++ b/crates/zed/src/languages/c.rs @@ -249,16 +249,21 @@ impl super::LspAdapter for CLspAdapter { #[cfg(test)] mod tests { use gpui::TestAppContext; - use language::{AutoindentMode, Buffer}; - use settings::Settings; + use language::{language_settings::AllLanguageSettings, AutoindentMode, Buffer}; + use settings::SettingsStore; + use std::num::NonZeroU32; #[gpui::test] async fn test_c_autoindent(cx: &mut TestAppContext) { cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX); cx.update(|cx| { - let mut settings = Settings::test(cx); - settings.editor_overrides.tab_size = Some(2.try_into().unwrap()); - cx.set_global(settings); + cx.set_global(SettingsStore::test(cx)); + language::init(cx); + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, |s| { + s.defaults.tab_size = NonZeroU32::new(2); + }); + }); }); let language = crate::languages::language("c", tree_sitter_c::language(), None).await; diff --git a/crates/zed/src/languages/json.rs b/crates/zed/src/languages/json.rs index d87d36abfe..406d54cc03 100644 --- a/crates/zed/src/languages/json.rs +++ b/crates/zed/src/languages/json.rs @@ -6,7 +6,7 @@ use gpui::AppContext; use language::{LanguageRegistry, LanguageServerBinary, LanguageServerName, LspAdapter}; use node_runtime::NodeRuntime; use serde_json::json; -use settings::{keymap_file_json_schema, settings_file_json_schema}; +use settings::{keymap_file_json_schema, SettingsJsonSchemaParams, SettingsStore}; use smol::fs; use staff_mode::StaffMode; use std::{ @@ -16,7 +16,6 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; -use theme::ThemeRegistry; use util::http::HttpClient; use util::{paths, ResultExt}; @@ -30,20 +29,11 @@ fn server_binary_arguments(server_path: &Path) -> Vec { pub struct JsonLspAdapter { node: Arc, languages: Arc, - themes: Arc, } impl JsonLspAdapter { - pub fn new( - node: Arc, - languages: Arc, - themes: Arc, - ) -> Self { - JsonLspAdapter { - node, - languages, - themes, - } + pub fn new(node: Arc, languages: Arc) -> Self { + JsonLspAdapter { node, languages } } } @@ -128,12 +118,15 @@ impl LspAdapter for JsonLspAdapter { cx: &mut AppContext, ) -> Option> { let action_names = cx.all_action_names().collect::>(); - let theme_names = self - .themes - .list(**cx.default_global::()) - .map(|meta| meta.name) - .collect(); - let language_names = self.languages.language_names(); + let staff_mode = cx.default_global::().0; + let language_names = &self.languages.language_names(); + let settings_schema = cx.global::().json_schema( + &SettingsJsonSchemaParams { + language_names, + staff_mode, + }, + cx, + ); Some( future::ready(serde_json::json!({ "json": { @@ -143,7 +136,7 @@ impl LspAdapter for JsonLspAdapter { "schemas": [ { "fileMatch": [schema_file_match(&paths::SETTINGS)], - "schema": settings_file_json_schema(theme_names, &language_names), + "schema": settings_schema, }, { "fileMatch": [schema_file_match(&paths::KEYMAP)], diff --git a/crates/zed/src/languages/python.rs b/crates/zed/src/languages/python.rs index acd31e8205..7aaddf5fe8 100644 --- a/crates/zed/src/languages/python.rs +++ b/crates/zed/src/languages/python.rs @@ -170,8 +170,9 @@ impl LspAdapter for PythonLspAdapter { #[cfg(test)] mod tests { use gpui::{ModelContext, TestAppContext}; - use language::{AutoindentMode, Buffer}; - use settings::Settings; + use language::{language_settings::AllLanguageSettings, AutoindentMode, Buffer}; + use settings::SettingsStore; + use std::num::NonZeroU32; #[gpui::test] async fn test_python_autoindent(cx: &mut TestAppContext) { @@ -179,9 +180,13 @@ mod tests { let language = crate::languages::language("python", tree_sitter_python::language(), None).await; cx.update(|cx| { - let mut settings = Settings::test(cx); - settings.editor_overrides.tab_size = Some(2.try_into().unwrap()); - cx.set_global(settings); + cx.set_global(SettingsStore::test(cx)); + language::init(cx); + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, |s| { + s.defaults.tab_size = NonZeroU32::new(2); + }); + }); }); cx.add_model(|cx| { diff --git a/crates/zed/src/languages/rust.rs b/crates/zed/src/languages/rust.rs index 92fb5bc3b2..15700ec80a 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/zed/src/languages/rust.rs @@ -253,10 +253,13 @@ impl LspAdapter for RustLspAdapter { #[cfg(test)] mod tests { + use std::num::NonZeroU32; + use super::*; use crate::languages::language; use gpui::{color::Color, TestAppContext}; - use settings::Settings; + use language::language_settings::AllLanguageSettings; + use settings::SettingsStore; use theme::SyntaxTheme; #[gpui::test] @@ -435,9 +438,13 @@ mod tests { async fn test_rust_autoindent(cx: &mut TestAppContext) { cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX); cx.update(|cx| { - let mut settings = Settings::test(cx); - settings.editor_overrides.tab_size = Some(2.try_into().unwrap()); - cx.set_global(settings); + cx.set_global(SettingsStore::test(cx)); + language::init(cx); + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, |s| { + s.defaults.tab_size = NonZeroU32::new(2); + }); + }); }); let language = crate::languages::language("rust", tree_sitter_rust::language(), None).await; diff --git a/crates/zed/src/languages/yaml.rs b/crates/zed/src/languages/yaml.rs index fed76cd5b9..bd5f2b4c02 100644 --- a/crates/zed/src/languages/yaml.rs +++ b/crates/zed/src/languages/yaml.rs @@ -2,10 +2,11 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use futures::{future::BoxFuture, FutureExt, StreamExt}; use gpui::AppContext; -use language::{LanguageServerBinary, LanguageServerName, LspAdapter}; +use language::{ + language_settings::language_settings, LanguageServerBinary, LanguageServerName, LspAdapter, +}; use node_runtime::NodeRuntime; use serde_json::Value; -use settings::Settings; use smol::fs; use std::{ any::Any, @@ -100,14 +101,13 @@ impl LspAdapter for YamlLspAdapter { } fn workspace_configuration(&self, cx: &mut AppContext) -> Option> { - let settings = cx.global::(); Some( future::ready(serde_json::json!({ "yaml": { "keyOrdering": false }, "[yaml]": { - "editor.tabSize": settings.tab_size(Some("YAML")) + "editor.tabSize": language_settings(Some("YAML"), cx).tab_size, } })) .boxed(), diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 362b714a92..c05efd3f02 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -6,55 +6,61 @@ use assets::Assets; use backtrace::Backtrace; use cli::{ ipc::{self, IpcSender}, - CliRequest, CliResponse, IpcHandshake, + CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME, }; -use client::{self, 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::Editor; +use editor::{scroll::autoscroll::Autoscroll, Editor}; use futures::{ channel::{mpsc, oneshot}, FutureExt, SinkExt, StreamExt, }; use gpui::{Action, App, AppContext, AssetSource, AsyncAppContext, Task, ViewContext}; use isahc::{config::Configurable, Request}; -use language::LanguageRegistry; +use language::{LanguageRegistry, Point}; use log::LevelFilter; use node_runtime::NodeRuntime; use parking_lot::Mutex; use project::Fs; use serde::{Deserialize, Serialize}; -use settings::{ - self, settings_file::SettingsFile, KeymapFileContent, Settings, SettingsFileContent, - WorkingDirectory, -}; +use settings::{default_settings, handle_settings_file_changes, watch_config_file, SettingsStore}; use simplelog::ConfigBuilder; use smol::process::Command; use std::{ + collections::HashMap, env, ffi::OsStr, fs::OpenOptions, io::Write as _, os::unix::prelude::OsStrExt, panic, - path::PathBuf, - sync::{Arc, Weak}, + path::{Path, PathBuf}, + str, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, Weak, + }, thread, time::Duration, }; -use terminal_view::{get_working_directory, TerminalView}; -use util::http::{self, HttpClient}; +use sum_tree::Bias; +use terminal_view::{get_working_directory, TerminalSettings, TerminalView}; +use util::{ + http::{self, HttpClient}, + paths::PathLikeWithPosition, +}; use welcome::{show_welcome_experience, FIRST_OPEN}; use fs::RealFs; -use settings::watched_json::WatchedJsonFile; #[cfg(debug_assertions)] use staff_mode::StaffMode; -use theme::ThemeRegistry; use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt}; use workspace::{ item::ItemHandle, notifications::NotifyResultExt, AppState, OpenSettings, Workspace, }; -use zed::{self, build_window_options, initialize_workspace, languages, menus}; +use zed::{ + self, build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus, +}; fn main() { let http = http::client(); @@ -74,10 +80,10 @@ fn main() { load_embedded_fonts(&app); let fs = Arc::new(RealFs); - - let themes = ThemeRegistry::new(Assets, app.font_cache()); - let default_settings = Settings::defaults(Assets, &app.font_cache(), &themes); - let config_files = load_config_files(&app, fs.clone()); + let user_settings_file_rx = + watch_config_file(app.background(), fs.clone(), paths::SETTINGS.clone()); + let user_keymap_file_rx = + watch_config_file(app.background(), fs.clone(), paths::KEYMAP.clone()); let login_shell_env_loaded = if stdout_is_a_pty() { Task::ready(()) @@ -88,29 +94,17 @@ fn main() { }; let (cli_connections_tx, mut cli_connections_rx) = mpsc::unbounded(); + let cli_connections_tx = Arc::new(cli_connections_tx); let (open_paths_tx, mut open_paths_rx) = mpsc::unbounded(); + let open_paths_tx = Arc::new(open_paths_tx); + let urls_callback_triggered = Arc::new(AtomicBool::new(false)); + + let callback_cli_connections_tx = Arc::clone(&cli_connections_tx); + let callback_open_paths_tx = Arc::clone(&open_paths_tx); + let callback_urls_callback_triggered = Arc::clone(&urls_callback_triggered); app.on_open_urls(move |urls, _| { - if let Some(server_name) = urls.first().and_then(|url| url.strip_prefix("zed-cli://")) { - if let Some(cli_connection) = connect_to_cli(server_name).log_err() { - cli_connections_tx - .unbounded_send(cli_connection) - .map_err(|_| anyhow!("no listener for cli connections")) - .log_err(); - }; - } else { - let paths: Vec<_> = urls - .iter() - .flat_map(|url| url.strip_prefix("file://")) - .map(|url| { - let decoded = urlencoding::decode_binary(url.as_bytes()); - PathBuf::from(OsStr::from_bytes(decoded.as_ref())) - }) - .collect(); - open_paths_tx - .unbounded_send(paths) - .map_err(|_| anyhow!("no listener for open urls requests")) - .log_err(); - } + callback_urls_callback_triggered.store(true, Ordering::Release); + open_urls(urls, &callback_cli_connections_tx, &callback_open_paths_tx); }) .on_reopen(move |cx| { if cx.has_global::>() { @@ -129,26 +123,13 @@ fn main() { #[cfg(debug_assertions)] cx.set_global(StaffMode(true)); - let (settings_file_content, keymap_file) = cx.background().block(config_files).unwrap(); - - //Setup settings global before binding actions - cx.set_global(SettingsFile::new( - &paths::SETTINGS, - settings_file_content.clone(), - fs.clone(), - )); - - settings::watch_files( - default_settings, - settings_file_content, - themes.clone(), - keymap_file, - cx, - ); - - if !stdout_is_a_pty() { - upload_previous_panics(http.clone(), cx); - } + let mut store = SettingsStore::default(); + store + .set_default_settings(default_settings().as_ref(), cx) + .unwrap(); + cx.set_global(store); + handle_settings_file_changes(user_settings_file_rx, cx); + handle_keymap_file_changes(user_keymap_file_rx, cx); let client = client::Client::new(http.clone(), cx); let mut languages = LanguageRegistry::new(login_shell_env_loaded); @@ -157,15 +138,17 @@ fn main() { let languages = Arc::new(languages); let node_runtime = NodeRuntime::new(http.clone(), cx.background().to_owned()); - languages::init(languages.clone(), themes.clone(), node_runtime.clone()); + languages::init(languages.clone(), node_runtime.clone()); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); cx.set_global(client.clone()); + theme::init(Assets, cx); context_menu::init(cx); - project::Project::init(&client); - client::init(client.clone(), cx); + project::Project::init(&client, cx); + client::init(&client, cx); command_palette::init(cx); + language::init(cx); editor::init(cx); go_to_line::init(cx); file_finder::init(cx); @@ -179,13 +162,12 @@ fn main() { theme_testbench::init(cx); copilot::init(http.clone(), node_runtime, cx); - cx.spawn(|cx| watch_themes(fs.clone(), themes.clone(), cx)) - .detach(); + cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach(); - languages.set_theme(cx.global::().theme.clone()); - cx.observe_global::({ + languages.set_theme(theme::current(cx).clone()); + cx.observe_global::({ let languages = languages.clone(); - move |cx| languages.set_theme(cx.global::().theme.clone()) + move |cx| languages.set_theme(theme::current(cx).clone()) }) .detach(); @@ -193,12 +175,11 @@ fn main() { client.telemetry().report_mixpanel_event( "start app", Default::default(), - cx.global::().telemetry(), + *settings::get::(cx), ); let app_state = Arc::new(AppState { languages, - themes, client: client.clone(), user_store, fs, @@ -207,7 +188,7 @@ fn main() { background_actions, }); cx.set_global(Arc::downgrade(&app_state)); - auto_update::init(http, client::ZED_SERVER_URL.clone(), cx); + auto_update::init(http.clone(), client::ZED_SERVER_URL.clone(), cx); workspace::init(app_state.clone(), cx); recent_projects::init(cx); @@ -215,10 +196,13 @@ fn main() { journal::init(app_state.clone(), cx); language_selector::init(cx); theme_selector::init(cx); - zed::init(&app_state, cx); + activity_indicator::init(cx); + lsp_log::init(cx); + call::init(app_state.client.clone(), app_state.user_store.clone(), cx); collab_ui::init(&app_state, cx); feedback::init(cx); welcome::init(cx); + zed::init(&app_state, cx); cx.set_menus(menus::menus()); @@ -232,6 +216,16 @@ fn main() { workspace::open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx); } } else { + upload_previous_panics(http.clone(), cx); + + // TODO Development mode that forces the CLI mode usually runs Zed binary as is instead + // of an *app, hence gets no specific callbacks run. Emulate them here, if needed. + if std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_some() + && !urls_callback_triggered.load(Ordering::Acquire) + { + open_urls(collect_url_args(), &cli_connections_tx, &open_paths_tx) + } + if let Ok(Some(connection)) = cli_connections_rx.try_next() { cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx)) .detach(); @@ -282,6 +276,37 @@ fn main() { }); } +fn open_urls( + urls: Vec, + cli_connections_tx: &mpsc::UnboundedSender<( + mpsc::Receiver, + IpcSender, + )>, + open_paths_tx: &mpsc::UnboundedSender>, +) { + if let Some(server_name) = urls.first().and_then(|url| url.strip_prefix("zed-cli://")) { + if let Some(cli_connection) = connect_to_cli(server_name).log_err() { + cli_connections_tx + .unbounded_send(cli_connection) + .map_err(|_| anyhow!("no listener for cli connections")) + .log_err(); + }; + } else { + let paths: Vec<_> = urls + .iter() + .flat_map(|url| url.strip_prefix("file://")) + .map(|url| { + let decoded = urlencoding::decode_binary(url.as_bytes()); + PathBuf::from(OsStr::from_bytes(decoded.as_ref())) + }) + .collect(); + open_paths_tx + .unbounded_send(paths) + .map_err(|_| anyhow!("no listener for open urls requests")) + .log_err(); + } +} + async fn restore_or_create_workspace(app_state: &Arc, mut cx: AsyncAppContext) { if let Some(location) = workspace::last_opened_workspace_paths().await { cx.update(|cx| workspace::open_paths(location.paths().as_ref(), app_state, None, cx)) @@ -412,7 +437,7 @@ fn init_panic_hook(app_version: String) { } fn upload_previous_panics(http: Arc, cx: &mut AppContext) { - let diagnostics_telemetry = cx.global::().telemetry_diagnostics(); + let telemetry_settings = *settings::get::(cx); cx.background() .spawn({ @@ -442,7 +467,7 @@ fn upload_previous_panics(http: Arc, cx: &mut AppContext) { continue; }; - if diagnostics_telemetry { + if telemetry_settings.diagnostics { let panic_data_text = smol::fs::read_to_string(&child_path) .await .context("error reading panic file")?; @@ -512,7 +537,8 @@ async fn load_login_shell_environment() -> Result<()> { } fn stdout_is_a_pty() -> bool { - unsafe { libc::isatty(libc::STDOUT_FILENO as i32) != 0 } + std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_none() + && unsafe { libc::isatty(libc::STDOUT_FILENO as i32) != 0 } } fn collect_path_args() -> Vec { @@ -525,7 +551,11 @@ fn collect_path_args() -> Vec { None } }) - .collect::>() + .collect() +} + +fn collect_url_args() -> Vec { + env::args().skip(1).collect() } fn load_embedded_fonts(app: &App) { @@ -547,11 +577,7 @@ fn load_embedded_fonts(app: &App) { } #[cfg(debug_assertions)] -async fn watch_themes( - fs: Arc, - themes: Arc, - mut cx: AsyncAppContext, -) -> Option<()> { +async fn watch_themes(fs: Arc, mut cx: AsyncAppContext) -> Option<()> { let mut events = fs .watch("styles/src".as_ref(), Duration::from_millis(100)) .await; @@ -563,7 +589,7 @@ async fn watch_themes( .await .log_err()?; if output.status.success() { - cx.update(|cx| theme_selector::reload(themes.clone(), cx)) + cx.update(|cx| theme_selector::reload(cx)) } else { eprintln!( "build script failed {}", @@ -575,35 +601,10 @@ async fn watch_themes( } #[cfg(not(debug_assertions))] -async fn watch_themes( - _fs: Arc, - _themes: Arc, - _cx: AsyncAppContext, -) -> Option<()> { +async fn watch_themes(_fs: Arc, _cx: AsyncAppContext) -> Option<()> { None } -fn load_config_files( - app: &App, - fs: Arc, -) -> oneshot::Receiver<( - WatchedJsonFile, - WatchedJsonFile, -)> { - let executor = app.background(); - let (tx, rx) = oneshot::channel(); - executor - .clone() - .spawn(async move { - let settings_file = - WatchedJsonFile::new(fs.clone(), &executor, paths::SETTINGS.clone()).await; - let keymap_file = WatchedJsonFile::new(fs, &executor, paths::KEYMAP.clone()).await; - tx.send((settings_file, keymap_file)).ok() - }) - .detach(); - rx -} - fn connect_to_cli( server_name: &str, ) -> Result<(mpsc::Receiver, IpcSender)> { @@ -641,13 +642,38 @@ async fn handle_cli_connection( if let Some(request) = requests.next().await { match request { CliRequest::Open { paths, wait } => { + let mut caret_positions = HashMap::new(); + let paths = if paths.is_empty() { workspace::last_opened_workspace_paths() .await .map(|location| location.paths().to_vec()) - .unwrap_or(paths) + .unwrap_or_default() } else { paths + .into_iter() + .filter_map(|path_with_position_string| { + let path_with_position = PathLikeWithPosition::parse_str( + &path_with_position_string, + |path_str| { + Ok::<_, std::convert::Infallible>( + Path::new(path_str).to_path_buf(), + ) + }, + ) + .expect("Infallible"); + let path = path_with_position.path_like; + if let Some(row) = path_with_position.row { + if path.is_file() { + let row = row.saturating_sub(1); + let col = + path_with_position.column.unwrap_or(0).saturating_sub(1); + caret_positions.insert(path.clone(), Point::new(row, col)); + } + } + Some(path) + }) + .collect() }; let mut errored = false; @@ -657,11 +683,32 @@ async fn handle_cli_connection( { Ok((workspace, items)) => { let mut item_release_futures = Vec::new(); - cx.update(|cx| { - for (item, path) in items.into_iter().zip(&paths) { - match item { - Some(Ok(item)) => { - let released = oneshot::channel(); + + for (item, path) in items.into_iter().zip(&paths) { + match item { + Some(Ok(item)) => { + if let Some(point) = caret_positions.remove(path) { + if let Some(active_editor) = item.downcast::() { + active_editor + .downgrade() + .update(&mut cx, |editor, cx| { + let snapshot = + editor.snapshot(cx).display_snapshot; + let point = snapshot + .buffer_snapshot + .clip_point(point, Bias::Left); + editor.change_selections( + Some(Autoscroll::center()), + cx, + |s| s.select_ranges([point..point]), + ); + }) + .log_err(); + } + } + + let released = oneshot::channel(); + cx.update(|cx| { item.on_release( cx, Box::new(move |_| { @@ -669,23 +716,20 @@ async fn handle_cli_connection( }), ) .detach(); - item_release_futures.push(released.1); - } - Some(Err(err)) => { - responses - .send(CliResponse::Stderr { - message: format!( - "error opening {:?}: {}", - path, err - ), - }) - .log_err(); - errored = true; - } - None => {} + }); + item_release_futures.push(released.1); } + Some(Err(err)) => { + responses + .send(CliResponse::Stderr { + message: format!("error opening {:?}: {}", path, err), + }) + .log_err(); + errored = true; + } + None => {} } - }); + } if wait { let background = cx.background(); @@ -748,13 +792,9 @@ pub fn dock_default_item_factory( workspace: &mut Workspace, cx: &mut ViewContext, ) -> Option> { - let strategy = cx - .global::() - .terminal_overrides + let strategy = settings::get::(cx) .working_directory - .clone() - .unwrap_or(WorkingDirectory::CurrentProjectDirectory); - + .clone(); let working_directory = get_working_directory(workspace, cx, strategy); let window_id = cx.window_id(); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 77a3bf350f..0c4b63081c 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -15,7 +15,7 @@ use anyhow::anyhow; use feedback::{ feedback_info_text::FeedbackInfoText, submit_feedback_button::SubmitFeedbackButton, }; -use futures::StreamExt; +use futures::{channel::mpsc, StreamExt}; use gpui::{ actions, anyhow::{self, Result}, @@ -30,15 +30,16 @@ use project_panel::ProjectPanel; use search::{BufferSearchBar, ProjectSearchBar}; use serde::Deserialize; use serde_json::to_string_pretty; -use settings::{Settings, DEFAULT_SETTINGS_ASSET_PATH}; +use settings::{KeymapFileContent, SettingsStore, DEFAULT_SETTINGS_ASSET_PATH}; use std::{borrow::Cow, str, sync::Arc}; use terminal_view::terminal_panel::{self, TerminalPanel}; use util::{channel::ReleaseChannel, paths, ResultExt}; use uuid::Uuid; +use welcome::BaseKeymap; pub use workspace; use workspace::{ create_and_open_local_file, dock::PanelHandle, open_new, AppState, NewFile, NewWindow, - Workspace, + Workspace, WorkspaceSettings, }; #[derive(Deserialize, Clone, PartialEq)] @@ -73,8 +74,6 @@ actions!( ] ); -const MIN_FONT_SIZE: f32 = 6.0; - pub fn init(app_state: &Arc, cx: &mut gpui::AppContext) { cx.add_action(about); cx.add_global_action(|_: &Hide, cx: &mut gpui::AppContext| { @@ -118,30 +117,12 @@ pub fn init(app_state: &Arc, cx: &mut gpui::AppContext) { cx.add_global_action(quit); cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url)); cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| { - cx.update_global::(|settings, cx| { - settings.buffer_font_size = (settings.buffer_font_size + 1.0).max(MIN_FONT_SIZE); - if let Some(terminal_font_size) = settings.terminal_overrides.font_size.as_mut() { - *terminal_font_size = (*terminal_font_size + 1.0).max(MIN_FONT_SIZE); - } - cx.refresh_windows(); - }); + theme::adjust_font_size(cx, |size| *size += 1.0) }); cx.add_global_action(move |_: &DecreaseBufferFontSize, cx| { - cx.update_global::(|settings, cx| { - settings.buffer_font_size = (settings.buffer_font_size - 1.0).max(MIN_FONT_SIZE); - if let Some(terminal_font_size) = settings.terminal_overrides.font_size.as_mut() { - *terminal_font_size = (*terminal_font_size - 1.0).max(MIN_FONT_SIZE); - } - cx.refresh_windows(); - }); - }); - cx.add_global_action(move |_: &ResetBufferFontSize, cx| { - cx.update_global::(|settings, cx| { - settings.buffer_font_size = settings.default_buffer_font_size; - settings.terminal_overrides.font_size = settings.terminal_defaults.font_size; - cx.refresh_windows(); - }); + theme::adjust_font_size(cx, |size| *size -= 1.0) }); + cx.add_global_action(move |_: &ResetBufferFontSize, cx| theme::reset_font_size(cx)); cx.add_global_action(move |_: &install_cli::Install, cx| { cx.spawn(|cx| async move { install_cli::install_cli(&cx) @@ -275,10 +256,7 @@ pub fn init(app_state: &Arc, cx: &mut gpui::AppContext) { } } }); - activity_indicator::init(cx); - lsp_log::init(cx); - call::init(app_state.client.clone(), app_state.user_store.clone(), cx); - settings::KeymapFileContent::load_defaults(cx); + load_default_keymap(cx); } pub fn initialize_workspace( @@ -323,7 +301,8 @@ pub fn initialize_workspace( cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx)); workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx); - let copilot = cx.add_view(|cx| copilot_button::CopilotButton::new(cx)); + let copilot = + cx.add_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx)); let diagnostic_summary = cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx)); let activity_indicator = activity_indicator::ActivityIndicator::new( @@ -408,7 +387,7 @@ pub fn build_window_options( } fn quit(_: &Quit, cx: &mut gpui::AppContext) { - let should_confirm = cx.global::().confirm_quit; + let should_confirm = settings::get::(cx).confirm_quit; cx.spawn(|mut cx| async move { let mut workspaces = cx .window_ids() @@ -519,6 +498,51 @@ fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext) { .detach(); } +pub fn load_default_keymap(cx: &mut AppContext) { + for path in ["keymaps/default.json", "keymaps/vim.json"] { + KeymapFileContent::load_asset(path, cx).unwrap(); + } + + if let Some(asset_path) = settings::get::(cx).asset_path() { + KeymapFileContent::load_asset(asset_path, cx).unwrap(); + } +} + +pub fn handle_keymap_file_changes( + mut user_keymap_file_rx: mpsc::UnboundedReceiver, + cx: &mut AppContext, +) { + cx.spawn(move |mut cx| async move { + let mut settings_subscription = None; + while let Some(user_keymap_content) = user_keymap_file_rx.next().await { + if let Ok(keymap_content) = KeymapFileContent::parse(&user_keymap_content) { + cx.update(|cx| { + cx.clear_bindings(); + load_default_keymap(cx); + keymap_content.clone().add_to_cx(cx).log_err(); + }); + + let mut old_base_keymap = cx.read(|cx| *settings::get::(cx)); + drop(settings_subscription); + settings_subscription = Some(cx.update(|cx| { + cx.observe_global::(move |cx| { + let new_base_keymap = *settings::get::(cx); + if new_base_keymap != old_base_keymap { + old_base_keymap = new_base_keymap.clone(); + + cx.clear_bindings(); + load_default_keymap(cx); + keymap_content.clone().add_to_cx(cx).log_err(); + } + }) + .detach(); + })); + } + } + }) + .detach(); +} + fn open_telemetry_log_file(workspace: &mut Workspace, cx: &mut ViewContext) { workspace.with_local_workspace(cx, move |workspace, cx| { let app_state = workspace.app_state().clone(); @@ -620,16 +644,21 @@ mod tests { use super::*; use assets::Assets; use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor}; - use gpui::{executor::Deterministic, AppContext, AssetSource, TestAppContext, ViewHandle}; + use fs::{FakeFs, Fs}; + use gpui::{ + elements::Empty, executor::Deterministic, Action, AnyElement, AppContext, AssetSource, + Element, Entity, TestAppContext, View, ViewHandle, + }; use language::LanguageRegistry; use node_runtime::NodeRuntime; use project::{Project, ProjectPath}; use serde_json::json; + use settings::{handle_settings_file_changes, watch_config_file, SettingsStore}; use std::{ collections::HashSet, path::{Path, PathBuf}, }; - use theme::ThemeRegistry; + use theme::{ThemeRegistry, ThemeSettings}; use util::http::FakeHttpClient; use workspace::{ item::{Item, ItemHandle}, @@ -638,7 +667,7 @@ mod tests { #[gpui::test] async fn test_open_paths_action(cx: &mut TestAppContext) { - let app_state = init(cx); + let app_state = init_test(cx); app_state .fs .as_fake() @@ -738,7 +767,7 @@ mod tests { #[gpui::test] async fn test_window_edit_state(executor: Arc, cx: &mut TestAppContext) { - let app_state = init(cx); + let app_state = init_test(cx); app_state .fs .as_fake() @@ -819,7 +848,7 @@ mod tests { #[gpui::test] async fn test_new_empty_workspace(cx: &mut TestAppContext) { - let app_state = init(cx); + let app_state = init_test(cx); cx.update(|cx| { open_new(&app_state, cx, |workspace, cx| { Editor::new_file(workspace, &Default::default(), cx) @@ -858,7 +887,7 @@ mod tests { #[gpui::test] async fn test_open_entry(cx: &mut TestAppContext) { - let app_state = init(cx); + let app_state = init_test(cx); app_state .fs .as_fake() @@ -971,7 +1000,7 @@ mod tests { #[gpui::test] async fn test_open_paths(cx: &mut TestAppContext) { - let app_state = init(cx); + let app_state = init_test(cx); app_state .fs @@ -1141,7 +1170,7 @@ mod tests { #[gpui::test] async fn test_save_conflicting_item(cx: &mut TestAppContext) { - let app_state = init(cx); + let app_state = init_test(cx); app_state .fs .as_fake() @@ -1185,7 +1214,7 @@ mod tests { #[gpui::test] async fn test_open_and_save_new_file(cx: &mut TestAppContext) { - let app_state = init(cx); + let app_state = init_test(cx); app_state.fs.create_dir(Path::new("/root")).await.unwrap(); let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; @@ -1274,7 +1303,7 @@ mod tests { #[gpui::test] async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) { - let app_state = init(cx); + let app_state = init_test(cx); app_state.fs.create_dir(Path::new("/root")).await.unwrap(); let project = Project::test(app_state.fs.clone(), [], cx).await; @@ -1313,9 +1342,7 @@ mod tests { #[gpui::test] async fn test_pane_actions(cx: &mut TestAppContext) { - init(cx); - - let app_state = cx.update(AppState::test); + let app_state = init_test(cx); app_state .fs .as_fake() @@ -1389,7 +1416,7 @@ mod tests { #[gpui::test] async fn test_navigation(cx: &mut TestAppContext) { - let app_state = init(cx); + let app_state = init_test(cx); app_state .fs .as_fake() @@ -1665,7 +1692,7 @@ mod tests { #[gpui::test] async fn test_reopening_closed_items(cx: &mut TestAppContext) { - let app_state = init(cx); + let app_state = init_test(cx); app_state .fs .as_fake() @@ -1828,6 +1855,175 @@ mod tests { } } + #[gpui::test] + async fn test_base_keymap(cx: &mut gpui::TestAppContext) { + struct TestView; + + impl Entity for TestView { + type Event = (); + } + + impl View for TestView { + fn ui_name() -> &'static str { + "TestView" + } + + fn render(&mut self, _: &mut ViewContext) -> AnyElement { + Empty::new().into_any() + } + } + + let executor = cx.background(); + let fs = FakeFs::new(executor.clone()); + + actions!(test, [A, B]); + // From the Atom keymap + actions!(workspace, [ActivatePreviousPane]); + // From the JetBrains keymap + actions!(pane, [ActivatePrevItem]); + + fs.save( + "/settings.json".as_ref(), + &r#" + { + "base_keymap": "Atom" + } + "# + .into(), + Default::default(), + ) + .await + .unwrap(); + + fs.save( + "/keymap.json".as_ref(), + &r#" + [ + { + "bindings": { + "backspace": "test::A" + } + } + ] + "# + .into(), + Default::default(), + ) + .await + .unwrap(); + + cx.update(|cx| { + cx.set_global(SettingsStore::test(cx)); + theme::init(Assets, cx); + welcome::init(cx); + + cx.add_global_action(|_: &A, _cx| {}); + cx.add_global_action(|_: &B, _cx| {}); + cx.add_global_action(|_: &ActivatePreviousPane, _cx| {}); + cx.add_global_action(|_: &ActivatePrevItem, _cx| {}); + + let settings_rx = watch_config_file( + executor.clone(), + fs.clone(), + PathBuf::from("/settings.json"), + ); + let keymap_rx = + watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json")); + + handle_keymap_file_changes(keymap_rx, cx); + handle_settings_file_changes(settings_rx, cx); + }); + + cx.foreground().run_until_parked(); + + let (window_id, _view) = cx.add_window(|_| TestView); + + // Test loading the keymap base at all + assert_key_bindings_for( + window_id, + cx, + vec![("backspace", &A), ("k", &ActivatePreviousPane)], + line!(), + ); + + // Test modifying the users keymap, while retaining the base keymap + fs.save( + "/keymap.json".as_ref(), + &r#" + [ + { + "bindings": { + "backspace": "test::B" + } + } + ] + "# + .into(), + Default::default(), + ) + .await + .unwrap(); + + cx.foreground().run_until_parked(); + + assert_key_bindings_for( + window_id, + cx, + vec![("backspace", &B), ("k", &ActivatePreviousPane)], + line!(), + ); + + // Test modifying the base, while retaining the users keymap + fs.save( + "/settings.json".as_ref(), + &r#" + { + "base_keymap": "JetBrains" + } + "# + .into(), + Default::default(), + ) + .await + .unwrap(); + + cx.foreground().run_until_parked(); + + assert_key_bindings_for( + window_id, + cx, + vec![("backspace", &B), ("[", &ActivatePrevItem)], + line!(), + ); + + fn assert_key_bindings_for<'a>( + window_id: usize, + cx: &TestAppContext, + actions: Vec<(&'static str, &'a dyn Action)>, + line: u32, + ) { + for (key, action) in actions { + // assert that... + assert!( + cx.available_actions(window_id, 0) + .into_iter() + .any(|(_, bound_action, b)| { + // action names match... + bound_action.name() == action.name() + && bound_action.namespace() == action.namespace() + // and key strokes contain the given key + && b.iter() + .any(|binding| binding.keystrokes().iter().any(|k| k.key == key)) + }), + "On {} Failed to find {} with key binding {}", + line, + action.name(), + key + ); + } + } + } + #[gpui::test] fn test_bundled_settings_and_themes(cx: &mut AppContext) { cx.platform() @@ -1846,15 +2042,20 @@ mod tests { ]) .unwrap(); let themes = ThemeRegistry::new(Assets, cx.font_cache().clone()); - let settings = Settings::defaults(Assets, cx.font_cache(), &themes); + let mut settings = SettingsStore::default(); + settings + .set_default_settings(&settings::default_settings(), cx) + .unwrap(); + cx.set_global(settings); + theme::init(Assets, cx); let mut has_default_theme = false; for theme_name in themes.list(false).map(|meta| meta.name) { let theme = themes.get(&theme_name).unwrap(); - if theme.meta.name == settings.theme.meta.name { + assert_eq!(theme.meta.name, theme_name); + if theme.meta.name == settings::get::(cx).theme.meta.name { has_default_theme = true; } - assert_eq!(theme.meta.name, theme_name); } assert!(has_default_theme); } @@ -1864,25 +2065,26 @@ mod tests { let mut languages = LanguageRegistry::test(); languages.set_executor(cx.background().clone()); let languages = Arc::new(languages); - let themes = ThemeRegistry::new((), cx.font_cache().clone()); let http = FakeHttpClient::with_404_response(); let node_runtime = NodeRuntime::new(http, cx.background().to_owned()); - languages::init(languages.clone(), themes, node_runtime); + languages::init(languages.clone(), node_runtime); for name in languages.language_names() { languages.language_for_name(&name); } cx.foreground().run_until_parked(); } - fn init(cx: &mut TestAppContext) -> Arc { + fn init_test(cx: &mut TestAppContext) -> Arc { cx.foreground().forbid_parking(); cx.update(|cx| { let mut app_state = AppState::test(cx); let state = Arc::get_mut(&mut app_state).unwrap(); state.initialize_workspace = initialize_workspace; state.build_window_options = build_window_options; + theme::init((), cx); call::init(app_state.client.clone(), app_state.user_store.clone(), cx); workspace::init(app_state.clone(), cx); + language::init(cx); editor::init(cx); pane::init(cx); app_state diff --git a/script/clear-target-dir-if-larger-than b/script/clear-target-dir-if-larger-than new file mode 100755 index 0000000000..59c07f77f7 --- /dev/null +++ b/script/clear-target-dir-if-larger-than @@ -0,0 +1,20 @@ +#!/bin/bash + +set -eu + +if [[ $# < 1 ]]; then + echo "usage: $0 " + exit 1 +fi + +max_size_gb=$1 + +current_size=$(du -s target | cut -f1) +current_size_gb=$(expr ${current_size} / 1024 / 1024) + +echo "target directory size: ${current_size_gb}gb. max size: ${max_size_gb}gb" + +if [[ ${current_size_gb} -gt ${max_size_gb} ]]; then + echo "clearing target directory" + rm -rf target +fi diff --git a/script/get-preview-channel-changes b/script/get-preview-channel-changes index ac1dcb5b6e..47623125f9 100755 --- a/script/get-preview-channel-changes +++ b/script/get-preview-channel-changes @@ -2,8 +2,8 @@ const { execFileSync } = require("child_process"); const { GITHUB_ACCESS_TOKEN } = process.env; -const PR_REGEX = /pull request #(\d+)/; -const FIXES_REGEX = /(fixes|closes) (.+[/#]\d+.*)$/im; +const PR_REGEX = /#\d+/ // Ex: matches on #4241 +const FIXES_REGEX = /(fixes|closes|completes) (.+[/#]\d+.*)$/im; main(); @@ -15,7 +15,7 @@ async function main() { { encoding: "utf8" } ) .split("\n") - .filter((t) => t.startsWith("v") && t.endsWith('-pre')); + .filter((t) => t.startsWith("v") && t.endsWith("-pre")); // Print the previous release console.log(`Changes from ${oldTag} to ${newTag}\n`); @@ -34,42 +34,16 @@ async function main() { } // Get the PRs merged between those two tags. - const pullRequestNumbers = execFileSync( - "git", - [ - "log", - `${oldTag}..${newTag}`, - "--oneline", - "--grep", - "Merge pull request", - ], - { encoding: "utf8" } - ) - .split("\n") - .filter((line) => line.length > 0) - .map((line) => line.match(PR_REGEX)[1]); + const pullRequestNumbers = getPullRequestNumbers(oldTag, newTag); // Get the PRs that were cherry-picked between main and the old tag. - const existingPullRequestNumbers = new Set(execFileSync( - "git", - [ - "log", - `main..${oldTag}`, - "--oneline", - "--grep", - "Merge pull request", - ], - { encoding: "utf8" } - ) - .split("\n") - .filter((line) => line.length > 0) - .map((line) => line.match(PR_REGEX)[1])); - + const existingPullRequestNumbers = new Set(getPullRequestNumbers("main", oldTag)); + // Filter out those existing PRs from the set of new PRs. const newPullRequestNumbers = pullRequestNumbers.filter(number => !existingPullRequestNumbers.has(number)); // Fetch the pull requests from the GitHub API. - console.log("Merged Pull requests:") + console.log("Merged Pull requests:"); for (const pullRequestNumber of newPullRequestNumbers) { const webURL = `https://github.com/zed-industries/zed/pull/${pullRequestNumber}`; const apiURL = `https://api.github.com/repos/zed-industries/zed/pulls/${pullRequestNumber}`; @@ -83,13 +57,44 @@ async function main() { // Print the pull request title and URL. const pullRequest = await response.json(); console.log("*", pullRequest.title); - console.log(" URL: ", webURL); + console.log(" PR URL: ", webURL); // If the pull request contains a 'closes' line, print the closed issue. - const fixesMatch = (pullRequest.body || '').match(FIXES_REGEX); + const fixesMatch = (pullRequest.body || "").match(FIXES_REGEX); if (fixesMatch) { const fixedIssueURL = fixesMatch[2]; - console.log(" Issue: ", fixedIssueURL); + console.log(" Issue URL: ", fixedIssueURL); } + + let releaseNotes = (pullRequest.body || "").split("Release Notes:")[1]; + + if (releaseNotes) { + releaseNotes = releaseNotes.trim(); + console.log(" Release Notes:"); + console.log(` ${releaseNotes}`); + } + + console.log() } } + +function getPullRequestNumbers(oldTag, newTag) { + const pullRequestNumbers = execFileSync( + "git", + [ + "log", + `${oldTag}..${newTag}`, + "--oneline" + ], + { encoding: "utf8" } + ) + .split("\n") + .filter(line => line.length > 0) + .map(line => { + const match = line.match(/#(\d+)/); + return match ? match[1] : null; + }) + .filter(line => line); + + return pullRequestNumbers; +}