Compare commits

..

1 Commits

Author SHA1 Message Date
Michael Sloan
c69e7499af Navigate to multibuffer headers (WIP)
Issue #5129
2025-02-25 13:26:56 -07:00
233 changed files with 3902 additions and 8480 deletions

View File

@@ -10,39 +10,16 @@ body:
value: |
<!-- Please insert a one line summary of the issue below -->
SUMMARY_SENTENCE_HERE
<!-- Be verbose: Include all steps necessary to reproduce from a clean Zed installation. -->
<!-- Code snippets are better than images, a repository link that reproduces the issue is ideal. -->
<!-- Include all steps necessary to reproduce from a clean Zed installation. Be verbose -->
Steps to trigger the problem:
1.
2.
3.
4.
Actual Behavior:
Expected Behavior:
<!--
Is there anything additional necessary to reproduce this issue?
- settings.json, keymap.json, .editorconfig etc?
- Does it happen intermittently or only with specific projects / file types?
- Have you found a workaround?
Did you check your Zed.log to see if there is any relevant details there?
- When including large items (videos, screenshots, logs, configs) please wrap with:
<details><summary>See inside for XXXXYYY</summary>
```shell
code
```
</details>
-->
validations:
required: true

View File

@@ -109,16 +109,8 @@ jobs:
- name: cargo clippy
run: ./script/clippy
- name: Install cargo-machete
uses: clechasseur/rs-cargo@8435b10f6e71c2e3d4d3b7573003a8ce4bfc6386 # v2
with:
command: install
args: cargo-machete@0.7.0
- name: Check unused dependencies
uses: clechasseur/rs-cargo@8435b10f6e71c2e3d4d3b7573003a8ce4bfc6386 # v2
with:
command: machete
uses: bnjbvr/cargo-machete@main
- name: Check licenses
run: |
@@ -240,7 +232,7 @@ jobs:
timeout-minutes: 60
name: (Windows) Run Clippy and tests
if: github.repository_owner == 'zed-industries'
runs-on: hosted-windows-2
runs-on: hosted-windows-1
steps:
# more info here:- https://github.com/rust-lang/cargo/issues/13020
- name: Enable longer pathnames for git
@@ -273,7 +265,8 @@ jobs:
- name: cargo clippy
working-directory: ${{ env.ZED_WORKSPACE }}
run: ./script/clippy.ps1
# Windows can't run shell scripts, so we need to use `cargo xtask`.
run: cargo xtask clippy
- name: Run tests
uses: ./.github/actions/run_tests_windows
@@ -358,14 +351,14 @@ jobs:
mv target/x86_64-apple-darwin/release/Zed.dmg target/x86_64-apple-darwin/release/Zed-x86_64.dmg
- name: Upload app bundle (aarch64) to workflow run if main branch or specific label
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-aarch64.dmg
path: target/aarch64-apple-darwin/release/Zed-aarch64.dmg
- name: Upload app bundle (x86_64) to workflow run if main branch or specific label
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-x86_64.dmg
@@ -416,7 +409,7 @@ jobs:
run: script/bundle-linux
- name: Upload Linux bundle to workflow run if main branch or specific label
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-linux-gnu.tar.gz
@@ -464,7 +457,7 @@ jobs:
run: script/bundle-linux
- name: Upload Linux bundle to workflow run if main branch or specific label
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-aarch64-unknown-linux-gnu.tar.gz

View File

@@ -65,7 +65,7 @@ jobs:
command: deploy .cloudflare/docs-proxy/src/worker.js
- name: Preserve Wrangler logs
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4
if: always()
with:
name: wrangler_logs

View File

@@ -21,8 +21,6 @@ Andrei Zvonimir Crnković <andrei@0x7f.dev>
Andrei Zvonimir Crnković <andrei@0x7f.dev> <andreicek@0x7f.dev>
Antonio Scandurra <me@as-cii.com>
Antonio Scandurra <me@as-cii.com> <antonio@zed.dev>
Ben Kunkle <ben@zed.dev>
Ben Kunkle <ben@zed.dev> <ben.kunkle@gmail.com>
Bennet Bo Fenner <bennet@zed.dev>
Bennet Bo Fenner <bennet@zed.dev> <53836821+bennetbo@users.noreply.github.com>
Bennet Bo Fenner <bennet@zed.dev> <bennetbo@gmx.de>
@@ -114,8 +112,6 @@ Sebastijan Kelnerič <sebastijan.kelneric@sebba.dev> <sebastijan.kelneric@vichav
Sergey Onufrienko <sergey@onufrienko.com>
Shish <webmaster@shishnet.org>
Shish <webmaster@shishnet.org> <shish@shishnet.org>
Smit Barmase <0xtimsb@gmail.com>
Smit Barmase <0xtimsb@gmail.com> <smit@zed.dev>
Thorben Kröger <dev@thorben.net>
Thorben Kröger <dev@thorben.net> <thorben.kroeger@hexagon.com>
Thorsten Ball <thorsten@zed.dev>

View File

@@ -14,12 +14,12 @@
},
"JSON": {
"tab_size": 2,
"preferred_line_length": 120,
"preferred_line_length": 100,
"formatter": "prettier"
},
"JSONC": {
"tab_size": 2,
"preferred_line_length": 120,
"preferred_line_length": 100,
"formatter": "prettier"
},
"JavaScript": {

208
Cargo.lock generated
View File

@@ -395,6 +395,7 @@ dependencies = [
"language",
"language_model",
"language_model_selector",
"language_models",
"languages",
"log",
"lsp",
@@ -462,6 +463,7 @@ dependencies = [
"language",
"language_model",
"language_model_selector",
"language_models",
"log",
"lsp",
"markdown",
@@ -490,7 +492,6 @@ dependencies = [
"ui",
"util",
"uuid",
"vim_mode_setting",
"workspace",
"zed_actions",
]
@@ -517,6 +518,7 @@ dependencies = [
"language",
"language_model",
"language_model_selector",
"language_models",
"languages",
"log",
"multi_buffer",
@@ -1176,9 +1178,9 @@ dependencies = [
[[package]]
name = "aws-config"
version = "1.5.17"
version = "1.5.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "490aa7465ee685b2ced076bb87ef654a47724a7844e2c7d3af4e749ce5b875dd"
checksum = "50236e4d60fe8458de90a71c0922c761e41755adf091b1b03de1cef537179915"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -1269,9 +1271,9 @@ dependencies = [
[[package]]
name = "aws-sdk-bedrockruntime"
version = "1.75.0"
version = "1.74.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ddf7475b6f50a1a5be8edb1bcdf6e4ae00feed5b890d14a3f1f0e14d76f5a16"
checksum = "6938541d1948a543bca23303fec4cff9c36bf0e63b8fa3ae1b337bcb9d5b81af"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -1293,9 +1295,9 @@ dependencies = [
[[package]]
name = "aws-sdk-kinesis"
version = "1.62.0"
version = "1.61.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e31622345afd0c35d33c1cbba73ccf9fb88e09857413d8963dea2c493e00704d"
checksum = "89f2163d8704e8fdcd51ec6c2e0441c418471e422ee9690451b17a1c46344e1a"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -1315,9 +1317,9 @@ dependencies = [
[[package]]
name = "aws-sdk-s3"
version = "1.77.0"
version = "1.76.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34e87342432a3de0e94e82c99a7cbd9042f99de029ae1f4e368160f9e9929264"
checksum = "66e83401ad7287ad15244d557e35502c2a94105ca5b41d656c391f1a4fc04ca2"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -1349,9 +1351,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sso"
version = "1.60.0"
version = "1.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60186fab60b24376d3e33b9ff0a43485f99efd470e3b75a9160c849741d63d56"
checksum = "16ff718c9ee45cc1ebd4774a0e086bb80a6ab752b4902edf1c9f56b86ee1f770"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -1371,9 +1373,9 @@ dependencies = [
[[package]]
name = "aws-sdk-ssooidc"
version = "1.61.0"
version = "1.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7033130ce1ee13e6018905b7b976c915963755aef299c1521897679d6cd4f8ef"
checksum = "5183e088715cc135d8d396fdd3bc02f018f0da4c511f53cb8d795b6a31c55809"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -1393,9 +1395,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sts"
version = "1.61.0"
version = "1.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5c1cac7677179d622b4448b0d31bcb359185295dc6fca891920cfb17e2b5156"
checksum = "c9f944ef032717596639cea4a2118a3a457268ef51bbb5fde9637e54c465da00"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -1456,9 +1458,9 @@ dependencies = [
[[package]]
name = "aws-smithy-checksums"
version = "0.63.0"
version = "0.62.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db2dc8d842d872529355c72632de49ef8c5a2949a4472f10e802f28cf925770c"
checksum = "f2f45a1c384d7a393026bc5f5c177105aa9fa68e4749653b985707ac27d77295"
dependencies = [
"aws-smithy-http",
"aws-smithy-types",
@@ -1808,7 +1810,7 @@ dependencies = [
"bitflags 2.8.0",
"cexpr",
"clang-sys",
"itertools 0.10.5",
"itertools 0.12.1",
"lazy_static",
"lazycell",
"log",
@@ -1831,7 +1833,7 @@ dependencies = [
"bitflags 2.8.0",
"cexpr",
"clang-sys",
"itertools 0.10.5",
"itertools 0.12.1",
"log",
"prettyplease",
"proc-macro2",
@@ -2401,6 +2403,25 @@ dependencies = [
"cipher",
]
[[package]]
name = "cbindgen"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fce8dd7fcfcbf3a0a87d8f515194b49d6135acab73e18bd380d1d93bb1a15eb"
dependencies = [
"clap",
"heck 0.4.1",
"indexmap",
"log",
"proc-macro2",
"quote",
"serde",
"serde_json",
"syn 2.0.90",
"tempfile",
"toml 0.8.20",
]
[[package]]
name = "cbindgen"
version = "0.28.0"
@@ -2498,9 +2519,9 @@ dependencies = [
[[package]]
name = "chrono"
version = "0.4.40"
version = "0.4.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
dependencies = [
"android-tzdata",
"iana-time-zone",
@@ -2508,7 +2529,7 @@ dependencies = [
"num-traits",
"serde",
"wasm-bindgen",
"windows-link",
"windows-targets 0.52.6",
]
[[package]]
@@ -2568,9 +2589,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.31"
version = "4.5.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767"
checksum = "92b7b18d71fad5313a1e320fa9897994228ce274b60faa4d694fe0ea89cd9e6d"
dependencies = [
"clap_builder",
"clap_derive",
@@ -2578,9 +2599,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.31"
version = "4.5.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863"
checksum = "a35db2071778a7344791a4fb4f95308b5673d219dee3ae348b86642574ecc90c"
dependencies = [
"anstream",
"anstyle",
@@ -2635,7 +2656,6 @@ dependencies = [
"serde",
"tempfile",
"util",
"windows 0.58.0",
]
[[package]]
@@ -2816,7 +2836,6 @@ dependencies = [
"futures 0.3.31",
"git",
"git_hosting_providers",
"git_ui",
"google_ai",
"gpui",
"hex",
@@ -3003,6 +3022,7 @@ dependencies = [
"collections",
"gpui",
"linkme",
"once_cell",
"parking_lot",
"theme",
]
@@ -3083,7 +3103,6 @@ version = "0.1.0"
dependencies = [
"anyhow",
"assistant_tool",
"async-trait",
"collections",
"command_palette_hooks",
"context_server_settings",
@@ -3124,9 +3143,9 @@ checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
[[package]]
name = "convert_case"
version = "0.8.0"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f"
checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7"
dependencies = [
"unicode-segmentation",
]
@@ -3505,10 +3524,11 @@ dependencies = [
[[package]]
name = "crc64fast-nvme"
version = "1.2.0"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4955638f00a809894c947f85a024020a20815b65a5eea633798ea7924edab2b3"
checksum = "d5e2ee08013e3f228d6d2394116c4549a6df77708442c62d887d83f68ef2ee37"
dependencies = [
"cbindgen 0.27.0",
"crc",
]
@@ -3645,19 +3665,18 @@ dependencies = [
[[package]]
name = "ctor"
version = "0.4.0"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7747ac3a66a06f4ee6718686c8ea976d2d05fb30ada93ebd76b3f9aef97257c"
checksum = "21d960ecacd0a1bf55e73144b72de745e7bf275c7952c50e36e8af0a0cb7ab1f"
dependencies = [
"ctor-proc-macro",
"dtor",
]
[[package]]
name = "ctor-proc-macro"
version = "0.0.5"
version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f211af61d8efdd104f96e57adf5e426ba1bc3ed7a4ead616e15e5881fd79c4d"
checksum = "c426d2ba3e525b39c1f0a9ba41b9fe61878dee11fa4e4a76b6ab440f46c5db5d"
[[package]]
name = "ctrlc"
@@ -4033,21 +4052,6 @@ dependencies = [
"phf",
]
[[package]]
name = "dtor"
version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bf39a0bfd1f94d62ffdb2802a7e6244c0f34f6ebacf5d4c26547d08cd1d67a5"
dependencies = [
"dtor-proc-macro",
]
[[package]]
name = "dtor-proc-macro"
version = "0.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7454e41ff9012c00d53cf7f475c5e3afa3b91b7c90568495495e8d9bf47a1055"
[[package]]
name = "dunce"
version = "1.0.5"
@@ -4102,7 +4106,7 @@ dependencies = [
"client",
"clock",
"collections",
"convert_case 0.8.0",
"convert_case 0.7.1",
"ctor",
"db",
"emojis",
@@ -5350,7 +5354,6 @@ dependencies = [
"serde_json",
"smol",
"sum_tree",
"tempfile",
"text",
"time",
"unindent",
@@ -5420,7 +5423,6 @@ dependencies = [
"theme",
"time",
"ui",
"unindent",
"util",
"windows 0.58.0",
"workspace",
@@ -5555,7 +5557,7 @@ dependencies = [
"bytemuck",
"calloop",
"calloop-wayland-source",
"cbindgen",
"cbindgen 0.28.0",
"cocoa 0.26.0",
"collections",
"core-foundation 0.9.4",
@@ -7012,7 +7014,6 @@ dependencies = [
"anthropic",
"anyhow",
"base64 0.22.1",
"client",
"collections",
"futures 0.3.31",
"google_ai",
@@ -7028,7 +7029,6 @@ dependencies = [
"serde_json",
"smol",
"strum",
"telemetry_events",
"thiserror 1.0.69",
"ui",
"util",
@@ -7055,10 +7055,6 @@ version = "0.1.0"
dependencies = [
"anthropic",
"anyhow",
"aws-config",
"aws-credential-types",
"aws_http_client",
"bedrock",
"client",
"collections",
"copilot",
@@ -7070,7 +7066,6 @@ dependencies = [
"futures 0.3.31",
"google_ai",
"gpui",
"gpui_tokio",
"http_client",
"language_model",
"lmstudio",
@@ -7086,9 +7081,10 @@ dependencies = [
"settings",
"smol",
"strum",
"telemetry_events",
"theme",
"thiserror 1.0.69",
"tiktoken-rs",
"tokio",
"ui",
"util",
]
@@ -7228,9 +7224,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
[[package]]
name = "libc"
version = "0.2.170"
version = "0.2.169"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
[[package]]
name = "libdbus-sys"
@@ -7574,9 +7570,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.26"
version = "0.4.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
dependencies = [
"serde",
"value-bag",
@@ -7947,7 +7943,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"convert_case 0.8.0",
"convert_case 0.7.1",
"log",
"pretty_assertions",
"streaming-iterator",
@@ -9757,15 +9753,6 @@ dependencies = [
"indexmap",
]
[[package]]
name = "pgvector"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0e8871b6d7ca78348c6cd29b911b94851f3429f0cd403130ca17f26c1fb91a6"
dependencies = [
"serde",
]
[[package]]
name = "phf"
version = "0.11.2"
@@ -10390,7 +10377,7 @@ checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4"
dependencies = [
"bytes 1.10.0",
"heck 0.5.0",
"itertools 0.10.5",
"itertools 0.12.1",
"log",
"multimap 0.10.0",
"once_cell",
@@ -10423,7 +10410,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1"
dependencies = [
"anyhow",
"itertools 0.10.5",
"itertools 0.12.1",
"proc-macro2",
"quote",
"syn 2.0.90",
@@ -11451,9 +11438,9 @@ dependencies = [
[[package]]
name = "rust-embed"
version = "8.6.0"
version = "8.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b3aba5104622db5c9fc61098de54708feb732e7763d7faa2fa625899f00bf6f"
checksum = "fa66af4a4fdd5e7ebc276f115e895611a34739a9c1c01028383d612d550953c0"
dependencies = [
"rust-embed-impl",
"rust-embed-utils",
@@ -11462,9 +11449,9 @@ dependencies = [
[[package]]
name = "rust-embed-impl"
version = "8.6.0"
version = "8.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f198c73be048d2c5aa8e12f7960ad08443e56fd39cc26336719fdb4ea0ebaae"
checksum = "6125dbc8867951125eec87294137f4e9c2c96566e61bf72c45095a7c77761478"
dependencies = [
"proc-macro2",
"quote",
@@ -11475,9 +11462,9 @@ dependencies = [
[[package]]
name = "rust-embed-utils"
version = "8.6.0"
version = "8.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a2fcdc9f40c8dc2922842ca9add611ad19f332227fc651d015881ad1552bd9a"
checksum = "2e5347777e9aacb56039b0e1f28785929a8a3b709e87482e7442c72e7c12529d"
dependencies = [
"globset",
"sha2",
@@ -11752,9 +11739,9 @@ dependencies = [
[[package]]
name = "schemars"
version = "0.8.22"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92"
dependencies = [
"dyn-clone",
"indexmap",
@@ -11765,9 +11752,9 @@ dependencies = [
[[package]]
name = "schemars_derive"
version = "0.8.22"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e"
dependencies = [
"proc-macro2",
"quote",
@@ -11830,18 +11817,17 @@ dependencies = [
[[package]]
name = "sea-orm"
version = "1.1.6"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13fba7b2c749b2d0a00303d5cb13e6761e39a4172554bdf930852cac4e7aeabd"
checksum = "00733e5418e8ae3758cdb988c3654174e716230cc53ee2cb884207cf86a23029"
dependencies = [
"async-stream",
"async-trait",
"bigdecimal",
"chrono",
"futures-util",
"futures 0.3.31",
"log",
"ouroboros",
"pgvector",
"rust_decimal",
"sea-orm-macros",
"sea-query",
@@ -11859,9 +11845,9 @@ dependencies = [
[[package]]
name = "sea-orm-macros"
version = "1.1.6"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2568cff8d35d5150b4276cc0dd766192a587f64b6ece60ae3706e0872c4eb209"
checksum = "a98408f82fb4875d41ef469a79944a7da29767c7b3e4028e22188a3dd613b10f"
dependencies = [
"heck 0.4.1",
"proc-macro2",
@@ -12054,18 +12040,18 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.218"
version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.218"
version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
dependencies = [
"proc-macro2",
"quote",
@@ -14578,7 +14564,7 @@ dependencies = [
name = "ui_macros"
version = "0.1.0"
dependencies = [
"convert_case 0.8.0",
"convert_case 0.7.1",
"linkme",
"proc-macro2",
"quote",
@@ -14781,9 +14767,9 @@ dependencies = [
[[package]]
name = "uuid"
version = "1.15.1"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587"
checksum = "8c1f41ffb7cf259f1ecc2876861a17e7142e63ead296f671f81f6ae85903e0d6"
dependencies = [
"getrandom 0.3.1",
"serde",
@@ -15906,12 +15892,6 @@ dependencies = [
"syn 2.0.90",
]
[[package]]
name = "windows-link"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3"
[[package]]
name = "windows-registry"
version = "0.2.0"
@@ -16747,7 +16727,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.177.0"
version = "0.176.0"
dependencies = [
"activity_indicator",
"anyhow",
@@ -16851,7 +16831,6 @@ dependencies = [
"tasks_ui",
"telemetry",
"telemetry_events",
"tempfile",
"terminal_view",
"theme",
"theme_extension",
@@ -16868,7 +16847,6 @@ dependencies = [
"vim",
"vim_mode_setting",
"welcome",
"which 6.0.3",
"windows 0.58.0",
"winresource",
"workspace",
@@ -16967,7 +16945,7 @@ dependencies = [
[[package]]
name = "zed_html"
version = "0.1.6"
version = "0.1.5"
dependencies = [
"zed_extension_api 0.1.0",
]
@@ -17212,7 +17190,7 @@ dependencies = [
"indoc",
"inline_completion",
"language",
"language_model",
"language_models",
"log",
"menu",
"migrator",

View File

@@ -370,7 +370,7 @@ zeta = { path = "crates/zeta" }
#
aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", rev = "03c2907b44b4189aac5fdeaea331f5aab5c7072e" }
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", rev = "03c2907b44b4189aac5fdeaea331f5aab5c7072e"}
any_vec = "0.14"
anyhow = "1.0.86"
arrayvec = { version = "0.7.4", features = ["serde"] }
@@ -405,10 +405,10 @@ chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4.4", features = ["derive"] }
cocoa = "0.26"
cocoa-foundation = "0.2.0"
convert_case = "0.8.0"
convert_case = "0.7.0"
core-foundation = "0.9.3"
core-foundation-sys = "0.8.6"
ctor = "0.4.0"
ctor = "0.3.0"
dashmap = "6.0"
derive_more = "0.99.17"
dirs = "4.0"
@@ -455,6 +455,7 @@ nanoid = "0.4"
nbformat = { version = "0.10.0" }
nix = "0.29"
num-format = "0.4.4"
once_cell = "1.20"
ordered-float = "2.1.1"
palette = { version = "0.7.5", default-features = false, features = ["std"] }
parking_lot = "0.12.1"
@@ -543,7 +544,7 @@ tree-sitter-cpp = "0.23"
tree-sitter-css = "0.23"
tree-sitter-elixir = "0.3"
tree-sitter-embedded-template = "0.23.0"
tree-sitter-gitcommit = { git = "https://github.com/zed-industries/tree-sitter-git-commit", rev = "88309716a69dd13ab83443721ba6e0b491d37ee9" }
tree-sitter-gitcommit = {git = "https://github.com/zed-industries/tree-sitter-git-commit", rev = "88309716a69dd13ab83443721ba6e0b491d37ee9"}
tree-sitter-go = "0.23"
tree-sitter-go-mod = { git = "https://github.com/camdencheek/tree-sitter-go-mod", rev = "6efb59652d30e0e9cd5f3b3a669afd6f1a926d3c", package = "tree-sitter-gomod" }
tree-sitter-gowork = { git = "https://github.com/zed-industries/tree-sitter-go-work", rev = "acb0617bf7f4fda02c6217676cc64acb89536dc7" }
@@ -618,7 +619,6 @@ features = [
"Win32_Storage_FileSystem",
"Win32_System_Com",
"Win32_System_Com_StructuredStorage",
"Win32_System_Console",
"Win32_System_DataExchange",
"Win32_System_LibraryLoader",
"Win32_System_Memory",
@@ -639,7 +639,7 @@ features = [
# TODO livekit https://github.com/RustAudio/cpal/pull/891
[patch.crates-io]
cpal = { git = "https://github.com/zed-industries/cpal", rev = "fd8bc2fd39f1f5fdee5a0690656caff9a26d9d50" }
real-async-tls = { git = "https://github.com/zed-industries/async-tls", rev = "1e759a4b5e370f87dc15e40756ac4f8815b61d9d", package = "async-tls" }
real-async-tls = { git = "https://github.com/zed-industries/async-tls", rev = "1e759a4b5e370f87dc15e40756ac4f8815b61d9d", package = "async-tls"}
[profile.dev]
split-debuginfo = "unpacked"
@@ -709,9 +709,6 @@ debug = "full"
lto = false
codegen-units = 16
[workspace.lints.rust]
unexpected_cfgs = { level = "allow" }
[workspace.lints.clippy]
dbg_macro = "deny"
todo = "deny"

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="283.6413 127.3453 56 55.9999" width="16px" height="16px">
<path d="M 808.592 158.131 C 807.489 158.131 806.592 157.234 806.592 156.131 C 806.592 155.028 807.489 154.131 808.592 154.131 C 809.695 154.131 810.592 155.028 810.592 156.131 C 810.592 157.234 809.695 158.131 808.592 158.131 Z M 776.705 185.039 L 773.457 183.145 L 780.122 178.979 L 779.062 177.283 L 771.505 182.006 L 765.592 178.557 L 765.592 169.666 L 771.147 165.963 L 770.037 164.299 L 764.551 167.956 L 758.592 164.551 L 758.592 159.711 L 765.088 155.999 L 764.096 154.263 L 758.592 157.408 L 758.592 153.711 L 764.592 150.283 L 770.592 153.711 L 770.592 157.565 L 766.077 160.274 L 767.107 161.988 L 771.592 159.297 L 776.077 161.988 L 777.107 160.274 L 772.592 157.565 L 772.592 153.666 L 778.147 149.963 C 778.425 149.777 778.592 149.465 778.592 149.131 L 778.592 142.131 L 776.592 142.131 L 776.592 148.596 L 771.551 151.956 L 765.592 148.551 L 765.592 139.705 L 770.592 136.789 L 770.592 145.131 L 772.592 145.131 L 772.592 135.622 L 776.705 133.223 L 784.592 135.852 L 784.592 164.565 L 770.077 173.274 L 771.107 174.988 L 784.592 166.897 L 784.592 182.41 L 776.705 185.039 Z M 806.592 169.131 C 806.592 170.234 805.695 171.131 804.592 171.131 C 803.489 171.131 802.592 170.234 802.592 169.131 C 802.592 168.028 803.489 167.131 804.592 167.131 C 805.695 167.131 806.592 168.028 806.592 169.131 Z M 796.592 179.131 C 796.592 180.234 795.695 181.131 794.592 181.131 C 793.489 181.131 792.592 180.234 792.592 179.131 C 792.592 178.028 793.489 177.131 794.592 177.131 C 795.695 177.131 796.592 178.028 796.592 179.131 Z M 795.592 139.131 C 795.592 138.028 796.489 137.131 797.592 137.131 C 798.695 137.131 799.592 138.028 799.592 139.131 C 799.592 140.234 798.695 141.131 797.592 141.131 C 796.489 141.131 795.592 140.234 795.592 139.131 Z M 808.592 152.131 C 806.733 152.131 805.181 153.411 804.734 155.131 L 786.592 155.131 L 786.592 150.131 L 797.592 150.131 C 798.145 150.131 798.592 149.683 798.592 149.131 L 798.592 142.989 C 800.312 142.542 801.592 140.989 801.592 139.131 C 801.592 136.925 799.798 135.131 797.592 135.131 C 795.386 135.131 793.592 136.925 793.592 139.131 C 793.592 140.989 794.872 142.542 796.592 142.989 L 796.592 148.131 L 786.592 148.131 L 786.592 135.131 C 786.592 134.7 786.317 134.319 785.908 134.182 L 776.908 131.182 C 776.634 131.092 776.336 131.122 776.088 131.267 L 764.088 138.267 C 763.78 138.446 763.592 138.776 763.592 139.131 L 763.592 148.551 L 757.096 152.263 C 756.784 152.441 756.592 152.772 756.592 153.131 L 756.592 165.131 C 756.592 165.49 756.784 165.821 757.096 165.999 L 763.592 169.711 L 763.592 179.131 C 763.592 179.486 763.78 179.816 764.088 179.995 L 776.088 186.995 C 776.242 187.085 776.417 187.131 776.592 187.131 C 776.698 187.131 776.805 187.114 776.908 187.08 L 785.908 184.08 C 786.317 183.943 786.592 183.562 786.592 183.131 L 786.592 171.131 L 793.592 171.131 L 793.592 175.273 C 791.872 175.72 790.592 177.273 790.592 179.131 C 790.592 181.337 792.386 183.131 794.592 183.131 C 796.798 183.131 798.592 181.337 798.592 179.131 C 798.592 177.273 797.312 175.72 795.592 175.273 L 795.592 170.131 C 795.592 169.579 795.145 169.131 794.592 169.131 L 786.592 169.131 L 786.592 164.131 L 799.092 164.131 L 801.23 166.981 C 800.831 167.603 800.592 168.338 800.592 169.131 C 800.592 171.337 802.386 173.131 804.592 173.131 C 806.798 173.131 808.592 171.337 808.592 169.131 C 808.592 166.925 806.798 165.131 804.592 165.131 C 803.908 165.131 803.274 165.319 802.711 165.623 L 800.392 162.531 C 800.203 162.279 799.906 162.131 799.592 162.131 L 786.592 162.131 L 786.592 157.131 L 804.734 157.131 C 805.181 158.851 806.733 160.131 808.592 160.131 C 810.798 160.131 812.592 158.337 812.592 156.131 C 812.592 153.925 810.798 152.131 808.592 152.131 Z" fill-rule="evenodd" fill-opacity="1" style="" id="object-0" transform="matrix(1, 0, 0, 1, -472.9506530761719, -3.7858259677886963)"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -84,7 +84,7 @@
"pageup": "editor::MovePageUp",
"alt-pageup": "editor::PageUp",
"shift-pageup": "editor::SelectPageUp",
"home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
"home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": true }],
"down": "editor::MoveDown",
"pagedown": "editor::MovePageDown",
"alt-pagedown": "editor::PageDown",
@@ -107,9 +107,9 @@
"ctrl-a": "editor::SelectAll",
"ctrl-l": "editor::SelectLine",
"ctrl-shift-i": "editor::Format",
// "cmd-shift-left": ["editor::SelectToBeginningOfLine", {"stop_at_soft_wraps": true, "stop_at_indent": true }],
// "ctrl-shift-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
"shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
// "cmd-shift-left": ["editor::SelectToBeginningOfLine", {"stop_at_soft_wraps": true }],
// "ctrl-shift-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true }],
"shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true }],
// "cmd-shift-right": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
// "ctrl-shift-e": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
"shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
@@ -606,7 +606,7 @@
"ctrl-n": "assistant2::NewThread",
"new": "assistant2::NewThread",
"ctrl-shift-h": "assistant2::OpenHistory",
"ctrl-alt-/": "assistant::ToggleModelSelector",
"ctrl-alt-/": "assistant2::ToggleModelSelector",
"ctrl-shift-a": "assistant2::ToggleContextPicker",
"ctrl-e": "assistant2::ChatMode",
"ctrl-alt-e": "assistant2::RemoveAllContext"
@@ -733,12 +733,6 @@
"ctrl-enter": "git::Commit"
}
},
{
"context": "GitDiff > Editor",
"bindings": {
"ctrl-enter": "git::Commit"
}
},
{
"context": "GitPanel > Editor",
"bindings": {

View File

@@ -91,9 +91,9 @@
"ctrl-l": "editor::ScrollCursorCenter",
"alt-left": "editor::MoveToPreviousWordStart",
"alt-right": "editor::MoveToNextWordEnd",
"cmd-left": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
"ctrl-a": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false, "stop_at_indent": true }],
"home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
"cmd-left": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": true }],
"ctrl-a": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }],
"home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": true }],
"cmd-right": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": true }],
"ctrl-e": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }],
"end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": true }],
@@ -118,9 +118,9 @@
"cmd-a": "editor::SelectAll",
"cmd-l": "editor::SelectLine",
"cmd-shift-i": "editor::Format",
"cmd-shift-left": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
"shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
"ctrl-shift-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
"cmd-shift-left": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true }],
"shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true }],
"ctrl-shift-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true }],
"cmd-shift-right": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
"shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
"ctrl-shift-e": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
@@ -157,7 +157,8 @@
"cmd->": "assistant::QuoteSelection",
"cmd-<": "assistant::InsertIntoEditor",
"cmd-alt-e": "editor::SelectEnclosingSymbol",
"alt-enter": "editor::OpenSelectionsInMultibuffer"
"alt-enter": "editor::OpenSelectionsInMultibuffer",
"cmd-g": "git::Commit"
}
},
{
@@ -238,7 +239,7 @@
"cmd-n": "assistant2::NewThread",
"cmd-alt-p": "assistant2::NewPromptEditor",
"cmd-shift-h": "assistant2::OpenHistory",
"cmd-alt-/": "assistant::ToggleModelSelector",
"cmd-alt-/": "assistant2::ToggleModelSelector",
"cmd-shift-a": "assistant2::ToggleContextPicker",
"cmd-e": "assistant2::ChatMode",
"cmd-alt-e": "assistant2::RemoveAllContext"
@@ -658,7 +659,7 @@
"use_key_equivalents": true,
"bindings": {
"cmd-shift-a": "assistant2::ToggleContextPicker",
"cmd-alt-/": "assistant::ToggleModelSelector",
"cmd-alt-/": "assistant2::ToggleModelSelector",
"cmd-alt-e": "assistant2::RemoveAllContext",
"ctrl-[": "assistant::CyclePreviousInlineAssist",
"ctrl-]": "assistant::CycleNextInlineAssist"
@@ -739,15 +740,7 @@
"alt-down": "git_panel::FocusEditor",
"tab": "git_panel::FocusEditor",
"shift-tab": "git_panel::FocusEditor",
"escape": "git_panel::ToggleFocus",
"cmd-enter": "git::Commit"
}
},
{
"context": "GitDiff > Editor",
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "git::Commit"
"escape": "git_panel::ToggleFocus"
}
},
{
@@ -839,7 +832,6 @@
"cmd-k": "terminal::Clear",
"cmd-n": "workspace::NewTerminal",
"ctrl-enter": "assistant::InlineAssist",
"ctrl-_": null, // emacs undo
// Some nice conveniences
"cmd-backspace": ["terminal::SendText", "\u0015"],
"cmd-right": ["terminal::SendText", "\u0005"],

View File

@@ -28,7 +28,6 @@
"ctrl-e": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }], // move-end-of-line
"shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false }], // move-beginning-of-line
"shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": false }], // move-end-of-line
"alt-m": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false, "stop_at_indent": true }], // back-to-indentation
"alt-f": "editor::MoveToNextSubwordEnd", // forward-word
"alt-b": "editor::MoveToPreviousSubwordStart", // backward-word
"alt-u": "editor::ConvertToUpperCase", // upcase-word

View File

@@ -28,7 +28,6 @@
"ctrl-e": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }], // move-end-of-line
"shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false }], // move-beginning-of-line
"shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": false }], // move-end-of-line
"alt-m": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false, "stop_at_indent": true }], // back-to-indentation
"alt-f": "editor::MoveToNextSubwordEnd", // forward-word
"alt-b": "editor::MoveToPreviousSubwordStart", // backward-word
"alt-u": "editor::ConvertToUpperCase", // upcase-word

View File

@@ -176,8 +176,8 @@
"show_completion_documentation": true,
// Show method signatures in the editor, when inside parentheses.
"auto_signature_help": false,
// Whether to show the signature help after completion or a bracket pair inserted.
// If `auto_signature_help` is enabled, this setting will be treated as enabled also.
/// Whether to show the signature help after completion or a bracket pair inserted.
/// If `auto_signature_help` is enabled, this setting will be treated as enabled also.
"show_signature_help_after_edits": false,
// Whether to show wrap guides (vertical rulers) in the editor.
// Setting this to true will show a guide at the 'preferred_line_length' value
@@ -298,11 +298,11 @@
// - "information": show only errors, warnings, and information
// - "all" or true: show all diagnostics
"diagnostics": "all",
// Forcefully enable or disable the scrollbar for each axis
/// Forcefully enable or disable the scrollbar for each axis
"axes": {
// When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings.
/// When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings.
"horizontal": true,
// When false, forcefully disables the vertical scrollbar. Otherwise, obey other settings.
/// When false, forcefully disables the vertical scrollbar. Otherwise, obey other settings.
"vertical": true
}
},
@@ -328,24 +328,24 @@
"folds": true
},
"indent_guides": {
// Whether to show indent guides in the editor.
/// Whether to show indent guides in the editor.
"enabled": true,
// The width of the indent guides in pixels, between 1 and 10.
/// The width of the indent guides in pixels, between 1 and 10.
"line_width": 1,
// The width of the active indent guide in pixels, between 1 and 10.
/// The width of the active indent guide in pixels, between 1 and 10.
"active_line_width": 1,
// Determines how indent guides are colored.
// This setting can take the following three values:
/// Determines how indent guides are colored.
/// This setting can take the following three values:
///
// 1. "disabled"
// 2. "fixed"
// 3. "indent_aware"
/// 1. "disabled"
/// 2. "fixed"
/// 3. "indent_aware"
"coloring": "fixed",
// Determines how indent guide backgrounds are colored.
// This setting can take the following two values:
/// Determines how indent guide backgrounds are colored.
/// This setting can take the following two values:
///
// 1. "disabled"
// 2. "indent_aware"
/// 1. "disabled"
/// 2. "indent_aware"
"background_coloring": "disabled"
},
// Whether the editor will scroll beyond the last line.
@@ -379,9 +379,6 @@
// 3. Never populate the search query
// "never"
"seed_search_query_from_cursor": "always",
// When enabled, automatically adjusts search case sensitivity based on your query.
// If your search query contains any uppercase letters, the search becomes case-sensitive;
// if it contains only lowercase letters, the search becomes case-insensitive.
"use_smartcase_search": false,
// Inlay hint related settings
"inlay_hints": {
@@ -401,16 +398,7 @@
"edit_debounce_ms": 700,
// Time to wait after scrolling the buffer, before requesting the hints,
// set to 0 to disable debouncing.
"scroll_debounce_ms": 50,
/// A set of modifiers which, when pressed, will toggle the visibility of inlay hints.
/// If the set if empty or not all the modifiers specified are pressed, inlay hints will not be toggled.
"toggle_on_modifiers_press": {
"control": false,
"shift": false,
"alt": false,
"platform": false,
"function": false
}
"scroll_debounce_ms": 50
},
"project_panel": {
// Whether to show the project panel button in the status bar
@@ -436,32 +424,32 @@
// Whether to fold directories automatically and show compact folders
// (e.g. "a/b/c" ) when a directory has only one subdirectory inside.
"auto_fold_dirs": true,
// Scrollbar-related settings
/// Scrollbar-related settings
"scrollbar": {
// When to show the scrollbar in the project panel.
// This setting can take five values:
/// When to show the scrollbar in the project panel.
/// This setting can take five values:
///
// 1. null (default): Inherit editor settings
// 2. Show the scrollbar if there's important information or
// follow the system's configured behavior (default):
// "auto"
// 3. Match the system's configured behavior:
// "system"
// 4. Always show the scrollbar:
// "always"
// 5. Never show the scrollbar:
// "never"
/// 1. null (default): Inherit editor settings
/// 2. Show the scrollbar if there's important information or
/// follow the system's configured behavior (default):
/// "auto"
/// 3. Match the system's configured behavior:
/// "system"
/// 4. Always show the scrollbar:
/// "always"
/// 5. Never show the scrollbar:
/// "never"
"show": null
},
// Which files containing diagnostic errors/warnings to mark in the project panel.
// This setting can take the following three values:
/// Which files containing diagnostic errors/warnings to mark in the project panel.
/// This setting can take the following three values:
///
// 1. Do not mark any files:
// "off"
// 2. Only mark files with errors:
// "errors"
// 3. Mark files with errors and warnings:
// "all"
/// 1. Do not mark any files:
/// "off"
/// 2. Only mark files with errors:
/// "errors"
/// 3. Mark files with errors and warnings:
/// "all"
"show_diagnostics": "all",
// Settings related to indent guides in the project panel.
"indent_guides": {
@@ -494,8 +482,8 @@
// when a corresponding outline entry becomes active.
// Gitignored entries are never auto revealed.
"auto_reveal_entries": true,
// Whether to fold directories automatically
// when a directory has only one directory inside.
/// Whether to fold directories automatically
/// when a directory has only one directory inside.
"auto_fold_dirs": true,
// Settings related to indent guides in the outline panel.
"indent_guides": {
@@ -508,21 +496,21 @@
// "never"
"show": "always"
},
// Scrollbar-related settings
/// Scrollbar-related settings
"scrollbar": {
// When to show the scrollbar in the project panel.
// This setting can take five values:
/// When to show the scrollbar in the project panel.
/// This setting can take five values:
///
// 1. null (default): Inherit editor settings
// 2. Show the scrollbar if there's important information or
// follow the system's configured behavior (default):
// "auto"
// 3. Match the system's configured behavior:
// "system"
// 4. Always show the scrollbar:
// "always"
// 5. Never show the scrollbar:
// "never"
/// 1. null (default): Inherit editor settings
/// 2. Show the scrollbar if there's important information or
/// follow the system's configured behavior (default):
/// "auto"
/// 3. Match the system's configured behavior:
/// "system"
/// 4. Always show the scrollbar:
/// "always"
/// 5. Never show the scrollbar:
/// "never"
"show": null
}
},
@@ -640,7 +628,7 @@
"show": true,
// Whether or not to show the navigation history buttons.
"show_nav_history_buttons": true,
// Whether or not to show the tab bar buttons.
/// Whether or not to show the tab bar buttons.
"show_tab_bar_buttons": true
},
// Settings related to the editor's tabs
@@ -662,16 +650,16 @@
// 3. Activate the left neighbour tab if present
// "left_neighbour"
"activate_on_close": "history",
// Which files containing diagnostic errors/warnings to mark in the tabs.
// Diagnostics are only shown when file icons are also active.
// This setting only works when can take the following three values:
/// Which files containing diagnostic errors/warnings to mark in the tabs.
/// Diagnostics are only shown when file icons are also active.
/// This setting only works when can take the following three values:
///
// 1. Do not mark any files:
// "off"
// 2. Only mark files with errors:
// "errors"
// 3. Mark files with errors and warnings:
// "all"
/// 1. Do not mark any files:
/// "off"
/// 2. Only mark files with errors:
/// "errors"
/// 3. Mark files with errors and warnings:
/// "all"
"show_diagnostics": "off"
},
// Settings related to preview tabs.
@@ -841,8 +829,6 @@
// A list of globs representing files that edit predictions should be disabled for.
// There's a sensible default list of globs already included.
// Any addition to this list will be merged with the default list.
// Globs are matched relative to the worktree root,
// except when starting with a slash (/) or equivalent in Windows.
"disabled_globs": [
"**/.env*",
"**/*.pem",
@@ -854,14 +840,11 @@
],
// When to show edit predictions previews in buffer.
// This setting takes two possible values:
// 1. Display predictions inline when there are no language server completions available.
// "mode": "eager"
// 2. Display predictions inline only when holding a modifier key (alt by default).
// "mode": "subtle"
"mode": "eager",
// Whether edit predictions are enabled in the assistant panel.
// This setting has no effect if globally disabled.
"enabled_in_assistant": true
// 1. Display inline when there are no language server completions available.
// "mode": "eager_preview"
// 2. Display inline when holding modifier key (alt by default).
// "mode": "auto"
"mode": "eager_preview"
},
// Settings specific to journaling
"journal": {
@@ -997,21 +980,21 @@
// Example: `echo -e "\e]2;New Title\007";`
"breadcrumbs": true
},
// Scrollbar-related settings
/// Scrollbar-related settings
"scrollbar": {
// When to show the scrollbar in the terminal.
// This setting can take five values:
/// When to show the scrollbar in the terminal.
/// This setting can take five values:
///
// 1. null (default): Inherit editor settings
// 2. Show the scrollbar if there's important information or
// follow the system's configured behavior (default):
// "auto"
// 3. Match the system's configured behavior:
// "system"
// 4. Always show the scrollbar:
// "always"
// 5. Never show the scrollbar:
// "never"
/// 1. null (default): Inherit editor settings
/// 2. Show the scrollbar if there's important information or
/// follow the system's configured behavior (default):
/// "auto"
/// 3. Match the system's configured behavior:
/// "system"
/// 4. Always show the scrollbar:
/// "always"
/// 5. Never show the scrollbar:
/// "never"
"show": null
}
// Set the terminal's font size. If this option is not included,
@@ -1030,7 +1013,7 @@
// "max_scroll_history_lines": 10000,
},
"code_actions_on_format": {},
// Settings related to running tasks.
/// Settings related to running tasks.
"tasks": {
"variables": {}
},
@@ -1051,20 +1034,20 @@
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "**/.vscode/**/*.json"],
"Shell Script": [".env.*"]
},
// By default use a recent system version of node, or install our own.
// You can override this to use a version of node that is not in $PATH with:
// {
// "node": {
// "path": "/path/to/node"
// "npm_path": "/path/to/npm" (defaults to node_path/../npm)
// }
// }
// or to ensure Zed always downloads and installs an isolated version of node:
// {
// "node": {
// "ignore_system_version": true,
// }
// NOTE: changing this setting currently requires restarting Zed.
/// By default use a recent system version of node, or install our own.
/// You can override this to use a version of node that is not in $PATH with:
/// {
/// "node": {
/// "path": "/path/to/node"
/// "npm_path": "/path/to/npm" (defaults to node_path/../npm)
/// }
/// }
/// or to ensure Zed always downloads and installs an isolated version of node:
/// {
/// "node": {
/// "ignore_system_version": true,
/// }
/// NOTE: changing this setting currently requires restarting Zed.
"node": {},
// The extensions that Zed should automatically install on startup.
//
@@ -1313,7 +1296,6 @@
},
// Vim settings
"vim": {
"default_mode": "normal",
"toggle_relative_line_numbers": false,
"use_system_clipboard": "always",
"use_multiline_find": false,

View File

@@ -43,6 +43,7 @@ indoc.workspace = true
language.workspace = true
language_model.workspace = true
language_model_selector.workspace = true
language_models.workspace = true
log.workspace = true
lsp.workspace = true
menu.workspace = true

View File

@@ -32,10 +32,11 @@ use gpui::{
};
use language::{line_diff, Buffer, IndentKind, Point, Selection, TransactionId};
use language_model::{
report_assistant_event, LanguageModel, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, LanguageModelTextStream, Role,
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
LanguageModelTextStream, Role,
};
use language_model_selector::{InlineLanguageModelSelector, LanguageModelSelector};
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use language_models::report_assistant_event;
use multi_buffer::MultiBufferRow;
use parking_lot::Mutex;
use project::{CodeAction, ProjectTransaction};
@@ -1589,10 +1590,29 @@ impl Render for PromptEditor {
.w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))
.justify_center()
.gap_2()
.child(
InlineLanguageModelSelector::new(self.language_model_selector.clone())
.render(window, cx),
)
.child(LanguageModelSelectorPopoverMenu::new(
self.language_model_selector.clone(),
IconButton::new("context", IconName::SettingsAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.icon_color(Color::Muted),
move |window, cx| {
Tooltip::with_meta(
format!(
"Using {}",
LanguageModelRegistry::read_global(cx)
.active_model()
.map(|model| model.name().0)
.unwrap_or_else(|| "No model selected".into()),
),
None,
"Change Model",
window,
cx,
)
},
gpui::Corner::TopRight,
))
.map(|el| {
let CodegenStatus::Error(error) = self.codegen.read(cx).status(cx) else {
return el;

View File

@@ -16,10 +16,10 @@ use gpui::{
};
use language::Buffer;
use language_model::{
report_assistant_event, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, Role,
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
};
use language_model_selector::{InlineLanguageModelSelector, LanguageModelSelector};
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use language_models::report_assistant_event;
use prompt_library::PromptBuilder;
use settings::{update_settings_file, Settings};
use std::{
@@ -506,7 +506,7 @@ struct PromptEditor {
impl EventEmitter<PromptEditorEvent> for PromptEditor {}
impl Render for PromptEditor {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let status = &self.codegen.read(cx).status;
let buttons = match status {
CodegenStatus::Idle => {
@@ -641,10 +641,29 @@ impl Render for PromptEditor {
.w_12()
.justify_center()
.gap_2()
.child(
InlineLanguageModelSelector::new(self.language_model_selector.clone())
.render(window, cx),
)
.child(LanguageModelSelectorPopoverMenu::new(
self.language_model_selector.clone(),
IconButton::new("context", IconName::SettingsAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.icon_color(Color::Muted),
move |window, cx| {
Tooltip::with_meta(
format!(
"Using {}",
LanguageModelRegistry::read_global(cx)
.active_model()
.map(|model| model.name().0)
.unwrap_or_else(|| "No model selected".into()),
),
None,
"Change Model",
window,
cx,
)
},
gpui::Corner::TopRight,
))
.children(
if let CodegenStatus::Error(error) = &self.codegen.read(cx).status {
let error_message = SharedString::from(error.to_string());
@@ -1054,10 +1073,7 @@ pub enum CodegenEvent {
impl EventEmitter<CodegenEvent> for Codegen {}
#[cfg(not(target_os = "windows"))]
const CLEAR_INPUT: &str = "\x15";
#[cfg(target_os = "windows")]
const CLEAR_INPUT: &str = "\x03";
const CARRIAGE_RETURN: &str = "\x0d";
struct TerminalTransaction {

View File

@@ -46,6 +46,7 @@ itertools.workspace = true
language.workspace = true
language_model.workspace = true
language_model_selector.workspace = true
language_models.workspace = true
log.workspace = true
lsp.workspace = true
markdown.workspace = true
@@ -73,7 +74,6 @@ time_format.workspace = true
ui.workspace = true
util.workspace = true
uuid.workspace = true
vim_mode_setting.workspace = true
workspace.workspace = true
zed_actions.workspace = true

View File

@@ -8,16 +8,15 @@ use gpui::{
UnderlineStyle, WeakEntity,
};
use language::LanguageRegistry;
use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
use language_model::Role;
use markdown::{Markdown, MarkdownStyle};
use settings::Settings as _;
use theme::ThemeSettings;
use ui::{prelude::*, Disclosure};
use ui::prelude::*;
use workspace::Workspace;
use crate::thread::{MessageId, RequestKind, Thread, ThreadError, ThreadEvent};
use crate::thread::{MessageId, Thread, ThreadError, ThreadEvent};
use crate::thread_store::ThreadStore;
use crate::tool_use::{ToolUse, ToolUseStatus};
use crate::ui::ContextPill;
pub struct ActiveThread {
@@ -29,7 +28,6 @@ pub struct ActiveThread {
messages: Vec<MessageId>,
list_state: ListState,
rendered_messages_by_id: HashMap<MessageId, Entity<Markdown>>,
expanded_tool_uses: HashMap<LanguageModelToolUseId, bool>,
last_error: Option<ThreadError>,
_subscriptions: Vec<Subscription>,
}
@@ -57,7 +55,6 @@ impl ActiveThread {
thread: thread.clone(),
messages: Vec::new(),
rendered_messages_by_id: HashMap::default(),
expanded_tool_uses: HashMap::default(),
list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), {
let this = cx.entity().downgrade();
move |ix, _: &mut Window, cx: &mut App| {
@@ -254,29 +251,17 @@ impl ActiveThread {
let task = tool.run(tool_use.input, self.workspace.clone(), window, cx);
self.thread.update(cx, |thread, cx| {
thread.insert_tool_output(tool_use.id.clone(), task, cx);
});
}
}
}
ThreadEvent::ToolFinished { .. } => {
let all_tools_finished = self
.thread
.read(cx)
.pending_tool_uses()
.into_iter()
.all(|tool_use| tool_use.status.is_error());
if all_tools_finished {
let model_registry = LanguageModelRegistry::read_global(cx);
if let Some(model) = model_registry.active_model() {
self.thread.update(cx, |thread, cx| {
// Insert an empty user message to contain the tool results.
thread.insert_user_message("", Vec::new(), cx);
thread.send_to_model(model, RequestKind::Chat, true, cx);
thread.insert_tool_output(
tool_use.assistant_message_id,
tool_use.id.clone(),
task,
cx,
);
});
}
}
}
ThreadEvent::ToolFinished { .. } => {}
}
}
@@ -291,17 +276,8 @@ impl ActiveThread {
};
let context = self.thread.read(cx).context_for_message(message_id);
let tool_uses = self.thread.read(cx).tool_uses_for_message(message_id);
let colors = cx.theme().colors();
// Don't render user messages that are just there for returning tool results.
if message.role == Role::User
&& message.text.is_empty()
&& self.thread.read(cx).message_has_tool_results(message_id)
{
return Empty.into_any();
}
let message_content = v_flex()
.child(div().p_2p5().text_ui(cx).child(markdown.clone()))
.when_some(context, |parent, context| {
@@ -356,22 +332,7 @@ impl ActiveThread {
)
.child(message_content),
),
Role::Assistant => div()
.id(("message-container", ix))
.child(message_content)
.map(|parent| {
if tool_uses.is_empty() {
return parent;
}
parent.child(
v_flex().children(
tool_uses
.into_iter()
.map(|tool_use| self.render_tool_use(tool_use, cx)),
),
)
}),
Role::Assistant => div().id(("message-container", ix)).child(message_content),
Role::System => div().id(("message-container", ix)).py_1().px_2().child(
v_flex()
.bg(colors.editor_background)
@@ -382,102 +343,6 @@ impl ActiveThread {
styled_message.into_any()
}
fn render_tool_use(&self, tool_use: ToolUse, cx: &mut Context<Self>) -> impl IntoElement {
let is_open = self
.expanded_tool_uses
.get(&tool_use.id)
.copied()
.unwrap_or_default();
div().px_2p5().child(
v_flex()
.gap_1()
.rounded_lg()
.border_1()
.border_color(cx.theme().colors().border)
.child(
h_flex()
.justify_between()
.py_0p5()
.pl_1()
.pr_2()
.bg(cx.theme().colors().editor_foreground.opacity(0.02))
.when(is_open, |element| element.border_b_1().rounded_t(px(6.)))
.when(!is_open, |element| element.rounded(px(6.)))
.border_color(cx.theme().colors().border)
.child(
h_flex()
.gap_1()
.child(Disclosure::new("tool-use-disclosure", is_open).on_click(
cx.listener({
let tool_use_id = tool_use.id.clone();
move |this, _event, _window, _cx| {
let is_open = this
.expanded_tool_uses
.entry(tool_use_id.clone())
.or_insert(false);
*is_open = !*is_open;
}
}),
))
.child(Label::new(tool_use.name)),
)
.child(
Label::new(match tool_use.status {
ToolUseStatus::Pending => "Pending",
ToolUseStatus::Running => "Running",
ToolUseStatus::Finished(_) => "Finished",
ToolUseStatus::Error(_) => "Error",
})
.size(LabelSize::XSmall)
.buffer_font(cx),
),
)
.map(|parent| {
if !is_open {
return parent;
}
parent.child(
v_flex()
.child(
v_flex()
.gap_0p5()
.py_1()
.px_2p5()
.border_b_1()
.border_color(cx.theme().colors().border)
.child(Label::new("Input:"))
.child(Label::new(
serde_json::to_string_pretty(&tool_use.input)
.unwrap_or_default(),
)),
)
.map(|parent| match tool_use.status {
ToolUseStatus::Finished(output) => parent.child(
v_flex()
.gap_0p5()
.py_1()
.px_2p5()
.child(Label::new("Result:"))
.child(Label::new(output)),
),
ToolUseStatus::Error(err) => parent.child(
v_flex()
.gap_0p5()
.py_1()
.px_2p5()
.child(Label::new("Error:"))
.child(Label::new(err)),
),
ToolUseStatus::Pending | ToolUseStatus::Running => parent,
}),
)
}),
)
}
}
impl Render for ActiveThread {

View File

@@ -16,7 +16,6 @@ mod terminal_inline_assistant;
mod thread;
mod thread_history;
mod thread_store;
mod tool_use;
mod ui;
use std::sync::Arc;
@@ -39,6 +38,7 @@ actions!(
NewThread,
NewPromptEditor,
ToggleContextPicker,
ToggleModelSelector,
RemoveAllContext,
OpenHistory,
OpenConfiguration,

View File

@@ -1,7 +1,7 @@
use std::sync::Arc;
use collections::HashMap;
use gpui::{Action, AnyView, App, EventEmitter, FocusHandle, Focusable, Subscription};
use gpui::{AnyView, App, EventEmitter, FocusHandle, Focusable, Subscription};
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
use ui::{prelude::*, Divider, DividerColor, ElevationIndex};
use zed_actions::assistant::DeployPromptLibrary;
@@ -158,16 +158,8 @@ impl Render for AssistantConfiguration {
.child(
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
.gap_2()
.child(
v_flex()
.gap_0p5()
.child(Headline::new("Prompt Library").size(HeadlineSize::Small))
.child(
Label::new("Create reusable prompts and tag which ones you want sent in every LLM interaction.")
.color(Color::Muted),
),
)
.gap_1()
.child(Headline::new("Prompt Library").size(HeadlineSize::Small))
.child(
Button::new("open-prompt-library", "Open Prompt Library")
.style(ButtonStyle::Filled)
@@ -176,8 +168,8 @@ impl Render for AssistantConfiguration {
.icon(IconName::Book)
.icon_size(IconSize::Small)
.icon_position(IconPosition::Start)
.on_click(|_event, window, cx| {
window.dispatch_action(DeployPromptLibrary.boxed_clone(), cx)
.on_click(|_event, _window, cx| {
cx.dispatch_action(&DeployPromptLibrary)
}),
),
)

View File

@@ -1,19 +1,24 @@
use assistant_settings::AssistantSettings;
use fs::Fs;
use gpui::{Entity, FocusHandle};
use language_model_selector::{AssistantLanguageModelSelector, LanguageModelSelector};
use gpui::{Entity, FocusHandle, SharedString};
use language_model::LanguageModelRegistry;
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use settings::update_settings_file;
use std::sync::Arc;
use ui::prelude::*;
use ui::{prelude::*, ButtonLike, PopoverMenuHandle, Tooltip};
use crate::ToggleModelSelector;
pub struct AssistantModelSelector {
pub selector: Entity<LanguageModelSelector>,
selector: Entity<LanguageModelSelector>,
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
focus_handle: FocusHandle,
}
impl AssistantModelSelector {
pub(crate) fn new(
fs: Arc<dyn Fs>,
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
focus_handle: FocusHandle,
window: &mut Window,
cx: &mut App,
@@ -33,14 +38,50 @@ impl AssistantModelSelector {
cx,
)
}),
menu_handle,
focus_handle,
}
}
}
impl Render for AssistantModelSelector {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
AssistantLanguageModelSelector::new(self.focus_handle.clone(), self.selector.clone())
.render(window, cx)
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let active_model = LanguageModelRegistry::read_global(cx).active_model();
let focus_handle = self.focus_handle.clone();
let model_name = match active_model {
Some(model) => model.name().0,
_ => SharedString::from("No model selected"),
};
LanguageModelSelectorPopoverMenu::new(
self.selector.clone(),
ButtonLike::new("active-model")
.style(ButtonStyle::Subtle)
.child(
h_flex()
.gap_0p5()
.child(
Label::new(model_name)
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(
Icon::new(IconName::ChevronDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
),
move |window, cx| {
Tooltip::for_action_in(
"Change Model",
&ToggleModelSelector,
&focus_handle,
window,
cx,
)
},
gpui::Corner::BottomRight,
)
.with_handle(self.menu_handle.clone())
}
}

View File

@@ -14,7 +14,7 @@ use client::zed_urls;
use editor::Editor;
use fs::Fs;
use gpui::{
prelude::*, Action, AnyElement, App, AsyncWindowContext, Corner, Entity, EventEmitter,
prelude::*, px, svg, Action, AnyElement, App, AsyncWindowContext, Corner, Entity, EventEmitter,
FocusHandle, Focusable, FontWeight, Pixels, Subscription, Task, UpdateGlobal, WeakEntity,
};
use language::LanguageRegistry;
@@ -458,12 +458,6 @@ impl AssistantPanel {
pub(crate) fn active_context_editor(&self) -> Option<Entity<ContextEditor>> {
self.context_editor.clone()
}
pub(crate) fn delete_context(&mut self, path: PathBuf, cx: &mut Context<Self>) {
self.context_store
.update(cx, |this, cx| this.delete_local_context(path, cx))
.detach_and_log_err(cx);
}
}
impl Focusable for AssistantPanel {
@@ -596,6 +590,7 @@ impl AssistantPanel {
h_flex()
.id("assistant-toolbar")
.px(DynamicSpacing::Base08.rems(cx))
.h(Tab::container_height(cx))
.flex_none()
.justify_between()
@@ -603,86 +598,72 @@ impl AssistantPanel {
.bg(cx.theme().colors().tab_bar_background)
.border_b_1()
.border_color(cx.theme().colors().border)
.child(
div()
.id("title")
.overflow_x_scroll()
.px(DynamicSpacing::Base08.rems(cx))
.child(Label::new(title).text_ellipsis()),
)
.child(
h_flex()
.h_full()
.pl_2()
.gap_2()
.bg(cx.theme().colors().tab_bar_background)
.w_full()
.gap_1()
.justify_between()
.child(Label::new(title))
.children(if matches!(self.active_view, ActiveView::PromptEditor) {
self.context_editor
.as_ref()
.and_then(|editor| render_remaining_tokens(editor, cx))
} else {
None
})
}),
)
.child(
h_flex()
.h_full()
.pl_1p5()
.border_l_1()
.border_color(cx.theme().colors().border)
.gap(DynamicSpacing::Base02.rems(cx))
.child(
h_flex()
.h_full()
.px(DynamicSpacing::Base08.rems(cx))
.border_l_1()
.border_color(cx.theme().colors().border)
.gap(DynamicSpacing::Base02.rems(cx))
.child(
PopoverMenu::new("assistant-toolbar-new-popover-menu")
.trigger_with_tooltip(
IconButton::new("new", IconName::Plus)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle),
Tooltip::text("New…"),
PopoverMenu::new("assistant-toolbar-new-popover-menu")
.trigger_with_tooltip(
IconButton::new("new", IconName::Plus)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle),
Tooltip::text("New…"),
)
.anchor(Corner::TopRight)
.with_handle(self.new_item_context_menu_handle.clone())
.menu(move |window, cx| {
Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
menu.action("New Thread", NewThread.boxed_clone())
.action("New Prompt Editor", NewPromptEditor.boxed_clone())
}))
}),
)
.child(
IconButton::new("open-history", IconName::HistoryRerun)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.tooltip({
let focus_handle = self.focus_handle(cx);
move |window, cx| {
Tooltip::for_action_in(
"History",
&OpenHistory,
&focus_handle,
window,
cx,
)
.anchor(Corner::TopRight)
.with_handle(self.new_item_context_menu_handle.clone())
.menu(move |window, cx| {
Some(ContextMenu::build(
window,
cx,
|menu, _window, _cx| {
menu.action("New Thread", NewThread.boxed_clone())
.action(
"New Prompt Editor",
NewPromptEditor.boxed_clone(),
)
},
))
}),
)
.child(
IconButton::new("open-history", IconName::HistoryRerun)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.tooltip({
let focus_handle = self.focus_handle(cx);
move |window, cx| {
Tooltip::for_action_in(
"History",
&OpenHistory,
&focus_handle,
window,
cx,
)
}
})
.on_click(move |_event, window, cx| {
window.dispatch_action(OpenHistory.boxed_clone(), cx);
}),
)
.child(
IconButton::new("configure-assistant", IconName::Settings)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.tooltip(Tooltip::text("Assistant Settings"))
.on_click(move |_event, window, cx| {
window.dispatch_action(OpenConfiguration.boxed_clone(), cx);
}),
),
}
})
.on_click(move |_event, window, cx| {
window.dispatch_action(OpenHistory.boxed_clone(), cx);
}),
)
.child(
IconButton::new("configure-assistant", IconName::Settings)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.tooltip(Tooltip::text("Assistant Settings"))
.on_click(move |_event, window, cx| {
window.dispatch_action(OpenConfiguration.boxed_clone(), cx);
}),
),
)
}
@@ -724,11 +705,12 @@ impl AssistantPanel {
) -> impl IntoElement {
let recent_history = self
.history_store
.update(cx, |this, cx| this.recent_entries(6, cx));
.update(cx, |this, cx| this.recent_entries(3, cx));
let create_welcome_heading = || {
h_flex()
.w_full()
.justify_center()
.child(Headline::new("Welcome to the Assistant Panel").size(HeadlineSize::Small))
};
@@ -736,27 +718,36 @@ impl AssistantPanel {
let no_error = configuration_error.is_none();
v_flex()
.p_1p5()
.size_full()
.justify_end()
.gap_1()
.gap_2()
.child(
v_flex().w_full().child(
svg()
.path("icons/logo_96.svg")
.text_color(cx.theme().colors().text)
.w(px(40.))
.h(px(40.))
.mx_auto()
.mb_4(),
),
)
.map(|parent| {
match configuration_error {
Some(ConfigurationError::ProviderNotAuthenticated)
| Some(ConfigurationError::NoProvider) => {
parent.child(
v_flex()
.px_1p5()
.gap_0p5()
.child(create_welcome_heading())
.child(
Label::new(
"To start using the assistant, configure at least one LLM provider.",
)
.color(Color::Muted),
h_flex().mb_2().w_full().justify_center().child(
Label::new(
"To start using the assistant, configure at least one LLM provider.",
)
.color(Color::Muted),
),
)
.child(
h_flex().mt_1().w_full().child(
h_flex().w_full().justify_center().child(
Button::new("open-configuration", "Configure a Provider")
.size(ButtonSize::Compact)
.icon(Some(IconName::Sliders))
@@ -770,7 +761,7 @@ impl AssistantPanel {
)
}
Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => parent
.child(v_flex().px_1p5().gap_0p5().child(create_welcome_heading()).children(
.child(v_flex().gap_0p5().child(create_welcome_heading()).children(
provider.render_accept_terms(
LanguageModelProviderTosView::ThreadEmptyState,
cx,
@@ -781,40 +772,21 @@ impl AssistantPanel {
})
.when(recent_history.is_empty() && no_error, |parent| {
parent.child(v_flex().gap_0p5().child(create_welcome_heading()).child(
Label::new("Start typing to chat with your codebase").color(Color::Muted),
h_flex().w_full().justify_center().child(
Label::new("Start typing to chat with your codebase").color(Color::Muted),
),
))
})
.when(!recent_history.is_empty(), |parent| {
parent
.child(
h_flex()
.pl_1p5()
.pb_1()
.w_full()
.justify_between()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.child(
Label::new("Past Interactions")
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(
Button::new("view-history", "View All")
.style(ButtonStyle::Subtle)
.label_size(LabelSize::Small)
.key_binding(KeyBinding::for_action_in(
&OpenHistory,
&self.focus_handle(cx),
window,
cx,
))
.on_click(move |_event, window, cx| {
window.dispatch_action(OpenHistory.boxed_clone(), cx);
}),
),
h_flex().w_full().justify_center().child(
Label::new("Recent Threads:")
.size(LabelSize::Small)
.color(Color::Muted),
),
)
.child(v_flex().gap_1().children(
.child(v_flex().mx_auto().w_4_5().gap_2().children(
recent_history.into_iter().map(|entry| {
// TODO: Add keyboard navigation.
match entry {
@@ -829,6 +801,22 @@ impl AssistantPanel {
}
}),
))
.child(
h_flex().w_full().justify_center().child(
Button::new("view-all-past-threads", "View All Past Threads")
.style(ButtonStyle::Subtle)
.label_size(LabelSize::Small)
.key_binding(KeyBinding::for_action_in(
&OpenHistory,
&self.focus_handle(cx),
window,
cx,
))
.on_click(move |_event, window, cx| {
window.dispatch_action(OpenHistory.boxed_clone(), cx);
}),
),
)
})
}

View File

@@ -9,9 +9,10 @@ use futures::{channel::mpsc, future::LocalBoxFuture, join, SinkExt, Stream, Stre
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Subscription, Task};
use language::{line_diff, Buffer, IndentKind, Point, TransactionId};
use language_model::{
report_assistant_event, LanguageModel, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, LanguageModelTextStream, Role,
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
LanguageModelTextStream, Role,
};
use language_models::report_assistant_event;
use multi_buffer::MultiBufferRow;
use parking_lot::Mutex;
use prompt_library::PromptBuilder;

View File

@@ -24,7 +24,8 @@ use gpui::{
UpdateGlobal, WeakEntity, Window,
};
use language::{Buffer, Point, Selection, TransactionId};
use language_model::{report_assistant_event, LanguageModelRegistry};
use language_model::LanguageModelRegistry;
use language_models::report_assistant_event;
use multi_buffer::MultiBufferRow;
use parking_lot::Mutex;
use project::{CodeAction, ProjectTransaction};

View File

@@ -6,7 +6,7 @@ use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
use crate::terminal_codegen::TerminalCodegen;
use crate::thread_store::ThreadStore;
use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist};
use crate::{RemoveAllContext, ToggleContextPicker};
use crate::{RemoveAllContext, ToggleContextPicker, ToggleModelSelector};
use client::ErrorExt;
use collections::VecDeque;
use editor::{
@@ -20,6 +20,7 @@ use gpui::{
EventEmitter, FocusHandle, Focusable, FontWeight, Subscription, TextStyle, WeakEntity, Window,
};
use language_model::{LanguageModel, LanguageModelRegistry};
use language_model_selector::LanguageModelSelector;
use parking_lot::Mutex;
use settings::Settings;
use std::cmp;
@@ -39,6 +40,7 @@ pub struct PromptEditor<T> {
context_strip: Entity<ContextStrip>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
model_selector: Entity<AssistantModelSelector>,
model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
edited_since_done: bool,
prompt_history: VecDeque<String>,
prompt_history_ix: Option<usize>,
@@ -102,12 +104,7 @@ impl<T: 'static> Render for PromptEditor<T> {
.items_start()
.cursor(CursorStyle::Arrow)
.on_action(cx.listener(Self::toggle_context_picker))
.on_action(cx.listener(|this, action, window, cx| {
let selector = this.model_selector.read(cx).selector.clone();
selector.update(cx, |selector, cx| {
selector.toggle_model_selector(action, window, cx);
})
}))
.on_action(cx.listener(Self::toggle_model_selector))
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::move_up))
@@ -350,6 +347,15 @@ impl<T: 'static> PromptEditor<T> {
self.context_picker_menu_handle.toggle(window, cx);
}
fn toggle_model_selector(
&mut self,
_: &ToggleModelSelector,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.model_selector_menu_handle.toggle(window, cx);
}
pub fn remove_all_context(
&mut self,
_: &RemoveAllContext,
@@ -858,6 +864,7 @@ impl PromptEditor<BufferCodegen> {
editor
});
let context_picker_menu_handle = PopoverMenuHandle::default();
let model_selector_menu_handle = PopoverMenuHandle::default();
let context_strip = cx.new(|cx| {
ContextStrip::new(
@@ -881,8 +888,15 @@ impl PromptEditor<BufferCodegen> {
context_strip,
context_picker_menu_handle,
model_selector: cx.new(|cx| {
AssistantModelSelector::new(fs, prompt_editor.focus_handle(cx), window, cx)
AssistantModelSelector::new(
fs,
model_selector_menu_handle.clone(),
prompt_editor.focus_handle(cx),
window,
cx,
)
}),
model_selector_menu_handle,
edited_since_done: false,
prompt_history,
prompt_history_ix: None,
@@ -1006,6 +1020,7 @@ impl PromptEditor<TerminalCodegen> {
editor
});
let context_picker_menu_handle = PopoverMenuHandle::default();
let model_selector_menu_handle = PopoverMenuHandle::default();
let context_strip = cx.new(|cx| {
ContextStrip::new(
@@ -1029,8 +1044,15 @@ impl PromptEditor<TerminalCodegen> {
context_strip,
context_picker_menu_handle,
model_selector: cx.new(|cx| {
AssistantModelSelector::new(fs, prompt_editor.focus_handle(cx), window, cx)
AssistantModelSelector::new(
fs,
model_selector_menu_handle.clone(),
prompt_editor.focus_handle(cx),
window,
cx,
)
}),
model_selector_menu_handle,
edited_since_done: false,
prompt_history,
prompt_history_ix: None,

View File

@@ -7,17 +7,16 @@ use gpui::{
pulsating_between, Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription,
TextStyle, WeakEntity,
};
use language_model::LanguageModelRegistry;
use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
use language_model_selector::LanguageModelSelector;
use rope::Point;
use settings::Settings;
use std::time::Duration;
use text::Bias;
use theme::ThemeSettings;
use ui::{
prelude::*, ButtonLike, KeyBinding, PlatformStyle, PopoverMenu, PopoverMenuHandle, Switch,
TintColor, Tooltip,
prelude::*, ButtonLike, KeyBinding, PopoverMenu, PopoverMenuHandle, Switch, TintColor, Tooltip,
};
use vim_mode_setting::VimModeSetting;
use workspace::Workspace;
use crate::assistant_model_selector::AssistantModelSelector;
@@ -26,7 +25,7 @@ use crate::context_store::{refresh_context_store_text, ContextStore};
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
use crate::thread::{RequestKind, Thread};
use crate::thread_store::ThreadStore;
use crate::{Chat, ChatMode, RemoveAllContext, ToggleContextPicker};
use crate::{Chat, ChatMode, RemoveAllContext, ToggleContextPicker, ToggleModelSelector};
pub struct MessageEditor {
thread: Entity<Thread>,
@@ -37,6 +36,7 @@ pub struct MessageEditor {
inline_context_picker: Entity<ContextPicker>,
inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
model_selector: Entity<AssistantModelSelector>,
model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
use_tools: bool,
_subscriptions: Vec<Subscription>,
}
@@ -53,6 +53,7 @@ impl MessageEditor {
let context_store = cx.new(|_cx| ContextStore::new(workspace.clone()));
let context_picker_menu_handle = PopoverMenuHandle::default();
let inline_context_picker_menu_handle = PopoverMenuHandle::default();
let model_selector_menu_handle = PopoverMenuHandle::default();
let editor = cx.new(|cx| {
let mut editor = Editor::auto_height(10, window, cx);
@@ -105,13 +106,30 @@ impl MessageEditor {
context_picker_menu_handle,
inline_context_picker,
inline_context_picker_menu_handle,
model_selector: cx
.new(|cx| AssistantModelSelector::new(fs, editor.focus_handle(cx), window, cx)),
model_selector: cx.new(|cx| {
AssistantModelSelector::new(
fs,
model_selector_menu_handle.clone(),
editor.focus_handle(cx),
window,
cx,
)
}),
model_selector_menu_handle,
use_tools: false,
_subscriptions: subscriptions,
}
}
fn toggle_model_selector(
&mut self,
_: &ToggleModelSelector,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.model_selector_menu_handle.toggle(window, cx)
}
fn toggle_chat_mode(&mut self, _: &ChatMode, _window: &mut Window, cx: &mut Context<Self>) {
self.use_tools = !self.use_tools;
cx.notify();
@@ -187,7 +205,22 @@ impl MessageEditor {
.update(&mut cx, |thread, cx| {
let context = context_store.read(cx).snapshot(cx).collect::<Vec<_>>();
thread.insert_user_message(user_message, context, cx);
thread.send_to_model(model, request_kind, use_tools, cx);
let mut request = thread.to_completion_request(request_kind, cx);
if use_tools {
request.tools = thread
.tools()
.tools(cx)
.into_iter()
.map(|tool| LanguageModelRequestTool {
name: tool.name(),
description: tool.description(),
input_schema: tool.input_schema(),
})
.collect();
}
thread.stream_completion(request, model, cx)
})
.ok();
})
@@ -276,6 +309,7 @@ impl Render for MessageEditor {
let inline_context_picker = self.inline_context_picker.clone();
let bg_color = cx.theme().colors().editor_background;
let is_streaming_completion = self.thread.read(cx).is_streaming();
let button_width = px(64.);
let is_model_selected = self.is_model_selected(cx);
let is_editor_empty = self.is_editor_empty(cx);
let submit_label_color = if is_editor_empty {
@@ -284,25 +318,10 @@ impl Render for MessageEditor {
Color::Default
};
let vim_mode_enabled = VimModeSetting::get_global(cx).0;
let platform = PlatformStyle::platform();
let linux = platform == PlatformStyle::Linux;
let windows = platform == PlatformStyle::Windows;
let button_width = if linux || windows || vim_mode_enabled {
px(92.)
} else {
px(64.)
};
v_flex()
.key_context("MessageEditor")
.on_action(cx.listener(Self::chat))
.on_action(cx.listener(|this, action, window, cx| {
let selector = this.model_selector.read(cx).selector.clone();
selector.update(cx, |this, cx| {
this.toggle_model_selector(action, window, cx);
})
}))
.on_action(cx.listener(Self::toggle_model_selector))
.on_action(cx.listener(Self::toggle_context_picker))
.on_action(cx.listener(Self::remove_all_context))
.on_action(cx.listener(Self::move_up))
@@ -314,7 +333,7 @@ impl Render for MessageEditor {
.child(self.context_strip.clone())
.child(
v_flex()
.gap_5()
.gap_4()
.child({
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {

View File

@@ -2,7 +2,8 @@ use crate::inline_prompt_editor::CodegenStatus;
use client::telemetry::Telemetry;
use futures::{channel::mpsc, SinkExt, StreamExt};
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Task};
use language_model::{report_assistant_event, LanguageModelRegistry, LanguageModelRequest};
use language_model::{LanguageModelRegistry, LanguageModelRequest};
use language_models::report_assistant_event;
use std::{sync::Arc, time::Instant};
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
use terminal::Terminal;
@@ -155,10 +156,7 @@ pub enum CodegenEvent {
Finished,
}
#[cfg(not(target_os = "windows"))]
pub const CLEAR_INPUT: &str = "\x15";
#[cfg(target_os = "windows")]
pub const CLEAR_INPUT: &str = "\x03";
const CARRIAGE_RETURN: &str = "\x0d";
struct TerminalTransaction {

View File

@@ -13,9 +13,9 @@ use fs::Fs;
use gpui::{App, Entity, Focusable, Global, Subscription, UpdateGlobal, WeakEntity};
use language::Buffer;
use language_model::{
report_assistant_event, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, Role,
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
};
use language_models::report_assistant_event;
use prompt_library::PromptBuilder;
use std::sync::Arc;
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};

View File

@@ -4,26 +4,25 @@ use anyhow::Result;
use assistant_tool::ToolWorkingSet;
use chrono::{DateTime, Utc};
use collections::{BTreeMap, HashMap, HashSet};
use futures::StreamExt as _;
use futures::future::Shared;
use futures::{FutureExt as _, StreamExt as _};
use gpui::{App, Context, EventEmitter, SharedString, Task};
use language_model::{
LanguageModel, LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolUseId,
MaxMonthlySpendReachedError, MessageContent, PaymentRequiredError, Role, StopReason,
LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolUse,
LanguageModelToolUseId, MessageContent, Role, StopReason,
};
use language_models::provider::cloud::{MaxMonthlySpendReachedError, PaymentRequiredError};
use serde::{Deserialize, Serialize};
use util::{post_inc, TryFutureExt as _};
use uuid::Uuid;
use crate::context::{attach_context_to_message, ContextId, ContextSnapshot};
use crate::thread_store::SavedThread;
use crate::tool_use::{PendingToolUse, ToolUse, ToolUseState};
#[derive(Debug, Clone, Copy)]
pub enum RequestKind {
Chat,
/// Used when summarizing a thread.
Summarize,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
@@ -42,7 +41,7 @@ impl std::fmt::Display for ThreadId {
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
pub struct MessageId(pub(crate) usize);
pub struct MessageId(usize);
impl MessageId {
fn post_inc(&mut self) -> Self {
@@ -71,7 +70,9 @@ pub struct Thread {
completion_count: usize,
pending_completions: Vec<PendingCompletion>,
tools: Arc<ToolWorkingSet>,
tool_use: ToolUseState,
tool_uses_by_message: HashMap<MessageId, Vec<LanguageModelToolUse>>,
tool_results_by_message: HashMap<MessageId, Vec<LanguageModelToolResult>>,
pending_tool_uses_by_id: HashMap<LanguageModelToolUseId, PendingToolUse>,
}
impl Thread {
@@ -88,7 +89,9 @@ impl Thread {
completion_count: 0,
pending_completions: Vec::new(),
tools,
tool_use: ToolUseState::default(),
tool_uses_by_message: HashMap::default(),
tool_results_by_message: HashMap::default(),
pending_tool_uses_by_id: HashMap::default(),
}
}
@@ -120,7 +123,9 @@ impl Thread {
completion_count: 0,
pending_completions: Vec::new(),
tools,
tool_use: ToolUseState::default(),
tool_uses_by_message: HashMap::default(),
tool_results_by_message: HashMap::default(),
pending_tool_uses_by_id: HashMap::default(),
}
}
@@ -182,15 +187,7 @@ impl Thread {
}
pub fn pending_tool_uses(&self) -> Vec<&PendingToolUse> {
self.tool_use.pending_tool_uses()
}
pub fn tool_uses_for_message(&self, id: MessageId) -> Vec<ToolUse> {
self.tool_use.tool_uses_for_message(id)
}
pub fn message_has_tool_results(&self, message_id: MessageId) -> bool {
self.tool_use.message_has_tool_results(message_id)
self.pending_tool_uses_by_id.values().collect()
}
pub fn insert_user_message(
@@ -244,34 +241,9 @@ impl Thread {
text
}
pub fn send_to_model(
&mut self,
model: Arc<dyn LanguageModel>,
request_kind: RequestKind,
use_tools: bool,
cx: &mut Context<Self>,
) {
let mut request = self.to_completion_request(request_kind, cx);
if use_tools {
request.tools = self
.tools()
.tools(cx)
.into_iter()
.map(|tool| LanguageModelRequestTool {
name: tool.name(),
description: tool.description(),
input_schema: tool.input_schema(),
})
.collect();
}
self.stream_completion(request, model, cx);
}
pub fn to_completion_request(
&self,
request_kind: RequestKind,
_request_kind: RequestKind,
_cx: &App,
) -> LanguageModelRequest {
let mut request = LanguageModelRequest {
@@ -293,13 +265,12 @@ impl Thread {
content: Vec::new(),
cache: false,
};
match request_kind {
RequestKind::Chat => {
self.tool_use
.attach_tool_results(message.id, &mut request_message);
}
RequestKind::Summarize => {
// We don't care about tool use during summarization.
if let Some(tool_results) = self.tool_results_by_message.get(&message.id) {
for tool_result in tool_results {
request_message
.content
.push(MessageContent::ToolResult(tool_result.clone()));
}
}
@@ -309,13 +280,11 @@ impl Thread {
.push(MessageContent::Text(message.text.clone()));
}
match request_kind {
RequestKind::Chat => {
self.tool_use
.attach_tool_uses(message.id, &mut request_message);
}
RequestKind::Summarize => {
// We don't care about tool use during summarization.
if let Some(tool_uses) = self.tool_uses_by_message.get(&message.id) {
for tool_use in tool_uses {
request_message
.content
.push(MessageContent::ToolUse(tool_use.clone()));
}
}
@@ -391,8 +360,21 @@ impl Thread {
.rfind(|message| message.role == Role::Assistant)
{
thread
.tool_use
.request_tool_use(last_assistant_message.id, tool_use);
.tool_uses_by_message
.entry(last_assistant_message.id)
.or_default()
.push(tool_use.clone());
thread.pending_tool_uses_by_id.insert(
tool_use.id.clone(),
PendingToolUse {
assistant_message_id: last_assistant_message.id,
id: tool_use.id,
name: tool_use.name,
input: tool_use.input,
status: PendingToolUseStatus::Idle,
},
);
}
}
}
@@ -469,7 +451,7 @@ impl Thread {
return;
}
let mut request = self.to_completion_request(RequestKind::Summarize, cx);
let mut request = self.to_completion_request(RequestKind::Chat, cx);
request.messages.push(LanguageModelRequestMessage {
role: Role::User,
content: vec![
@@ -512,6 +494,7 @@ impl Thread {
pub fn insert_tool_output(
&mut self,
assistant_message_id: MessageId,
tool_use_id: LanguageModelToolUseId,
output: Task<Result<String>>,
cx: &mut Context<Self>,
@@ -522,18 +505,50 @@ impl Thread {
let output = output.await;
thread
.update(&mut cx, |thread, cx| {
thread
.tool_use
.insert_tool_output(tool_use_id.clone(), output);
// The tool use was requested by an Assistant message,
// so we want to attach the tool results to the next
// user message.
let next_user_message = MessageId(assistant_message_id.0 + 1);
cx.emit(ThreadEvent::ToolFinished { tool_use_id });
let tool_results = thread
.tool_results_by_message
.entry(next_user_message)
.or_default();
match output {
Ok(output) => {
tool_results.push(LanguageModelToolResult {
tool_use_id: tool_use_id.to_string(),
content: output,
is_error: false,
});
cx.emit(ThreadEvent::ToolFinished { tool_use_id });
}
Err(err) => {
tool_results.push(LanguageModelToolResult {
tool_use_id: tool_use_id.to_string(),
content: err.to_string(),
is_error: true,
});
if let Some(tool_use) =
thread.pending_tool_uses_by_id.get_mut(&tool_use_id)
{
tool_use.status = PendingToolUseStatus::Error(err.to_string());
}
}
}
})
.ok();
}
});
self.tool_use
.run_pending_tool(tool_use_id, insert_output_task);
if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) {
tool_use.status = PendingToolUseStatus::Running {
_task: insert_output_task.shared(),
};
}
}
/// Cancels the last pending completion, if there are any pending.
@@ -575,3 +590,26 @@ struct PendingCompletion {
id: usize,
_task: Task<()>,
}
#[derive(Debug, Clone)]
pub struct PendingToolUse {
pub id: LanguageModelToolUseId,
/// The ID of the Assistant message in which the tool use was requested.
pub assistant_message_id: MessageId,
pub name: String,
pub input: serde_json::Value,
pub status: PendingToolUseStatus,
}
#[derive(Debug, Clone)]
pub enum PendingToolUseStatus {
Idle,
Running { _task: Shared<Task<()>> },
Error(#[allow(unused)] String),
}
impl PendingToolUseStatus {
pub fn is_idle(&self) -> bool {
matches!(self, PendingToolUseStatus::Idle)
}
}

View File

@@ -134,13 +134,7 @@ impl ThreadHistory {
})
.ok();
}
HistoryEntry::Context(context) => {
self.assistant_panel
.update(cx, |this, cx| {
this.delete_context(context.path.clone(), cx);
})
.ok();
}
HistoryEntry::Context(_context) => {}
}
cx.notify();
@@ -254,28 +248,18 @@ impl RenderOnce for PastThread {
);
ListItem::new(SharedString::from(self.thread.id.to_string()))
.rounded()
.outlined()
.toggle_state(self.selected)
.spacing(ListItemSpacing::Sparse)
.start_slot(
div()
.max_w_4_5()
.child(Label::new(summary).size(LabelSize::Small).text_ellipsis()),
Icon::new(IconName::MessageCircle)
.size(IconSize::Small)
.color(Color::Muted),
)
.spacing(ListItemSpacing::Sparse)
.child(Label::new(summary).size(LabelSize::Small).text_ellipsis())
.end_slot(
h_flex()
.gap_1p5()
.child(
Label::new("Thread")
.color(Color::Muted)
.size(LabelSize::XSmall),
)
.child(
div()
.size(px(3.))
.rounded_full()
.bg(cx.theme().colors().text_disabled),
)
.child(
Label::new(thread_timestamp)
.color(Color::Muted)
@@ -350,50 +334,21 @@ impl RenderOnce for PastContext {
ListItem::new(SharedString::from(
self.context.path.to_string_lossy().to_string(),
))
.rounded()
.outlined()
.toggle_state(self.selected)
.spacing(ListItemSpacing::Sparse)
.start_slot(
div()
.max_w_4_5()
.child(Label::new(summary).size(LabelSize::Small).text_ellipsis()),
Icon::new(IconName::Code)
.size(IconSize::Small)
.color(Color::Muted),
)
.spacing(ListItemSpacing::Sparse)
.child(Label::new(summary).size(LabelSize::Small).text_ellipsis())
.end_slot(
h_flex()
.gap_1p5()
.child(
Label::new("Prompt Editor")
.color(Color::Muted)
.size(LabelSize::XSmall),
)
.child(
div()
.size(px(3.))
.rounded_full()
.bg(cx.theme().colors().text_disabled),
)
.child(
Label::new(context_timestamp)
.color(Color::Muted)
.size(LabelSize::XSmall),
)
.child(
IconButton::new("delete", IconName::TrashAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.tooltip(Tooltip::text("Delete Prompt Editor"))
.on_click({
let assistant_panel = self.assistant_panel.clone();
let path = self.context.path.clone();
move |_event, _window, cx| {
assistant_panel
.update(cx, |this, cx| {
this.delete_context(path.clone(), cx);
})
.ok();
}
}),
),
h_flex().gap_1p5().child(
Label::new(context_timestamp)
.color(Color::Muted)
.size(LabelSize::XSmall),
),
)
.on_click({
let assistant_panel = self.assistant_panel.clone();

View File

@@ -1,221 +0,0 @@
use std::sync::Arc;
use anyhow::Result;
use collections::HashMap;
use futures::future::Shared;
use futures::FutureExt as _;
use gpui::{SharedString, Task};
use language_model::{
LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolUse,
LanguageModelToolUseId, MessageContent,
};
use crate::thread::MessageId;
#[derive(Debug)]
pub struct ToolUse {
pub id: LanguageModelToolUseId,
pub name: SharedString,
pub status: ToolUseStatus,
pub input: serde_json::Value,
}
#[derive(Debug, Clone)]
pub enum ToolUseStatus {
Pending,
Running,
Finished(SharedString),
Error(SharedString),
}
#[derive(Default)]
pub struct ToolUseState {
tool_uses_by_assistant_message: HashMap<MessageId, Vec<LanguageModelToolUse>>,
tool_uses_by_user_message: HashMap<MessageId, Vec<LanguageModelToolUseId>>,
tool_results: HashMap<LanguageModelToolUseId, LanguageModelToolResult>,
pending_tool_uses_by_id: HashMap<LanguageModelToolUseId, PendingToolUse>,
}
impl ToolUseState {
pub fn pending_tool_uses(&self) -> Vec<&PendingToolUse> {
self.pending_tool_uses_by_id.values().collect()
}
pub fn tool_uses_for_message(&self, id: MessageId) -> Vec<ToolUse> {
let Some(tool_uses_for_message) = &self.tool_uses_by_assistant_message.get(&id) else {
return Vec::new();
};
let mut tool_uses = Vec::new();
for tool_use in tool_uses_for_message.iter() {
let tool_result = self.tool_results.get(&tool_use.id);
let status = (|| {
if let Some(tool_result) = tool_result {
return if tool_result.is_error {
ToolUseStatus::Error(tool_result.content.clone().into())
} else {
ToolUseStatus::Finished(tool_result.content.clone().into())
};
}
if let Some(pending_tool_use) = self.pending_tool_uses_by_id.get(&tool_use.id) {
return match pending_tool_use.status {
PendingToolUseStatus::Idle => ToolUseStatus::Pending,
PendingToolUseStatus::Running { .. } => ToolUseStatus::Running,
PendingToolUseStatus::Error(ref err) => {
ToolUseStatus::Error(err.clone().into())
}
};
}
ToolUseStatus::Pending
})();
tool_uses.push(ToolUse {
id: tool_use.id.clone(),
name: tool_use.name.clone().into(),
input: tool_use.input.clone(),
status,
})
}
tool_uses
}
pub fn message_has_tool_results(&self, message_id: MessageId) -> bool {
self.tool_uses_by_user_message
.get(&message_id)
.map_or(false, |results| !results.is_empty())
}
pub fn request_tool_use(
&mut self,
assistant_message_id: MessageId,
tool_use: LanguageModelToolUse,
) {
self.tool_uses_by_assistant_message
.entry(assistant_message_id)
.or_default()
.push(tool_use.clone());
// The tool use is being requested by the Assistant, so we want to
// attach the tool results to the next user message.
let next_user_message_id = MessageId(assistant_message_id.0 + 1);
self.tool_uses_by_user_message
.entry(next_user_message_id)
.or_default()
.push(tool_use.id.clone());
self.pending_tool_uses_by_id.insert(
tool_use.id.clone(),
PendingToolUse {
assistant_message_id,
id: tool_use.id,
name: tool_use.name,
input: tool_use.input,
status: PendingToolUseStatus::Idle,
},
);
}
pub fn run_pending_tool(&mut self, tool_use_id: LanguageModelToolUseId, task: Task<()>) {
if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) {
tool_use.status = PendingToolUseStatus::Running {
_task: task.shared(),
};
}
}
pub fn insert_tool_output(
&mut self,
tool_use_id: LanguageModelToolUseId,
output: Result<String>,
) {
match output {
Ok(output) => {
self.tool_results.insert(
tool_use_id.clone(),
LanguageModelToolResult {
tool_use_id: tool_use_id.clone(),
content: output.into(),
is_error: false,
},
);
self.pending_tool_uses_by_id.remove(&tool_use_id);
}
Err(err) => {
self.tool_results.insert(
tool_use_id.clone(),
LanguageModelToolResult {
tool_use_id: tool_use_id.clone(),
content: err.to_string().into(),
is_error: true,
},
);
if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) {
tool_use.status = PendingToolUseStatus::Error(err.to_string().into());
}
}
}
}
pub fn attach_tool_uses(
&self,
message_id: MessageId,
request_message: &mut LanguageModelRequestMessage,
) {
if let Some(tool_uses) = self.tool_uses_by_assistant_message.get(&message_id) {
for tool_use in tool_uses {
request_message
.content
.push(MessageContent::ToolUse(tool_use.clone()));
}
}
}
pub fn attach_tool_results(
&self,
message_id: MessageId,
request_message: &mut LanguageModelRequestMessage,
) {
if let Some(tool_uses) = self.tool_uses_by_user_message.get(&message_id) {
for tool_use_id in tool_uses {
if let Some(tool_result) = self.tool_results.get(tool_use_id) {
request_message
.content
.push(MessageContent::ToolResult(tool_result.clone()));
}
}
}
}
}
#[derive(Debug, Clone)]
pub struct PendingToolUse {
pub id: LanguageModelToolUseId,
/// The ID of the Assistant message in which the tool use was requested.
pub assistant_message_id: MessageId,
pub name: Arc<str>,
pub input: serde_json::Value,
pub status: PendingToolUseStatus,
}
#[derive(Debug, Clone)]
pub enum PendingToolUseStatus {
Idle,
Running { _task: Shared<Task<()>> },
Error(#[allow(unused)] Arc<str>),
}
impl PendingToolUseStatus {
pub fn is_idle(&self) -> bool {
matches!(self, PendingToolUseStatus::Idle)
}
pub fn is_error(&self) -> bool {
matches!(self, PendingToolUseStatus::Error(_))
}
}

View File

@@ -30,6 +30,7 @@ indexed_docs.workspace = true
language.workspace = true
language_model.workspace = true
language_model_selector.workspace = true
language_models.workspace = true
log.workspace = true
multi_buffer.workspace = true
open_ai.workspace = true

View File

@@ -19,10 +19,13 @@ use gpui::{
};
use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset};
use language_model::{
report_assistant_event, LanguageModel, LanguageModelCacheConfiguration,
LanguageModelCompletionEvent, LanguageModelImage, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, LanguageModelToolUseId, MaxMonthlySpendReachedError,
MessageContent, PaymentRequiredError, Role, StopReason,
LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionEvent,
LanguageModelImage, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
LanguageModelToolUseId, MessageContent, Role, StopReason,
};
use language_models::{
provider::cloud::{MaxMonthlySpendReachedError, PaymentRequiredError},
report_assistant_event,
};
use open_ai::Model as OpenAiModel;
use paths::contexts_dir;

View File

@@ -29,22 +29,19 @@ use gpui::{
WeakEntity,
};
use indexed_docs::IndexedDocsStore;
use language::{
language_settings::{all_language_settings, SoftWrap},
BufferSnapshot, LspAdapterDelegate, ToOffset,
};
use language::{language_settings::SoftWrap, BufferSnapshot, LspAdapterDelegate, ToOffset};
use language_model::{
LanguageModelImage, LanguageModelProvider, LanguageModelProviderTosView, LanguageModelRegistry,
Role,
};
use language_model_selector::{AssistantLanguageModelSelector, LanguageModelSelector};
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use multi_buffer::MultiBufferRow;
use picker::Picker;
use project::lsp_store::LocalLspAdapterDelegate;
use project::{Project, Worktree};
use rope::Point;
use serde::{Deserialize, Serialize};
use settings::{update_settings_file, Settings, SettingsStore};
use settings::{update_settings_file, Settings};
use std::{any::TypeId, borrow::Cow, cmp, ops::Range, path::PathBuf, sync::Arc, time::Duration};
use text::SelectionGoal;
use ui::{
@@ -80,6 +77,7 @@ actions!(
InsertIntoEditor,
QuoteSelection,
Split,
ToggleModelSelector,
]
);
@@ -196,6 +194,7 @@ pub struct ContextEditor {
// context editor, we keep a reference here.
dragged_file_worktrees: Vec<Entity<Worktree>>,
language_model_selector: Entity<LanguageModelSelector>,
language_model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
}
pub const DEFAULT_TAB_TITLE: &str = "New Chat";
@@ -231,13 +230,6 @@ impl ContextEditor {
editor.set_completion_provider(Some(Box::new(completion_provider)));
editor.set_menu_inline_completions_policy(MenuInlineCompletionsPolicy::Never);
editor.set_collaboration_hub(Box::new(project.clone()));
let show_edit_predictions = all_language_settings(None, cx)
.edit_predictions
.enabled_in_assistant;
editor.set_show_edit_predictions(Some(show_edit_predictions), window, cx);
editor
});
@@ -246,7 +238,6 @@ impl ContextEditor {
cx.subscribe_in(&context, window, Self::handle_context_event),
cx.subscribe_in(&editor, window, Self::handle_editor_event),
cx.subscribe_in(&editor, window, Self::handle_editor_search_event),
cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),
];
let fs_clone = fs.clone();
@@ -264,6 +255,7 @@ impl ContextEditor {
)
});
let language_model_selector_menu_handle = PopoverMenuHandle::default();
let sections = context.read(cx).slash_command_output_sections().to_vec();
let patch_ranges = context.read(cx).patch_ranges().collect::<Vec<_>>();
let slash_commands = context.read(cx).slash_commands().clone();
@@ -289,6 +281,7 @@ impl ContextEditor {
slash_menu_handle: Default::default(),
dragged_file_worktrees: Vec::new(),
language_model_selector,
language_model_selector_menu_handle,
};
this.update_message_headers(cx);
this.update_image_blocks(cx);
@@ -297,16 +290,6 @@ impl ContextEditor {
this
}
fn settings_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.editor.update(cx, |editor, cx| {
let show_edit_predictions = all_language_settings(None, cx)
.edit_predictions
.enabled_in_assistant;
editor.set_show_edit_predictions(Some(show_edit_predictions), window, cx);
});
}
pub fn context(&self) -> &Entity<AssistantContext> {
&self.context
}
@@ -1104,7 +1087,7 @@ impl ContextEditor {
patch: AssistantPatch,
mut cx: AsyncWindowContext,
) -> Result<()> {
let project = this.read_with(&cx, |this, _| this.project.clone())?;
let project = this.update(&mut cx, |this, _| this.project.clone())?;
let resolved_patch = patch.resolve(project.clone(), &mut cx).await;
let editor = cx.new_window_entity(|window, cx| {
@@ -1129,7 +1112,7 @@ impl ContextEditor {
editor
})?;
this.update(&mut cx, |this, _| {
this.update_in(&mut cx, |this, window, cx| {
if let Some(patch_state) = this.patches.get_mut(&patch.range) {
patch_state.editor = Some(PatchEditorState {
editor: editor.downgrade(),
@@ -1137,12 +1120,19 @@ impl ContextEditor {
});
patch_state.update_task.take();
}
this.workspace
.update(cx, |workspace, cx| {
workspace.add_item_to_active_pane(
Box::new(editor.clone()),
None,
false,
window,
cx,
)
})
.log_err();
})?;
this.read_with(&cx, |this, _| this.workspace.clone())?
.update_in(&mut cx, |workspace, window, cx| {
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, false, window, cx)
})
.log_err();
Ok(())
}
@@ -1524,11 +1514,15 @@ impl ContextEditor {
(!text.is_empty()).then_some((text, true))
} else {
let selection = context_editor.selections.newest_adjusted(cx);
let buffer = context_editor.buffer().read(cx).snapshot(cx);
let selected_text = buffer.text_for_range(selection.range()).collect::<String>();
let anchor = context_editor.selections.newest_anchor();
let text = context_editor
.buffer()
.read(cx)
.read(cx)
.text_for_range(anchor.range())
.collect::<String>();
(!selected_text.is_empty()).then_some((selected_text, false))
(!text.is_empty()).then_some((text, false))
}
})
}
@@ -1783,16 +1777,23 @@ impl ContextEditor {
&mut self,
cx: &mut Context<Self>,
) -> (String, CopyMetadata, Vec<text::Selection<usize>>) {
let (selection, creases) = self.editor.update(cx, |editor, cx| {
let mut selection = editor.selections.newest_adjusted(cx);
let (snapshot, selection, creases) = self.editor.update(cx, |editor, cx| {
let mut selection = editor.selections.newest::<Point>(cx);
let snapshot = editor.buffer().read(cx).snapshot(cx);
selection.goal = SelectionGoal::None;
let is_entire_line = selection.is_empty() || editor.selections.line_mode;
if is_entire_line {
selection.start = Point::new(selection.start.row, 0);
selection.end =
cmp::min(snapshot.max_point(), Point::new(selection.start.row + 1, 0));
selection.goal = SelectionGoal::None;
}
let selection_start = snapshot.point_to_offset(selection.start);
(
selection.map(|point| snapshot.point_to_offset(point)),
snapshot.clone(),
selection.clone(),
editor.display_map.update(cx, |display_map, cx| {
display_map
.snapshot(cx)
@@ -1832,6 +1833,7 @@ impl ContextEditor {
)
});
let selection = selection.map(|point| snapshot.point_to_offset(point));
let context = self.context.read(cx);
let mut text = String::new();
@@ -2041,6 +2043,15 @@ impl ContextEditor {
});
}
fn toggle_model_selector(
&mut self,
_: &ToggleModelSelector,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.language_model_selector_menu_handle.toggle(window, cx);
}
fn save(&mut self, _: &Save, _window: &mut Window, cx: &mut Context<Self>) {
self.context.update(cx, |context, cx| {
context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx)
@@ -2388,6 +2399,46 @@ impl ContextEditor {
)
}
fn render_language_model_selector(&self, cx: &mut Context<Self>) -> impl IntoElement {
let active_model = LanguageModelRegistry::read_global(cx).active_model();
let focus_handle = self.editor().focus_handle(cx).clone();
let model_name = match active_model {
Some(model) => model.name().0,
None => SharedString::from("No model selected"),
};
LanguageModelSelectorPopoverMenu::new(
self.language_model_selector.clone(),
ButtonLike::new("active-model")
.style(ButtonStyle::Subtle)
.child(
h_flex()
.gap_0p5()
.child(
Label::new(model_name)
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(
Icon::new(IconName::ChevronDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
),
move |window, cx| {
Tooltip::for_action_in(
"Change Model",
&ToggleModelSelector,
&focus_handle,
window,
cx,
)
},
gpui::Corner::BottomLeft,
)
.with_handle(self.language_model_selector_menu_handle.clone())
}
fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
let last_error = self.last_error.as_ref()?;
@@ -2832,7 +2883,6 @@ impl Render for ContextEditor {
None
};
let language_model_selector = self.language_model_selector.clone();
v_flex()
.key_context("ContextEditor")
.capture_action(cx.listener(ContextEditor::cancel))
@@ -2845,11 +2895,7 @@ impl Render for ContextEditor {
.on_action(cx.listener(ContextEditor::edit))
.on_action(cx.listener(ContextEditor::assist))
.on_action(cx.listener(ContextEditor::split))
.on_action(move |action, window, cx| {
language_model_selector.update(cx, |this, cx| {
this.toggle_model_selector(action, window, cx);
})
})
.on_action(cx.listener(ContextEditor::toggle_model_selector))
.size_full()
.children(self.render_notice(cx))
.child(
@@ -2887,14 +2933,11 @@ impl Render for ContextEditor {
.gap_1()
.child(self.render_inject_context_menu(cx))
.child(ui::Divider::vertical())
.child(div().pl_0p5().child({
let focus_handle = self.editor().focus_handle(cx).clone();
AssistantLanguageModelSelector::new(
focus_handle,
self.language_model_selector.clone(),
)
.render(window, cx)
})),
.child(
div()
.pl_0p5()
.child(self.render_language_model_selector(cx)),
),
)
.child(
h_flex()

View File

@@ -9,7 +9,7 @@ use clock::ReplicaId;
use collections::HashMap;
use context_server::manager::ContextServerManager;
use context_server::ContextServerFactoryRegistry;
use fs::{Fs, RemoveOptions};
use fs::Fs;
use futures::StreamExt;
use fuzzy::StringMatchCandidate;
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Task, WeakEntity};
@@ -475,38 +475,6 @@ impl ContextStore {
})
}
pub fn delete_local_context(
&mut self,
path: PathBuf,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let fs = self.fs.clone();
cx.spawn(|this, mut cx| async move {
fs.remove_file(
&path,
RemoveOptions {
recursive: false,
ignore_if_not_exists: true,
},
)
.await?;
this.update(&mut cx, |this, cx| {
this.contexts.retain(|context| {
context
.upgrade()
.and_then(|context| context.read(cx).path())
!= Some(&path)
});
this.contexts_metadata
.retain(|context| context.path != path);
})?;
Ok(())
})
}
fn loaded_context_for_path(&self, path: &Path, cx: &App) -> Option<Entity<AssistantContext>> {
self.contexts.iter().find_map(|context| {
let context = context.upgrade()?;

View File

@@ -359,7 +359,6 @@ fn providers_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema:
schemars::schema::SchemaObject {
enum_values: Some(vec![
"anthropic".into(),
"bedrock".into(),
"google".into(),
"lmstudio".into(),
"ollama".into(),

View File

@@ -21,11 +21,11 @@ impl SlashCommand for DefaultSlashCommand {
}
fn description(&self) -> String {
"Insert default prompt".into()
"insert default prompt".into()
}
fn menu_text(&self) -> String {
self.description()
"Insert Default Prompt".into()
}
fn requires_argument(&self) -> bool {

View File

@@ -7,10 +7,8 @@ use strum::EnumIter;
pub enum Model {
// Anthropic models (already included)
#[default]
#[serde(rename = "claude-3-5-sonnet-v2", alias = "claude-3-5-sonnet-latest")]
#[serde(rename = "claude-3-5-sonnet", alias = "claude-3-5-sonnet-latest")]
Claude3_5Sonnet,
#[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")]
Claude3_7Sonnet,
#[serde(rename = "claude-3-opus", alias = "claude-3-opus-latest")]
Claude3Opus,
#[serde(rename = "claude-3-sonnet", alias = "claude-3-sonnet-latest")]
@@ -66,7 +64,7 @@ pub enum Model {
impl Model {
pub fn from_id(id: &str) -> anyhow::Result<Self> {
if id.starts_with("claude-3-5-sonnet-v2") {
if id.starts_with("claude-3-5-sonnet") {
Ok(Self::Claude3_5Sonnet)
} else if id.starts_with("claude-3-opus") {
Ok(Self::Claude3Opus)
@@ -74,8 +72,6 @@ impl Model {
Ok(Self::Claude3Sonnet)
} else if id.starts_with("claude-3-5-haiku") {
Ok(Self::Claude3_5Haiku)
} else if id.starts_with("claude-3-7-sonnet") {
Ok(Self::Claude3_7Sonnet)
} else {
Err(anyhow!("invalid model id"))
}
@@ -87,7 +83,6 @@ impl Model {
Model::Claude3Opus => "us.anthropic.claude-3-opus-20240229-v1:0",
Model::Claude3Sonnet => "us.anthropic.claude-3-sonnet-20240229-v1:0",
Model::Claude3_5Haiku => "us.anthropic.claude-3-5-haiku-20241022-v1:0",
Model::Claude3_7Sonnet => "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
Model::AmazonNovaLite => "us.amazon.nova-lite-v1:0",
Model::AmazonNovaMicro => "us.amazon.nova-micro-v1:0",
Model::AmazonNovaPro => "us.amazon.nova-pro-v1:0",
@@ -125,11 +120,10 @@ impl Model {
pub fn display_name(&self) -> &str {
match self {
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet v2",
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
Self::Claude3Opus => "Claude 3 Opus",
Self::Claude3Sonnet => "Claude 3 Sonnet",
Self::Claude3_5Haiku => "Claude 3.5 Haiku",
Self::Claude3_7Sonnet => "Claude 3.7 Sonnet",
Self::AmazonNovaLite => "Amazon Nova Lite",
Self::AmazonNovaMicro => "Amazon Nova Micro",
Self::AmazonNovaPro => "Amazon Nova Pro",
@@ -172,8 +166,7 @@ impl Model {
Self::Claude3_5Sonnet
| Self::Claude3Opus
| Self::Claude3Sonnet
| Self::Claude3_5Haiku
| Self::Claude3_7Sonnet => 200_000,
| Self::Claude3_5Haiku => 200_000,
Self::Custom { max_tokens, .. } => *max_tokens,
_ => 200_000,
}
@@ -195,8 +188,7 @@ impl Model {
Self::Claude3_5Sonnet
| Self::Claude3Opus
| Self::Claude3Sonnet
| Self::Claude3_5Haiku
| Self::Claude3_7Sonnet => 1.0,
| Self::Claude3_5Haiku => 1.0,
Self::Custom {
default_temperature,
..

View File

@@ -81,7 +81,7 @@ impl Render for Breadcrumbs {
}
text_style.color = Color::Muted.color(cx);
StyledText::new(segment.text.replace('\n', ""))
StyledText::new(segment.text.replace('\n', ""))
.with_highlights(&text_style, segment.highlights.unwrap_or_default())
.into_any()
});

View File

@@ -3,8 +3,7 @@ use git2::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter};
use language::{Language, LanguageRegistry};
use rope::Rope;
use std::cmp::Ordering;
use std::{future::Future, iter, ops::Range, sync::Arc};
use std::{cmp, future::Future, iter, ops::Range, sync::Arc};
use sum_tree::SumTree;
use text::ToOffset as _;
use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point};
@@ -69,6 +68,7 @@ pub struct DiffHunk {
/// The range in the buffer's diff base text to which this hunk corresponds.
pub diff_base_byte_range: Range<usize>,
pub secondary_status: DiffHunkSecondaryStatus,
pub secondary_diff_base_byte_range: Option<Range<usize>>,
}
/// We store [`InternalDiffHunk`]s internally so we don't need to store the additional row range.
@@ -109,18 +109,13 @@ impl sum_tree::Summary for DiffHunkSummary {
}
}
impl sum_tree::SeekTarget<'_, DiffHunkSummary, DiffHunkSummary> for Anchor {
fn cmp(&self, cursor_location: &DiffHunkSummary, buffer: &text::BufferSnapshot) -> Ordering {
if self
.cmp(&cursor_location.buffer_range.start, buffer)
.is_lt()
{
Ordering::Less
} else if self.cmp(&cursor_location.buffer_range.end, buffer).is_gt() {
Ordering::Greater
} else {
Ordering::Equal
}
impl<'a> sum_tree::SeekTarget<'a, DiffHunkSummary, DiffHunkSummary> for Anchor {
fn cmp(
&self,
cursor_location: &DiffHunkSummary,
buffer: &text::BufferSnapshot,
) -> cmp::Ordering {
self.cmp(&cursor_location.buffer_range.end, buffer)
}
}
@@ -176,110 +171,97 @@ impl BufferDiffSnapshot {
}
}
pub fn new_secondary_text_for_stage_or_unstage(
fn buffer_range_to_unchanged_diff_base_range(
&self,
buffer_range: Range<Anchor>,
buffer: &text::BufferSnapshot,
) -> Option<Range<usize>> {
let mut hunks = self.inner.hunks.iter();
let mut start = 0;
let mut pos = buffer.anchor_before(0);
while let Some(hunk) = hunks.next() {
assert!(buffer_range.start.cmp(&pos, buffer).is_ge());
assert!(hunk.buffer_range.start.cmp(&pos, buffer).is_ge());
if hunk
.buffer_range
.start
.cmp(&buffer_range.end, buffer)
.is_ge()
{
// target buffer range is contained in the unchanged stretch leading up to this next hunk,
// so do a final adjustment based on that
break;
}
// if the target buffer range intersects this hunk at all, no dice
if buffer_range
.start
.cmp(&hunk.buffer_range.end, buffer)
.is_lt()
{
return None;
}
start += hunk.buffer_range.start.to_offset(buffer) - pos.to_offset(buffer);
start += hunk.diff_base_byte_range.end - hunk.diff_base_byte_range.start;
pos = hunk.buffer_range.end;
}
start += buffer_range.start.to_offset(buffer) - pos.to_offset(buffer);
let end = start + buffer_range.end.to_offset(buffer) - buffer_range.start.to_offset(buffer);
Some(start..end)
}
pub fn secondary_edits_for_stage_or_unstage(
&self,
stage: bool,
hunks: impl Iterator<Item = (Range<Anchor>, Range<usize>)>,
hunks: impl Iterator<Item = (Range<usize>, Option<Range<usize>>, Range<Anchor>)>,
buffer: &text::BufferSnapshot,
cx: &mut App,
) -> Option<Rope> {
let secondary_diff = self.secondary_diff()?;
let head_text = self.base_text().map(|text| text.as_rope().clone());
let index_text = secondary_diff
.base_text()
.map(|text| text.as_rope().clone());
let (index_text, head_text) = match (index_text, head_text) {
(Some(index_text), Some(head_text)) => (index_text, head_text),
// file is deleted in both index and head
(None, None) => return None,
// file is deleted in index
(None, Some(head_text)) => {
return if stage {
Some(buffer.as_rope().clone())
} else {
Some(head_text)
}
}
// file exists in the index, but is deleted in head
(Some(_), None) => {
return if stage {
Some(buffer.as_rope().clone())
} else {
None
}
}
) -> Vec<(Range<usize>, String)> {
let Some(secondary_diff) = self.secondary_diff() else {
log::debug!("no secondary diff");
return Vec::new();
};
let mut secondary_cursor = secondary_diff.inner.hunks.cursor::<DiffHunkSummary>(buffer);
secondary_cursor.next(buffer);
let index_base = secondary_diff.base_text().map_or_else(
|| Rope::from(""),
|snapshot| snapshot.text.as_rope().clone(),
);
let head_base = self.base_text().map_or_else(
|| Rope::from(""),
|snapshot| snapshot.text.as_rope().clone(),
);
log::debug!("original: {:?}", index_base.to_string());
let mut edits = Vec::new();
let mut prev_secondary_hunk_buffer_offset = 0;
let mut prev_secondary_hunk_base_text_offset = 0;
for (buffer_range, diff_base_byte_range) in hunks {
let skipped_hunks = secondary_cursor.slice(&buffer_range.start, Bias::Left, buffer);
if let Some(secondary_hunk) = skipped_hunks.last() {
prev_secondary_hunk_base_text_offset = secondary_hunk.diff_base_byte_range.end;
prev_secondary_hunk_buffer_offset =
secondary_hunk.buffer_range.end.to_offset(buffer);
}
let mut buffer_offset_range = buffer_range.to_offset(buffer);
let start_overshoot = buffer_offset_range.start - prev_secondary_hunk_buffer_offset;
let mut secondary_base_text_start =
prev_secondary_hunk_base_text_offset + start_overshoot;
while let Some(secondary_hunk) = secondary_cursor.item().filter(|item| {
item.buffer_range
.start
.cmp(&buffer_range.end, buffer)
.is_le()
}) {
let secondary_hunk_offset_range = secondary_hunk.buffer_range.to_offset(buffer);
prev_secondary_hunk_base_text_offset = secondary_hunk.diff_base_byte_range.end;
prev_secondary_hunk_buffer_offset = secondary_hunk_offset_range.end;
secondary_base_text_start =
secondary_base_text_start.min(secondary_hunk.diff_base_byte_range.start);
buffer_offset_range.start = buffer_offset_range
.start
.min(secondary_hunk_offset_range.start);
secondary_cursor.next(buffer);
}
let end_overshoot = buffer_offset_range
.end
.saturating_sub(prev_secondary_hunk_buffer_offset);
let secondary_base_text_end = prev_secondary_hunk_base_text_offset + end_overshoot;
let secondary_base_text_range = secondary_base_text_start..secondary_base_text_end;
buffer_offset_range.end = buffer_offset_range
.end
.max(prev_secondary_hunk_buffer_offset);
let replacement_text = if stage {
for (diff_base_byte_range, secondary_diff_base_byte_range, buffer_range) in hunks {
let (index_byte_range, replacement_text) = if stage {
log::debug!("staging");
buffer
.text_for_range(buffer_offset_range)
.collect::<String>()
let mut replacement_text = String::new();
let Some(index_byte_range) = secondary_diff_base_byte_range.clone() else {
log::debug!("not a stageable hunk");
continue;
};
log::debug!("using {:?}", index_byte_range);
for chunk in buffer.text_for_range(buffer_range.clone()) {
replacement_text.push_str(chunk);
}
(index_byte_range, replacement_text)
} else {
log::debug!("unstaging");
head_text
.chunks_in_range(diff_base_byte_range.clone())
.collect::<String>()
let mut replacement_text = String::new();
let Some(index_byte_range) = secondary_diff
.buffer_range_to_unchanged_diff_base_range(buffer_range.clone(), &buffer)
else {
log::debug!("not an unstageable hunk");
continue;
};
for chunk in head_base.chunks_in_range(diff_base_byte_range.clone()) {
replacement_text.push_str(chunk);
}
(index_byte_range, replacement_text)
};
edits.push((secondary_base_text_range, replacement_text));
edits.push((index_byte_range, replacement_text));
}
let buffer = cx.new(|cx| {
language::Buffer::local_normalized(index_text, text::LineEnding::default(), cx)
});
let new_text = buffer.update(cx, |buffer, cx| {
buffer.edit(edits, None, cx);
buffer.as_rope().clone()
});
Some(new_text)
log::debug!("edits: {edits:?}");
edits
}
}
@@ -340,12 +322,13 @@ impl BufferDiffInner {
}
let mut secondary_status = DiffHunkSecondaryStatus::None;
let mut secondary_diff_base_byte_range = None;
if let Some(secondary_cursor) = secondary_cursor.as_mut() {
if start_anchor
.cmp(&secondary_cursor.start().buffer_range.start, buffer)
.is_gt()
{
secondary_cursor.seek_forward(&start_anchor, Bias::Left, buffer);
secondary_cursor.seek_forward(&end_anchor, Bias::Left, buffer);
}
if let Some(secondary_hunk) = secondary_cursor.item() {
@@ -356,12 +339,12 @@ impl BufferDiffInner {
}
if secondary_range == (start_point..end_point) {
secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
secondary_diff_base_byte_range =
Some(secondary_hunk.diff_base_byte_range.clone());
} else if secondary_range.start <= end_point {
secondary_status = DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk;
}
}
} else {
log::debug!("no secondary cursor!!");
}
return Some(DiffHunk {
@@ -369,6 +352,7 @@ impl BufferDiffInner {
diff_base_byte_range: start_base..end_base,
buffer_range: start_anchor..end_anchor,
secondary_status,
secondary_diff_base_byte_range,
});
})
}
@@ -403,6 +387,7 @@ impl BufferDiffInner {
buffer_range: hunk.buffer_range.clone(),
// The secondary status is not used by callers of this method.
secondary_status: DiffHunkSecondaryStatus::None,
secondary_diff_base_byte_range: None,
})
})
}
@@ -423,12 +408,12 @@ impl BufferDiffInner {
.start
.cmp(&old_hunk.buffer_range.start, new_snapshot)
{
Ordering::Less => {
cmp::Ordering::Less => {
start.get_or_insert(new_hunk.buffer_range.start);
end.replace(new_hunk.buffer_range.end);
new_cursor.next(new_snapshot);
}
Ordering::Equal => {
cmp::Ordering::Equal => {
if new_hunk != old_hunk {
start.get_or_insert(new_hunk.buffer_range.start);
if old_hunk
@@ -446,7 +431,7 @@ impl BufferDiffInner {
new_cursor.next(new_snapshot);
old_cursor.next(new_snapshot);
}
Ordering::Greater => {
cmp::Ordering::Greater => {
start.get_or_insert(old_hunk.buffer_range.start);
end.replace(old_hunk.buffer_range.end);
old_cursor.next(new_snapshot);
@@ -1074,7 +1059,6 @@ mod tests {
use rand::{rngs::StdRng, Rng as _};
use text::{Buffer, BufferId, Rope};
use unindent::Unindent as _;
use util::test::marked_text_ranges;
#[ctor::ctor]
fn init_logger() {
@@ -1273,208 +1257,6 @@ mod tests {
);
}
#[gpui::test]
async fn test_stage_hunk(cx: &mut TestAppContext) {
struct Example {
name: &'static str,
head_text: String,
index_text: String,
buffer_marked_text: String,
final_index_text: String,
}
let table = [
Example {
name: "uncommitted hunk straddles end of unstaged hunk",
head_text: "
one
two
three
four
five
"
.unindent(),
index_text: "
one
TWO_HUNDRED
three
FOUR_HUNDRED
five
"
.unindent(),
buffer_marked_text: "
ZERO
one
two
«THREE_HUNDRED
FOUR_HUNDRED»
five
SIX
"
.unindent(),
final_index_text: "
one
two
THREE_HUNDRED
FOUR_HUNDRED
five
"
.unindent(),
},
Example {
name: "uncommitted hunk straddles start of unstaged hunk",
head_text: "
one
two
three
four
five
"
.unindent(),
index_text: "
one
TWO_HUNDRED
three
FOUR_HUNDRED
five
"
.unindent(),
buffer_marked_text: "
ZERO
one
«TWO_HUNDRED
THREE_HUNDRED»
four
five
SIX
"
.unindent(),
final_index_text: "
one
TWO_HUNDRED
THREE_HUNDRED
four
five
"
.unindent(),
},
Example {
name: "uncommitted hunk strictly contains unstaged hunks",
head_text: "
one
two
three
four
five
six
seven
"
.unindent(),
index_text: "
one
TWO
THREE
FOUR
FIVE
SIX
seven
"
.unindent(),
buffer_marked_text: "
one
TWO
«THREE_HUNDRED
FOUR
FIVE_HUNDRED»
SIX
seven
"
.unindent(),
final_index_text: "
one
TWO
THREE_HUNDRED
FOUR
FIVE_HUNDRED
SIX
seven
"
.unindent(),
},
Example {
name: "uncommitted deletion hunk",
head_text: "
one
two
three
four
five
"
.unindent(),
index_text: "
one
two
three
four
five
"
.unindent(),
buffer_marked_text: "
one
ˇfive
"
.unindent(),
final_index_text: "
one
five
"
.unindent(),
},
];
for example in table {
let (buffer_text, ranges) = marked_text_ranges(&example.buffer_marked_text, false);
let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
let uncommitted_diff =
BufferDiff::build_sync(buffer.clone(), example.head_text.clone(), cx);
let unstaged_diff =
BufferDiff::build_sync(buffer.clone(), example.index_text.clone(), cx);
let uncommitted_diff = BufferDiffSnapshot {
inner: uncommitted_diff,
secondary_diff: Some(Box::new(BufferDiffSnapshot {
inner: unstaged_diff,
is_single_insertion: false,
secondary_diff: None,
})),
is_single_insertion: false,
};
let range = buffer.anchor_before(ranges[0].start)..buffer.anchor_before(ranges[0].end);
let new_index_text = cx
.update(|cx| {
uncommitted_diff.new_secondary_text_for_stage_or_unstage(
true,
uncommitted_diff
.hunks_intersecting_range(range, &buffer)
.map(|hunk| {
(hunk.buffer_range.clone(), hunk.diff_base_byte_range.clone())
}),
&buffer,
cx,
)
})
.unwrap()
.to_string();
pretty_assertions::assert_eq!(
new_index_text,
example.final_index_text,
"example: {}",
example.name
);
}
}
#[gpui::test]
async fn test_buffer_diff_compare(cx: &mut TestAppContext) {
let base_text = "
@@ -1600,7 +1382,7 @@ mod tests {
}
#[gpui::test(iterations = 100)]
async fn test_staging_and_unstaging_hunks(cx: &mut TestAppContext, mut rng: StdRng) {
async fn test_secondary_edits_for_stage_unstage(cx: &mut TestAppContext, mut rng: StdRng) {
fn gen_line(rng: &mut StdRng) -> String {
if rng.gen_bool(0.2) {
"\n".to_owned()
@@ -1665,7 +1447,7 @@ mod tests {
fn uncommitted_diff(
working_copy: &language::BufferSnapshot,
index_text: &Rope,
index_text: &Entity<language::Buffer>,
head_text: String,
cx: &mut TestAppContext,
) -> BufferDiff {
@@ -1674,7 +1456,7 @@ mod tests {
buffer_id: working_copy.remote_id(),
inner: BufferDiff::build_sync(
working_copy.text.clone(),
index_text.to_string(),
index_text.read_with(cx, |index_text, _| index_text.text()),
cx,
),
secondary_diff: None,
@@ -1705,11 +1487,17 @@ mod tests {
)
});
let working_copy = working_copy.read_with(cx, |working_copy, _| working_copy.snapshot());
let mut index_text = if rng.gen() {
Rope::from(head_text.as_str())
} else {
working_copy.as_rope().clone()
};
let index_text = cx.new(|cx| {
language::Buffer::local_normalized(
if rng.gen() {
Rope::from(head_text.as_str())
} else {
working_copy.as_rope().clone()
},
text::LineEnding::default(),
cx,
)
});
let mut diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx);
let mut hunks = cx.update(|cx| {
@@ -1723,29 +1511,37 @@ mod tests {
for _ in 0..operations {
let i = rng.gen_range(0..hunks.len());
let hunk = &mut hunks[i];
let stage = match hunk.secondary_status {
DiffHunkSecondaryStatus::HasSecondaryHunk => {
let hunk_fields = (
hunk.diff_base_byte_range.clone(),
hunk.secondary_diff_base_byte_range.clone(),
hunk.buffer_range.clone(),
);
let stage = match (
hunk.secondary_status,
hunk.secondary_diff_base_byte_range.clone(),
) {
(DiffHunkSecondaryStatus::HasSecondaryHunk, Some(_)) => {
hunk.secondary_status = DiffHunkSecondaryStatus::None;
hunk.secondary_diff_base_byte_range = None;
true
}
DiffHunkSecondaryStatus::None => {
(DiffHunkSecondaryStatus::None, None) => {
hunk.secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
// We don't look at this, just notice whether it's Some or not.
hunk.secondary_diff_base_byte_range = Some(17..17);
false
}
_ => unreachable!(),
};
let snapshot = cx.update(|cx| diff.snapshot(cx));
index_text = cx.update(|cx| {
snapshot
.new_secondary_text_for_stage_or_unstage(
stage,
[(hunk.buffer_range.clone(), hunk.diff_base_byte_range.clone())]
.into_iter(),
&working_copy,
cx,
)
.unwrap()
let edits = snapshot.secondary_edits_for_stage_or_unstage(
stage,
[hunk_fields].into_iter(),
&working_copy,
);
index_text.update(cx, |index_text, cx| {
index_text.edit(edits, None, cx);
});
diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx);
@@ -1754,7 +1550,6 @@ mod tests {
.collect::<Vec<_>>()
});
assert_eq!(hunks.len(), found_hunks.len());
for (expected_hunk, found_hunk) in hunks.iter().zip(&found_hunks) {
assert_eq!(
expected_hunk.buffer_range.to_point(&working_copy),
@@ -1765,6 +1560,10 @@ mod tests {
found_hunk.diff_base_byte_range
);
assert_eq!(expected_hunk.secondary_status, found_hunk.secondary_status);
assert_eq!(
expected_hunk.secondary_diff_base_byte_range.is_some(),
found_hunk.secondary_diff_base_byte_range.is_some()
)
}
hunks = found_hunks;
}

View File

@@ -48,7 +48,7 @@ pub struct ChannelPathsInsertGuard<'a> {
channels_by_id: &'a mut BTreeMap<ChannelId, Arc<Channel>>,
}
impl ChannelPathsInsertGuard<'_> {
impl<'a> ChannelPathsInsertGuard<'a> {
pub fn insert(&mut self, channel_proto: proto::Channel) -> bool {
let mut ret = false;
let parent_path = channel_proto
@@ -86,7 +86,7 @@ impl ChannelPathsInsertGuard<'_> {
}
}
impl Drop for ChannelPathsInsertGuard<'_> {
impl<'a> Drop for ChannelPathsInsertGuard<'a> {
fn drop(&mut self) {
self.channels_ordered.sort_by(|a, b| {
let a = channel_path_sorting_key(*a, self.channels_by_id);

View File

@@ -33,13 +33,10 @@ util.workspace = true
tempfile.workspace = true
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
exec.workspace = true
exec.workspace = true
fork.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation.workspace = true
core-services = "0.2"
plist = "1.3"
[target.'cfg(target_os = "windows")'.dependencies]
windows.workspace = true

View File

@@ -521,108 +521,30 @@ mod flatpak {
}
}
// todo("windows")
#[cfg(target_os = "windows")]
mod windows {
use anyhow::Context;
use release_channel::app_identifier;
use windows::{
core::HSTRING,
Win32::{
Foundation::{CloseHandle, GetLastError, ERROR_ALREADY_EXISTS, GENERIC_WRITE},
Storage::FileSystem::{
CreateFileW, WriteFile, FILE_FLAGS_AND_ATTRIBUTES, FILE_SHARE_MODE, OPEN_EXISTING,
},
System::Threading::CreateMutexW,
},
};
use crate::{Detect, InstalledApp};
use std::io;
use std::path::{Path, PathBuf};
use std::path::Path;
use std::process::ExitStatus;
fn check_single_instance() -> bool {
let mutex = unsafe {
CreateMutexW(
None,
false,
&HSTRING::from(format!("{}-Instance-Mutex", app_identifier())),
)
.expect("Unable to create instance sync event")
};
let last_err = unsafe { GetLastError() };
let _ = unsafe { CloseHandle(mutex) };
last_err != ERROR_ALREADY_EXISTS
}
struct App(PathBuf);
struct App;
impl InstalledApp for App {
fn zed_version_string(&self) -> String {
format!(
"Zed {}{}{} {}",
if *release_channel::RELEASE_CHANNEL_NAME == "stable" {
"".to_string()
} else {
format!("{} ", *release_channel::RELEASE_CHANNEL_NAME)
},
option_env!("RELEASE_VERSION").unwrap_or_default(),
match option_env!("ZED_COMMIT_SHA") {
Some(commit_sha) => format!(" {commit_sha} "),
None => "".to_string(),
},
self.0.display(),
)
unimplemented!()
}
fn launch(&self, ipc_url: String) -> anyhow::Result<()> {
if check_single_instance() {
std::process::Command::new(self.0.clone())
.arg(ipc_url)
.spawn()?;
} else {
unsafe {
let pipe = CreateFileW(
&HSTRING::from(format!("\\\\.\\pipe\\{}-Named-Pipe", app_identifier())),
GENERIC_WRITE.0,
FILE_SHARE_MODE::default(),
None,
OPEN_EXISTING,
FILE_FLAGS_AND_ATTRIBUTES::default(),
None,
)?;
let message = ipc_url.as_bytes();
let mut bytes_written = 0;
WriteFile(pipe, Some(message), Some(&mut bytes_written), None)?;
CloseHandle(pipe)?;
}
}
Ok(())
fn launch(&self, _ipc_url: String) -> anyhow::Result<()> {
unimplemented!()
}
fn run_foreground(&self, ipc_url: String) -> io::Result<ExitStatus> {
std::process::Command::new(self.0.clone())
.arg(ipc_url)
.arg("--foreground")
.spawn()?
.wait()
fn run_foreground(&self, _ipc_url: String) -> io::Result<ExitStatus> {
unimplemented!()
}
}
impl Detect {
pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
let path = if let Some(path) = path {
path.to_path_buf().canonicalize()?
} else {
std::env::current_exe()?
.parent()
.context("no parent path for cli")?
.parent()
.context("no parent path for cli folder")?
.join("Zed.exe")
};
Ok(App(path))
pub fn detect(_path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
Ok(App)
}
}
}

View File

@@ -97,7 +97,6 @@ extension.workspace = true
file_finder.workspace = true
fs = { workspace = true, features = ["test-support"] }
git = { workspace = true, features = ["test-support"] }
git_ui = { workspace = true, features = ["test-support"] }
git_hosting_providers.workspace = true
gpui = { workspace = true, features = ["test-support"] }
hyper.workspace = true

View File

@@ -202,7 +202,7 @@ impl Database {
.await
}
pub async fn get_known_extension_versions(&self) -> Result<HashMap<String, Vec<String>>> {
pub async fn get_known_extension_versions<'a>(&self) -> Result<HashMap<String, Vec<String>>> {
self.transaction(|tx| async move {
let mut extension_external_ids_by_id = HashMap::default();

View File

@@ -451,15 +451,19 @@ async fn check_usage_limit(
return Ok(());
}
let user_id = UserId::from_proto(claims.user_id);
let model = state.db.model(provider, model_name)?;
let usage = state
.db
.get_usage(
UserId::from_proto(claims.user_id),
provider,
model_name,
Utc::now(),
)
.await?;
let free_tier = claims.free_tier_monthly_spending_limit();
let spending_this_month = state
.db
.get_user_spending_for_month(user_id, Utc::now())
.await?;
if spending_this_month >= free_tier {
if usage.spending_this_month >= free_tier {
if !claims.has_llm_subscription {
return Err(Error::http(
StatusCode::PAYMENT_REQUIRED,
@@ -467,8 +471,7 @@ async fn check_usage_limit(
));
}
let monthly_spend = spending_this_month.saturating_sub(free_tier);
if monthly_spend >= Cents(claims.max_monthly_spend_in_cents) {
if (usage.spending_this_month - free_tier) >= Cents(claims.max_monthly_spend_in_cents) {
return Err(Error::Http(
StatusCode::FORBIDDEN,
"Maximum spending limit reached for this month.".to_string(),
@@ -493,11 +496,6 @@ async fn check_usage_limit(
model.max_tokens_per_minute as usize / users_in_recent_minutes;
let per_user_max_tokens_per_day = model.max_tokens_per_day as usize / users_in_recent_days;
let usage = state
.db
.get_usage(user_id, provider, model_name, Utc::now())
.await?;
let checks = [
(
usage.requests_this_minute,

View File

@@ -27,7 +27,7 @@ fn authorize_access_to_model(
}
if provider == LanguageModelProvider::Anthropic {
if model == "claude-3-5-sonnet" || model == "claude-3-7-sonnet" {
if model == "claude-3-5-sonnet" {
return Ok(());
}

View File

@@ -975,7 +975,7 @@ impl Server {
}
}
impl Deref for ConnectionPoolGuard<'_> {
impl<'a> Deref for ConnectionPoolGuard<'a> {
type Target = ConnectionPool;
fn deref(&self) -> &Self::Target {
@@ -983,13 +983,13 @@ impl Deref for ConnectionPoolGuard<'_> {
}
}
impl DerefMut for ConnectionPoolGuard<'_> {
impl<'a> DerefMut for ConnectionPoolGuard<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.guard
}
}
impl Drop for ConnectionPoolGuard<'_> {
impl<'a> Drop for ConnectionPoolGuard<'a> {
fn drop(&mut self) {
#[cfg(test)]
self.check_invariants();

View File

@@ -13,7 +13,6 @@ mod channel_message_tests;
mod channel_tests;
mod editor_tests;
mod following_tests;
mod git_tests;
mod integration_tests;
mod notification_tests;
mod random_channel_buffer_tests;

View File

@@ -1537,7 +1537,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
show_parameter_hints: false,
show_other_hints: true,
show_background: false,
toggle_on_modifiers_press: None,
})
});
});
@@ -1553,7 +1552,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
show_parameter_hints: false,
show_other_hints: true,
show_background: false,
toggle_on_modifiers_press: None,
})
});
});
@@ -1772,7 +1770,6 @@ async fn test_inlay_hint_refresh_is_forwarded(
show_parameter_hints: false,
show_other_hints: false,
show_background: false,
toggle_on_modifiers_press: None,
})
});
});
@@ -1788,7 +1785,6 @@ async fn test_inlay_hint_refresh_is_forwarded(
show_parameter_hints: true,
show_other_hints: true,
show_background: false,
toggle_on_modifiers_press: None,
})
});
});

View File

@@ -1,130 +0,0 @@
use std::{
path::{Path, PathBuf},
sync::Arc,
};
use call::ActiveCall;
use git::status::{FileStatus, StatusCode, TrackedStatus};
use git_ui::project_diff::ProjectDiff;
use gpui::{TestAppContext, VisualTestContext};
use project::ProjectPath;
use serde_json::json;
use workspace::Workspace;
//
use crate::tests::TestServer;
#[gpui::test]
async fn test_project_diff(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
let mut server = TestServer::start(cx_a.background_executor.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
cx_a.set_name("cx_a");
cx_b.set_name("cx_b");
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
client_a
.fs()
.insert_tree(
"/a",
json!({
".git": {},
"changed.txt": "after\n",
"unchanged.txt": "unchanged\n",
"created.txt": "created\n",
"secret.pem": "secret-changed\n",
}),
)
.await;
client_a.fs().set_git_content_for_repo(
Path::new("/a/.git"),
&[
("changed.txt".into(), "before\n".to_string(), None),
("unchanged.txt".into(), "unchanged\n".to_string(), None),
("deleted.txt".into(), "deleted\n".to_string(), None),
("secret.pem".into(), "shh\n".to_string(), None),
],
);
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
let active_call_a = cx_a.read(ActiveCall::global);
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
cx_b.update(editor::init);
cx_b.update(git_ui::init);
let project_b = client_b.join_remote_project(project_id, cx_b).await;
let workspace_b = cx_b.add_window(|window, cx| {
Workspace::new(
None,
project_b.clone(),
client_b.app_state.clone(),
window,
cx,
)
});
let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b);
let workspace_b = workspace_b.root(cx_b).unwrap();
cx_b.update(|window, cx| {
window
.focused(cx)
.unwrap()
.dispatch_action(&git_ui::project_diff::Diff, window, cx)
});
let diff = workspace_b.update(cx_b, |workspace, cx| {
workspace.active_item(cx).unwrap().act_as::<ProjectDiff>(cx)
});
let diff = diff.unwrap();
cx_b.run_until_parked();
diff.update(cx_b, |diff, cx| {
assert_eq!(
diff.excerpt_paths(cx),
vec!["changed.txt", "deleted.txt", "created.txt"]
);
});
client_a
.fs()
.insert_tree(
"/a",
json!({
".git": {},
"changed.txt": "before\n",
"unchanged.txt": "changed\n",
"created.txt": "created\n",
"secret.pem": "secret-changed\n",
}),
)
.await;
client_a.fs().recalculate_git_status(Path::new("/a/.git"));
cx_b.run_until_parked();
project_b.update(cx_b, |project, cx| {
let project_path = ProjectPath {
worktree_id,
path: Arc::from(PathBuf::from("unchanged.txt")),
};
let status = project.project_path_git_status(&project_path, cx);
assert_eq!(
status.unwrap(),
FileStatus::Tracked(TrackedStatus {
worktree_status: StatusCode::Modified,
index_status: StatusCode::Unmodified,
})
);
});
diff.update(cx_b, |diff, cx| {
assert_eq!(
diff.excerpt_paths(cx),
vec!["deleted.txt", "unchanged.txt", "created.txt"]
);
});
}

View File

@@ -6354,7 +6354,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
// Open item 1 as preview
workspace
.update_in(cx, |workspace, window, cx| {
workspace.open_path_preview(path_1.clone(), None, true, true, true, window, cx)
workspace.open_path_preview(path_1.clone(), None, true, true, window, cx)
})
.await
.unwrap();
@@ -6375,7 +6375,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
// Open item 2 as preview
workspace
.update_in(cx, |workspace, window, cx| {
workspace.open_path_preview(path_2.clone(), None, true, true, true, window, cx)
workspace.open_path_preview(path_2.clone(), None, true, true, window, cx)
})
.await
.unwrap();
@@ -6507,7 +6507,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
// Open item 2 as preview in right pane
workspace
.update_in(cx, |workspace, window, cx| {
workspace.open_path_preview(path_2.clone(), None, true, true, true, window, cx)
workspace.open_path_preview(path_2.clone(), None, true, true, window, cx)
})
.await
.unwrap();
@@ -6545,7 +6545,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
// Open item 2 as preview in left pane
workspace
.update_in(cx, |workspace, window, cx| {
workspace.open_path_preview(path_2.clone(), None, true, true, true, window, cx)
workspace.open_path_preview(path_2.clone(), None, true, true, window, cx)
})
.await
.unwrap();

View File

@@ -27,7 +27,7 @@ impl<'de> Deserialize<'de> for ChatPanelButton {
{
struct Visitor;
impl serde::de::Visitor<'_> for Visitor {
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = ChatPanelButton;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {

View File

@@ -15,6 +15,7 @@ path = "src/component.rs"
collections.workspace = true
gpui.workspace = true
linkme.workspace = true
once_cell.workspace = true
parking_lot.workspace = true
theme.workspace = true

View File

@@ -1,9 +1,9 @@
use std::ops::{Deref, DerefMut};
use std::sync::LazyLock;
use collections::HashMap;
use gpui::{div, prelude::*, px, AnyElement, App, IntoElement, RenderOnce, SharedString, Window};
use linkme::distributed_slice;
use once_cell::sync::Lazy;
use parking_lot::RwLock;
use theme::ActiveTheme;
@@ -27,8 +27,8 @@ pub static __ALL_COMPONENTS: [fn()] = [..];
#[distributed_slice]
pub static __ALL_PREVIEWS: [fn()] = [..];
pub static COMPONENT_DATA: LazyLock<RwLock<ComponentRegistry>> =
LazyLock::new(|| RwLock::new(ComponentRegistry::new()));
pub static COMPONENT_DATA: Lazy<RwLock<ComponentRegistry>> =
Lazy::new(|| RwLock::new(ComponentRegistry::new()));
pub struct ComponentRegistry {
components: Vec<(Option<&'static str>, &'static str, Option<&'static str>)>,

View File

@@ -14,7 +14,6 @@ path = "src/context_server.rs"
[dependencies]
anyhow.workspace = true
assistant_tool.workspace = true
async-trait.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
context_server_settings.workspace = true

View File

@@ -1,12 +1,16 @@
use anyhow::{anyhow, Context, Result};
use anyhow::{anyhow, Context as _, Result};
use collections::HashMap;
use futures::{channel::oneshot, select, FutureExt, StreamExt};
use futures::{channel::oneshot, io::BufWriter, select, AsyncRead, AsyncWrite, FutureExt};
use gpui::{AppContext as _, AsyncApp, BackgroundExecutor, Task};
use parking_lot::Mutex;
use postage::barrier;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::{value::RawValue, Value};
use smol::channel;
use smol::{
channel,
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
process::Child,
};
use std::{
fmt,
path::PathBuf,
@@ -18,8 +22,6 @@ use std::{
};
use util::TryFutureExt;
use crate::transport::{StdioTransport, Transport};
const JSON_RPC_VERSION: &str = "2.0";
const REQUEST_TIMEOUT: Duration = Duration::from_secs(60);
@@ -53,8 +55,7 @@ pub struct Client {
#[allow(dead_code)]
output_done_rx: Mutex<Option<barrier::Receiver>>,
executor: BackgroundExecutor,
#[allow(dead_code)]
transport: Arc<dyn Transport>,
server: Arc<Mutex<Option<Child>>>,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
@@ -151,13 +152,25 @@ impl Client {
&binary.args
);
let server_name = binary
.executable
.file_name()
.map(|name| name.to_string_lossy().to_string())
.unwrap_or_else(String::new);
let mut command = util::command::new_smol_command(&binary.executable);
command
.args(&binary.args)
.envs(binary.env.unwrap_or_default())
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.kill_on_drop(true);
let transport = Arc::new(StdioTransport::new(binary, &cx)?);
let mut server = command.spawn().with_context(|| {
format!(
"failed to spawn command. (path={:?}, args={:?})",
binary.executable, &binary.args
)
})?;
let stdin = server.stdin.take().unwrap();
let stdout = server.stdout.take().unwrap();
let stderr = server.stderr.take().unwrap();
let (outbound_tx, outbound_rx) = channel::unbounded::<String>();
let (output_done_tx, output_done_rx) = barrier::channel();
@@ -170,22 +183,18 @@ impl Client {
let stdout_input_task = cx.spawn({
let notification_handlers = notification_handlers.clone();
let response_handlers = response_handlers.clone();
let transport = transport.clone();
move |cx| {
Self::handle_input(transport, notification_handlers, response_handlers, cx)
.log_err()
Self::handle_input(stdout, notification_handlers, response_handlers, cx).log_err()
}
});
let stderr_input_task = cx.spawn(|_| Self::handle_stderr(transport.clone()).log_err());
let stderr_input_task = cx.spawn(|_| Self::handle_stderr(stderr).log_err());
let input_task = cx.spawn(|_| async move {
let (stdout, stderr) = futures::join!(stdout_input_task, stderr_input_task);
stdout.or(stderr)
});
let output_task = cx.background_spawn({
let transport = transport.clone();
Self::handle_output(
transport,
stdin,
outbound_rx,
output_done_tx,
response_handlers.clone(),
@@ -193,18 +202,24 @@ impl Client {
.log_err()
});
Ok(Self {
let mut context_server = Self {
server_id,
notification_handlers,
response_handlers,
name: server_name.into(),
name: "".into(),
next_id: Default::default(),
outbound_tx,
executor: cx.background_executor().clone(),
io_tasks: Mutex::new(Some((input_task, output_task))),
output_done_rx: Mutex::new(Some(output_done_rx)),
transport,
})
server: Arc::new(Mutex::new(Some(server))),
};
if let Some(name) = binary.executable.file_name() {
context_server.name = name.to_string_lossy().into();
}
Ok(context_server)
}
/// Handles input from the server's stdout.
@@ -213,53 +228,79 @@ impl Client {
/// parses them as JSON-RPC responses or notifications, and dispatches them
/// to the appropriate handlers. It processes both responses (which are matched
/// to pending requests) and notifications (which trigger registered handlers).
async fn handle_input(
transport: Arc<dyn Transport>,
async fn handle_input<Stdout>(
stdout: Stdout,
notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
response_handlers: Arc<Mutex<Option<HashMap<RequestId, ResponseHandler>>>>,
cx: AsyncApp,
) -> anyhow::Result<()> {
let mut receiver = transport.receive();
) -> anyhow::Result<()>
where
Stdout: AsyncRead + Unpin + Send + 'static,
{
let mut stdout = BufReader::new(stdout);
let mut buffer = String::new();
while let Some(message) = receiver.next().await {
if let Ok(response) = serde_json::from_str::<AnyResponse>(&message) {
if let Some(handlers) = response_handlers.lock().as_mut() {
if let Some(handler) = handlers.remove(&response.id) {
handler(Ok(message.to_string()));
loop {
buffer.clear();
if stdout.read_line(&mut buffer).await? == 0 {
return Ok(());
}
let content = buffer.trim();
if !content.is_empty() {
if let Ok(response) = serde_json::from_str::<AnyResponse>(content) {
if let Some(handlers) = response_handlers.lock().as_mut() {
if let Some(handler) = handlers.remove(&response.id) {
handler(Ok(content.to_string()));
}
}
} else if let Ok(notification) = serde_json::from_str::<AnyNotification>(content) {
let mut notification_handlers = notification_handlers.lock();
if let Some(handler) =
notification_handlers.get_mut(notification.method.as_str())
{
handler(notification.params.unwrap_or(Value::Null), cx.clone());
}
}
} else if let Ok(notification) = serde_json::from_str::<AnyNotification>(&message) {
let mut notification_handlers = notification_handlers.lock();
if let Some(handler) = notification_handlers.get_mut(notification.method.as_str()) {
handler(notification.params.unwrap_or(Value::Null), cx.clone());
}
}
smol::future::yield_now().await;
}
smol::future::yield_now().await;
Ok(())
}
/// Handles the stderr output from the context server.
/// Continuously reads and logs any error messages from the server.
async fn handle_stderr(transport: Arc<dyn Transport>) -> anyhow::Result<()> {
while let Some(err) = transport.receive_err().next().await {
log::warn!("context server stderr: {}", err.trim());
}
async fn handle_stderr<Stderr>(stderr: Stderr) -> anyhow::Result<()>
where
Stderr: AsyncRead + Unpin + Send + 'static,
{
let mut stderr = BufReader::new(stderr);
let mut buffer = String::new();
Ok(())
loop {
buffer.clear();
if stderr.read_line(&mut buffer).await? == 0 {
return Ok(());
}
log::warn!("context server stderr: {}", buffer.trim());
smol::future::yield_now().await;
}
}
/// Handles the output to the context server's stdin.
/// This function continuously receives messages from the outbound channel,
/// writes them to the server's stdin, and manages the lifecycle of response handlers.
async fn handle_output(
transport: Arc<dyn Transport>,
async fn handle_output<Stdin>(
stdin: Stdin,
outbound_rx: channel::Receiver<String>,
output_done_tx: barrier::Sender,
response_handlers: Arc<Mutex<Option<HashMap<RequestId, ResponseHandler>>>>,
) -> anyhow::Result<()> {
) -> anyhow::Result<()>
where
Stdin: AsyncWrite + Unpin + Send + 'static,
{
let mut stdin = BufWriter::new(stdin);
let _clear_response_handlers = util::defer({
let response_handlers = response_handlers.clone();
move || {
@@ -268,7 +309,10 @@ impl Client {
});
while let Ok(message) = outbound_rx.recv().await {
log::trace!("outgoing message: {}", message);
transport.send(message).await?;
stdin.write_all(message.as_bytes()).await?;
stdin.write_all(b"\n").await?;
stdin.flush().await?;
}
drop(output_done_tx);
Ok(())
@@ -372,6 +416,14 @@ impl Client {
}
}
impl Drop for Client {
fn drop(&mut self) {
if let Some(mut server) = self.server.lock().take() {
let _ = server.kill();
}
}
}
impl fmt::Display for ContextServerId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)

View File

@@ -4,7 +4,6 @@ mod extension_context_server;
pub mod manager;
pub mod protocol;
mod registry;
mod transport;
pub mod types;
use command_palette_hooks::CommandPaletteFilter;

View File

@@ -1,16 +0,0 @@
mod stdio_transport;
use std::pin::Pin;
use anyhow::Result;
use async_trait::async_trait;
use futures::Stream;
pub use stdio_transport::*;
#[async_trait]
pub trait Transport: Send + Sync {
async fn send(&self, message: String) -> Result<()>;
fn receive(&self) -> Pin<Box<dyn Stream<Item = String> + Send>>;
fn receive_err(&self) -> Pin<Box<dyn Stream<Item = String> + Send>>;
}

View File

@@ -1,140 +0,0 @@
use std::pin::Pin;
use anyhow::{Context as _, Result};
use async_trait::async_trait;
use futures::io::{BufReader, BufWriter};
use futures::{
AsyncBufReadExt as _, AsyncRead, AsyncWrite, AsyncWriteExt as _, Stream, StreamExt as _,
};
use gpui::AsyncApp;
use smol::channel;
use smol::process::Child;
use util::TryFutureExt as _;
use crate::client::ModelContextServerBinary;
use crate::transport::Transport;
pub struct StdioTransport {
stdout_sender: channel::Sender<String>,
stdin_receiver: channel::Receiver<String>,
stderr_receiver: channel::Receiver<String>,
server: Child,
}
impl StdioTransport {
pub fn new(binary: ModelContextServerBinary, cx: &AsyncApp) -> Result<Self> {
let mut command = util::command::new_smol_command(&binary.executable);
command
.args(&binary.args)
.envs(binary.env.unwrap_or_default())
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.kill_on_drop(true);
let mut server = command.spawn().with_context(|| {
format!(
"failed to spawn command. (path={:?}, args={:?})",
binary.executable, &binary.args
)
})?;
let stdin = server.stdin.take().unwrap();
let stdout = server.stdout.take().unwrap();
let stderr = server.stderr.take().unwrap();
let (stdin_sender, stdin_receiver) = channel::unbounded::<String>();
let (stdout_sender, stdout_receiver) = channel::unbounded::<String>();
let (stderr_sender, stderr_receiver) = channel::unbounded::<String>();
cx.spawn(|_| Self::handle_output(stdin, stdout_receiver).log_err())
.detach();
cx.spawn(|_| async move { Self::handle_input(stdout, stdin_sender).await })
.detach();
cx.spawn(|_| async move { Self::handle_err(stderr, stderr_sender).await })
.detach();
Ok(Self {
stdout_sender,
stdin_receiver,
stderr_receiver,
server,
})
}
async fn handle_input<Stdout>(stdin: Stdout, inbound_rx: channel::Sender<String>)
where
Stdout: AsyncRead + Unpin + Send + 'static,
{
let mut stdin = BufReader::new(stdin);
let mut line = String::new();
while let Ok(n) = stdin.read_line(&mut line).await {
if n == 0 {
break;
}
if inbound_rx.send(line.clone()).await.is_err() {
break;
}
line.clear();
}
}
async fn handle_output<Stdin>(
stdin: Stdin,
outbound_rx: channel::Receiver<String>,
) -> Result<()>
where
Stdin: AsyncWrite + Unpin + Send + 'static,
{
let mut stdin = BufWriter::new(stdin);
let mut pinned_rx = Box::pin(outbound_rx);
while let Some(message) = pinned_rx.next().await {
log::trace!("outgoing message: {}", message);
stdin.write_all(message.as_bytes()).await?;
stdin.write_all(b"\n").await?;
stdin.flush().await?;
}
Ok(())
}
async fn handle_err<Stderr>(stderr: Stderr, stderr_tx: channel::Sender<String>)
where
Stderr: AsyncRead + Unpin + Send + 'static,
{
let mut stderr = BufReader::new(stderr);
let mut line = String::new();
while let Ok(n) = stderr.read_line(&mut line).await {
if n == 0 {
break;
}
if stderr_tx.send(line.clone()).await.is_err() {
break;
}
line.clear();
}
}
}
#[async_trait]
impl Transport for StdioTransport {
async fn send(&self, message: String) -> Result<()> {
Ok(self.stdout_sender.send(message).await?)
}
fn receive(&self) -> Pin<Box<dyn Stream<Item = String> + Send>> {
Box::pin(self.stdin_receiver.clone())
}
fn receive_err(&self) -> Pin<Box<dyn Stream<Item = String> + Send>> {
Box::pin(self.stderr_receiver.clone())
}
}
impl Drop for StdioTransport {
fn drop(&mut self) {
let _ = self.server.kill();
}
}

View File

@@ -475,7 +475,6 @@ impl Copilot {
binary,
root_path,
None,
Default::default(),
cx.clone(),
)?;

View File

@@ -40,8 +40,6 @@ pub enum Model {
O3Mini,
#[serde(alias = "claude-3-5-sonnet", rename = "claude-3.5-sonnet")]
Claude3_5Sonnet,
#[serde(alias = "claude-3-7-sonnet", rename = "claude-3.7-sonnet")]
Claude3_7Sonnet,
#[serde(alias = "gemini-2.0-flash", rename = "gemini-2.0-flash-001")]
Gemini20Flash,
}
@@ -49,11 +47,7 @@ pub enum Model {
impl Model {
pub fn uses_streaming(&self) -> bool {
match self {
Self::Gpt4o
| Self::Gpt4
| Self::Gpt3_5Turbo
| Self::Claude3_5Sonnet
| Self::Claude3_7Sonnet => true,
Self::Gpt4o | Self::Gpt4 | Self::Gpt3_5Turbo | Self::Claude3_5Sonnet => true,
Self::O3Mini | Self::O1 | Self::Gemini20Flash => false,
}
}
@@ -66,7 +60,6 @@ impl Model {
"o1" => Ok(Self::O1),
"o3-mini" => Ok(Self::O3Mini),
"claude-3-5-sonnet" => Ok(Self::Claude3_5Sonnet),
"claude-3-7-sonnet" => Ok(Self::Claude3_7Sonnet),
"gemini-2.0-flash-001" => Ok(Self::Gemini20Flash),
_ => Err(anyhow!("Invalid model id: {}", id)),
}
@@ -80,7 +73,6 @@ impl Model {
Self::O3Mini => "o3-mini",
Self::O1 => "o1",
Self::Claude3_5Sonnet => "claude-3-5-sonnet",
Self::Claude3_7Sonnet => "claude-3-7-sonnet",
Self::Gemini20Flash => "gemini-2.0-flash-001",
}
}
@@ -93,7 +85,6 @@ impl Model {
Self::O3Mini => "o3-mini",
Self::O1 => "o1",
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
Self::Claude3_7Sonnet => "Claude 3.7 Sonnet",
Self::Gemini20Flash => "Gemini 2.0 Flash",
}
}
@@ -105,8 +96,7 @@ impl Model {
Self::Gpt3_5Turbo => 12_288,
Self::O3Mini => 64_000,
Self::O1 => 20_000,
Self::Claude3_5Sonnet => 200_000,
Self::Claude3_7Sonnet => 90_000,
Self::Claude3_5Sonnet => 128_000,
Model::Gemini20Flash => 128_000,
}
}
@@ -411,7 +401,7 @@ async fn stream_completion(
match serde_json::from_str::<ResponseEvent>(line) {
Ok(response) => {
if response.choices.is_empty()
if response.choices.first().is_none()
|| response.choices.first().unwrap().finish_reason.is_some()
{
None

View File

@@ -192,7 +192,7 @@ impl EditPredictionProvider for CopilotCompletionProvider {
fn discard(&mut self, cx: &mut Context<Self>) {
let settings = AllLanguageSettings::get_global(cx);
let copilot_enabled = settings.show_edit_predictions(None, cx);
let copilot_enabled = settings.show_inline_completions(None, cx);
if !copilot_enabled {
return;

View File

@@ -88,46 +88,15 @@ const DIAGNOSTICS_UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
impl Render for ProjectDiagnosticsEditor {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let warning_count = if self.include_warnings {
self.summary.warning_count
} else {
0
};
let child = if warning_count + self.summary.error_count == 0 {
let label = if self.summary.warning_count == 0 {
SharedString::new_static("No problems in workspace")
} else {
SharedString::new_static("No errors in workspace")
};
v_flex()
let child = if self.path_states.is_empty() {
div()
.key_context("EmptyPane")
.size_full()
.gap_1()
.justify_center()
.items_center()
.text_center()
.bg(cx.theme().colors().editor_background)
.child(Label::new(label).color(Color::Muted))
.when(self.summary.warning_count > 0, |this| {
let plural_suffix = if self.summary.warning_count > 1 {
"s"
} else {
""
};
let label = format!(
"Show {} warning{}",
self.summary.warning_count, plural_suffix
);
this.child(
Button::new("diagnostics-show-warning-label", label).on_click(cx.listener(
|this, _, window, cx| {
this.toggle_warnings(&Default::default(), window, cx);
cx.notify();
},
)),
)
})
.flex()
.items_center()
.justify_center()
.size_full()
.child(Label::new("No problems in workspace"))
} else {
div().size_full().child(self.editor.clone())
};
@@ -562,7 +531,9 @@ impl ProjectDiagnosticsEditor {
)),
height: diagnostic.message.matches('\n').count() as u32 + 1,
style: BlockStyle::Fixed,
render: diagnostic_block_renderer(diagnostic, None, true),
render: diagnostic_block_renderer(
diagnostic, None, true, true,
),
priority: 0,
});
}

View File

@@ -22,8 +22,6 @@ pub struct SelectPrevious {
pub struct MoveToBeginningOfLine {
#[serde(default = "default_true")]
pub stop_at_soft_wraps: bool,
#[serde(default)]
pub stop_at_indent: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
@@ -31,8 +29,6 @@ pub struct MoveToBeginningOfLine {
pub struct SelectToBeginningOfLine {
#[serde(default)]
pub(super) stop_at_soft_wraps: bool,
#[serde(default)]
pub stop_at_indent: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]

View File

@@ -330,11 +330,7 @@ impl DisplayMap {
block_map.remove_intersecting_replace_blocks(offset_ranges, inclusive);
}
pub fn fold_buffers(
&mut self,
buffer_ids: impl IntoIterator<Item = language::BufferId>,
cx: &mut Context<Self>,
) {
pub fn fold_buffer(&mut self, buffer_id: language::BufferId, cx: &mut Context<Self>) {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
@@ -345,14 +341,10 @@ impl DisplayMap {
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
let mut block_map = self.block_map.write(snapshot, edits);
block_map.fold_buffers(buffer_ids, self.buffer.read(cx), cx)
block_map.fold_buffer(buffer_id, self.buffer.read(cx), cx)
}
pub fn unfold_buffers(
&mut self,
buffer_ids: impl IntoIterator<Item = language::BufferId>,
cx: &mut Context<Self>,
) {
pub fn unfold_buffer(&mut self, buffer_id: language::BufferId, cx: &mut Context<Self>) {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
@@ -363,7 +355,7 @@ impl DisplayMap {
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
let mut block_map = self.block_map.write(snapshot, edits);
block_map.unfold_buffers(buffer_ids, self.buffer.read(cx), cx)
block_map.unfold_buffer(buffer_id, self.buffer.read(cx), cx)
}
pub(crate) fn is_buffer_folded(&self, buffer_id: language::BufferId) -> bool {
@@ -1919,67 +1911,6 @@ pub mod tests {
);
}
#[gpui::test]
fn test_inlays_with_newlines_after_blocks(cx: &mut gpui::TestAppContext) {
cx.update(|cx| init_test(cx, |_| {}));
let buffer = cx.new(|cx| Buffer::local("a", cx));
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
let font_size = px(14.0);
let map = cx.new(|cx| {
DisplayMap::new(
buffer.clone(),
font("Helvetica"),
font_size,
None,
true,
1,
1,
1,
FoldPlaceholder::test(),
cx,
)
});
map.update(cx, |map, cx| {
map.insert_blocks(
[BlockProperties {
placement: BlockPlacement::Above(
buffer_snapshot.anchor_before(Point::new(0, 0)),
),
height: 2,
style: BlockStyle::Sticky,
render: Arc::new(|_| div().into_any()),
priority: 0,
}],
cx,
);
});
map.update(cx, |m, cx| assert_eq!(m.snapshot(cx).text(), "\n\na"));
map.update(cx, |map, cx| {
map.splice_inlays(
&[],
vec![Inlay {
id: InlayId::InlineCompletion(0),
position: buffer_snapshot.anchor_after(0),
text: "\n".into(),
}],
cx,
);
});
map.update(cx, |m, cx| assert_eq!(m.snapshot(cx).text(), "\n\n\na"));
// Regression test: updating the display map does not crash when a
// block is immediately followed by a multi-line inlay.
buffer.update(cx, |buffer, cx| {
buffer.edit([(1..1, "b")], None, cx);
});
map.update(cx, |m, cx| assert_eq!(m.snapshot(cx).text(), "\n\n\nab"));
}
#[gpui::test]
async fn test_chunks(cx: &mut gpui::TestAppContext) {
let text = r#"

View File

@@ -638,13 +638,10 @@ impl BlockMap {
self.custom_blocks[start_block_ix..end_block_ix]
.iter()
.filter_map(|block| {
let placement = block.placement.to_wrap_row(wrap_snapshot)?;
if let BlockPlacement::Above(row) = placement {
if row < new_start {
return None;
}
}
Some((placement, Block::Custom(block.clone())))
Some((
block.placement.to_wrap_row(wrap_snapshot)?,
Block::Custom(block.clone()),
))
}),
);
@@ -999,7 +996,7 @@ impl std::ops::DerefMut for BlockPoint {
}
}
impl Deref for BlockMapReader<'_> {
impl<'a> Deref for BlockMapReader<'a> {
type Target = BlockSnapshot;
fn deref(&self) -> &Self::Target {
@@ -1007,13 +1004,13 @@ impl Deref for BlockMapReader<'_> {
}
}
impl DerefMut for BlockMapReader<'_> {
impl<'a> DerefMut for BlockMapReader<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.snapshot
}
}
impl BlockMapReader<'_> {
impl<'a> BlockMapReader<'a> {
pub fn row_for_block(&self, block_id: CustomBlockId) -> Option<BlockRow> {
let block = self.blocks.iter().find(|block| block.id == block_id)?;
let buffer_row = block
@@ -1053,7 +1050,7 @@ impl BlockMapReader<'_> {
}
}
impl BlockMapWriter<'_> {
impl<'a> BlockMapWriter<'a> {
pub fn insert(
&mut self,
blocks: impl IntoIterator<Item = BlockProperties<Anchor>>,
@@ -1239,45 +1236,26 @@ impl BlockMapWriter<'_> {
self.remove(blocks_to_remove);
}
pub fn fold_buffers(
&mut self,
buffer_ids: impl IntoIterator<Item = BufferId>,
multi_buffer: &MultiBuffer,
cx: &App,
) {
self.fold_or_unfold_buffers(true, buffer_ids, multi_buffer, cx);
pub fn fold_buffer(&mut self, buffer_id: BufferId, multi_buffer: &MultiBuffer, cx: &App) {
self.0.folded_buffers.insert(buffer_id);
self.recompute_blocks_for_buffer(buffer_id, multi_buffer, cx);
}
pub fn unfold_buffers(
&mut self,
buffer_ids: impl IntoIterator<Item = BufferId>,
multi_buffer: &MultiBuffer,
cx: &App,
) {
self.fold_or_unfold_buffers(false, buffer_ids, multi_buffer, cx);
pub fn unfold_buffer(&mut self, buffer_id: BufferId, multi_buffer: &MultiBuffer, cx: &App) {
self.0.folded_buffers.remove(&buffer_id);
self.recompute_blocks_for_buffer(buffer_id, multi_buffer, cx);
}
fn fold_or_unfold_buffers(
fn recompute_blocks_for_buffer(
&mut self,
fold: bool,
buffer_ids: impl IntoIterator<Item = BufferId>,
buffer_id: BufferId,
multi_buffer: &MultiBuffer,
cx: &App,
) {
let mut ranges = Vec::new();
for buffer_id in buffer_ids {
if fold {
self.0.folded_buffers.insert(buffer_id);
} else {
self.0.folded_buffers.remove(&buffer_id);
}
ranges.extend(multi_buffer.excerpt_ranges_for_buffer(buffer_id, cx));
}
ranges.sort_unstable_by_key(|range| range.start);
let wrap_snapshot = self.0.wrap_snapshot.borrow().clone();
let mut edits = Patch::default();
let wrap_snapshot = self.0.wrap_snapshot.borrow().clone();
for range in ranges {
for range in multi_buffer.excerpt_ranges_for_buffer(buffer_id, cx) {
let last_edit_row = cmp::min(
wrap_snapshot.make_wrap_point(range.end, Bias::Right).row() + 1,
wrap_snapshot.max_point().row(),
@@ -1740,7 +1718,7 @@ impl BlockSnapshot {
}
}
impl BlockChunks<'_> {
impl<'a> BlockChunks<'a> {
/// Go to the next transform
fn advance(&mut self) {
self.input_chunk = Chunk::default();
@@ -1856,7 +1834,7 @@ impl<'a> Iterator for BlockChunks<'a> {
}
}
impl Iterator for BlockRows<'_> {
impl<'a> Iterator for BlockRows<'a> {
type Item = RowInfo;
fn next(&mut self) -> Option<Self::Item> {
@@ -1952,7 +1930,7 @@ impl<'a> sum_tree::Dimension<'a, TransformSummary> for BlockRow {
}
}
impl Deref for BlockContext<'_, '_> {
impl<'a> Deref for BlockContext<'a, '_> {
type Target = App;
fn deref(&self) -> &Self::Target {
@@ -2749,7 +2727,7 @@ mod tests {
let mut writer = block_map.write(wrap_snapshot.clone(), Patch::default());
buffer.read_with(cx, |buffer, cx| {
writer.fold_buffers([buffer_id_1], buffer, cx);
writer.fold_buffer(buffer_id_1, buffer, cx);
});
let excerpt_blocks_1 = writer.insert(vec![BlockProperties {
style: BlockStyle::Fixed,
@@ -2824,7 +2802,7 @@ mod tests {
let mut writer = block_map.write(wrap_snapshot.clone(), Patch::default());
buffer.read_with(cx, |buffer, cx| {
writer.fold_buffers([buffer_id_2], buffer, cx);
writer.fold_buffer(buffer_id_2, buffer, cx);
});
let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
let blocks = blocks_snapshot
@@ -2880,7 +2858,7 @@ mod tests {
let mut writer = block_map.write(wrap_snapshot.clone(), Patch::default());
buffer.read_with(cx, |buffer, cx| {
writer.unfold_buffers([buffer_id_1], buffer, cx);
writer.unfold_buffer(buffer_id_1, buffer, cx);
});
let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
let blocks = blocks_snapshot
@@ -2941,7 +2919,7 @@ mod tests {
let mut writer = block_map.write(wrap_snapshot.clone(), Patch::default());
buffer.read_with(cx, |buffer, cx| {
writer.fold_buffers([buffer_id_3], buffer, cx);
writer.fold_buffer(buffer_id_3, buffer, cx);
});
let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
let blocks = blocks_snapshot
@@ -3019,7 +2997,7 @@ mod tests {
let mut writer = block_map.write(wrap_snapshot.clone(), Patch::default());
buffer.read_with(cx, |buffer, cx| {
writer.fold_buffers([buffer_id], buffer, cx);
writer.fold_buffer(buffer_id, buffer, cx);
});
let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
let blocks = blocks_snapshot
@@ -3269,7 +3247,7 @@ mod tests {
);
folded_count += 1;
unfolded_count -= 1;
block_map.fold_buffers([buffer_to_fold], buffer, cx);
block_map.fold_buffer(buffer_to_fold, buffer, cx);
}
if unfold {
let buffer_to_unfold =
@@ -3277,7 +3255,7 @@ mod tests {
log::info!("Unfolding {buffer_to_unfold:?}");
unfolded_count += 1;
folded_count -= 1;
block_map.unfold_buffers([buffer_to_unfold], buffer, cx);
block_map.unfold_buffer(buffer_to_unfold, buffer, cx);
}
log::info!(
"Unfolded buffers: {unfolded_count}, folded buffers: {folded_count}"

View File

@@ -132,7 +132,7 @@ impl<'a> sum_tree::Dimension<'a, TransformSummary> for FoldPoint {
pub(crate) struct FoldMapWriter<'a>(&'a mut FoldMap);
impl FoldMapWriter<'_> {
impl<'a> FoldMapWriter<'a> {
pub(crate) fn fold<T: ToOffset>(
&mut self,
ranges: impl IntoIterator<Item = (Range<T>, FoldPlaceholder)>,
@@ -1121,7 +1121,7 @@ impl<'a> sum_tree::Dimension<'a, FoldSummary> for FoldRange {
}
}
impl sum_tree::SeekTarget<'_, FoldSummary, FoldRange> for FoldRange {
impl<'a> sum_tree::SeekTarget<'a, FoldSummary, FoldRange> for FoldRange {
fn cmp(&self, other: &Self, buffer: &MultiBufferSnapshot) -> Ordering {
AnchorRangeExt::cmp(&self.0, &other.0, buffer)
}
@@ -1144,7 +1144,7 @@ pub struct FoldRows<'a> {
fold_point: FoldPoint,
}
impl FoldRows<'_> {
impl<'a> FoldRows<'a> {
pub(crate) fn seek(&mut self, row: u32) {
let fold_point = FoldPoint::new(row, 0);
self.cursor.seek(&fold_point, Bias::Left, &());
@@ -1155,7 +1155,7 @@ impl FoldRows<'_> {
}
}
impl Iterator for FoldRows<'_> {
impl<'a> Iterator for FoldRows<'a> {
type Item = RowInfo;
fn next(&mut self) -> Option<Self::Item> {
@@ -1190,7 +1190,7 @@ pub struct FoldChunks<'a> {
max_output_offset: FoldOffset,
}
impl FoldChunks<'_> {
impl<'a> FoldChunks<'a> {
pub(crate) fn seek(&mut self, range: Range<FoldOffset>) {
self.transform_cursor.seek(&range.start, Bias::Right, &());

View File

@@ -215,7 +215,7 @@ pub struct InlayChunks<'a> {
snapshot: &'a InlaySnapshot,
}
impl InlayChunks<'_> {
impl<'a> InlayChunks<'a> {
pub fn seek(&mut self, new_range: Range<InlayOffset>) {
self.transforms.seek(&new_range.start, Bias::Right, &());
@@ -341,7 +341,7 @@ impl<'a> Iterator for InlayChunks<'a> {
}
}
impl InlayBufferRows<'_> {
impl<'a> InlayBufferRows<'a> {
pub fn seek(&mut self, row: u32) {
let inlay_point = InlayPoint::new(row, 0);
self.transforms.seek(&inlay_point, Bias::Left, &());
@@ -363,7 +363,7 @@ impl InlayBufferRows<'_> {
}
}
impl Iterator for InlayBufferRows<'_> {
impl<'a> Iterator for InlayBufferRows<'a> {
type Item = RowInfo;
fn next(&mut self) -> Option<Self::Item> {

View File

@@ -498,7 +498,7 @@ pub struct TabChunks<'a> {
inside_leading_tab: bool,
}
impl TabChunks<'_> {
impl<'a> TabChunks<'a> {
pub(crate) fn seek(&mut self, range: Range<TabPoint>) {
let (input_start, expanded_char_column, to_next_stop) =
self.snapshot.to_fold_point(range.start, Bias::Left);

View File

@@ -69,7 +69,7 @@ pub struct WrapRows<'a> {
transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>,
}
impl WrapRows<'_> {
impl<'a> WrapRows<'a> {
pub(crate) fn seek(&mut self, start_row: u32) {
self.transforms
.seek(&WrapPoint::new(start_row, 0), Bias::Left, &());
@@ -872,7 +872,7 @@ impl WrapSnapshot {
}
}
impl WrapChunks<'_> {
impl<'a> WrapChunks<'a> {
pub(crate) fn seek(&mut self, rows: Range<u32>) {
let output_start = WrapPoint::new(rows.start, 0);
let output_end = WrapPoint::new(rows.end, 0);
@@ -955,7 +955,7 @@ impl<'a> Iterator for WrapChunks<'a> {
}
}
impl Iterator for WrapRows<'_> {
impl<'a> Iterator for WrapRows<'a> {
type Item = RowInfo;
fn next(&mut self) -> Option<Self::Item> {
@@ -1120,7 +1120,7 @@ impl<'a> sum_tree::Dimension<'a, TransformSummary> for TabPoint {
}
}
impl sum_tree::SeekTarget<'_, TransformSummary, TransformSummary> for TabPoint {
impl<'a> sum_tree::SeekTarget<'a, TransformSummary, TransformSummary> for TabPoint {
fn cmp(&self, cursor_location: &TransformSummary, _: &()) -> std::cmp::Ordering {
Ord::cmp(&self.0, &cursor_location.input.lines)
}

File diff suppressed because it is too large Load Diff

View File

@@ -177,7 +177,7 @@ impl<'de> Deserialize<'de> for ScrollbarDiagnostics {
{
struct Visitor;
impl serde::de::Visitor<'_> for Visitor {
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = ScrollbarDiagnostics;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {

View File

@@ -7,7 +7,7 @@ use crate::{
},
JoinLines,
};
use buffer_diff::{BufferDiff, DiffHunkStatus, DiffHunkStatusKind};
use buffer_diff::{BufferDiff, DiffHunkStatus};
use futures::StreamExt;
use gpui::{
div, BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext,
@@ -24,7 +24,7 @@ use language::{
Override, Point,
};
use language_settings::{Formatter, FormatterList, IndentGuideSettings};
use multi_buffer::{IndentGuide, PathKey};
use multi_buffer::IndentGuide;
use parking_lot::Mutex;
use pretty_assertions::{assert_eq, assert_ne};
use project::project_settings::{LspSettings, ProjectSettings};
@@ -36,7 +36,6 @@ use std::{
sync::atomic::{self, AtomicUsize},
};
use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
use text::ToPoint as _;
use unindent::Unindent;
use util::{
assert_set_eq, path,
@@ -1511,7 +1510,6 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let move_to_beg = MoveToBeginningOfLine {
stop_at_soft_wraps: true,
stop_at_indent: true,
};
let move_to_end = MoveToEndOfLine {
@@ -1592,7 +1590,6 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) {
editor.select_to_beginning_of_line(
&SelectToBeginningOfLine {
stop_at_soft_wraps: true,
stop_at_indent: true,
},
window,
cx,
@@ -1610,7 +1607,6 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) {
editor.select_to_beginning_of_line(
&SelectToBeginningOfLine {
stop_at_soft_wraps: true,
stop_at_indent: true,
},
window,
cx,
@@ -1628,7 +1624,6 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) {
editor.select_to_beginning_of_line(
&SelectToBeginningOfLine {
stop_at_soft_wraps: true,
stop_at_indent: true,
},
window,
cx,
@@ -1689,7 +1684,6 @@ fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let move_to_beg = MoveToBeginningOfLine {
stop_at_soft_wraps: false,
stop_at_indent: false,
};
let move_to_end = MoveToEndOfLine {
@@ -3395,7 +3389,7 @@ async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &m
.unindent(),
);
cx.set_head_text(&diff_base);
cx.set_diff_base(&diff_base);
executor.run_until_parked();
// Join lines
@@ -3435,7 +3429,7 @@ async fn test_custom_newlines_cause_no_false_positive_diffs(
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
cx.set_diff_base("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
executor.run_until_parked();
cx.update_editor(|editor, window, cx| {
@@ -5817,7 +5811,7 @@ async fn test_fold_function_bodies(cx: &mut TestAppContext) {
let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
cx.set_state(&text);
cx.set_head_text(&base_text);
cx.set_diff_base(&base_text);
cx.update_editor(|editor, window, cx| {
editor.expand_all_diff_hunks(&Default::default(), window, cx);
});
@@ -10970,106 +10964,6 @@ async fn cycle_through_same_place_diagnostics(
"});
}
#[gpui::test]
async fn active_diagnostics_dismiss_after_invalidation(
executor: BackgroundExecutor,
cx: &mut TestAppContext,
) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let lsp_store =
cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
cx.set_state(indoc! {"
ˇfn func(abc def: i32) -> u32 {
}
"});
let message = "Something's wrong!";
cx.update(|_, cx| {
lsp_store.update(cx, |lsp_store, cx| {
lsp_store
.update_diagnostics(
LanguageServerId(0),
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
version: None,
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range::new(
lsp::Position::new(0, 11),
lsp::Position::new(0, 12),
),
severity: Some(lsp::DiagnosticSeverity::ERROR),
message: message.to_string(),
..Default::default()
}],
},
&[],
cx,
)
.unwrap()
});
});
executor.run_until_parked();
cx.update_editor(|editor, window, cx| {
editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
assert_eq!(
editor
.active_diagnostics
.as_ref()
.map(|diagnostics_group| diagnostics_group.primary_message.as_str()),
Some(message),
"Should have a diagnostics group activated"
);
});
cx.assert_editor_state(indoc! {"
fn func(abcˇ def: i32) -> u32 {
}
"});
cx.update(|_, cx| {
lsp_store.update(cx, |lsp_store, cx| {
lsp_store
.update_diagnostics(
LanguageServerId(0),
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
version: None,
diagnostics: Vec::new(),
},
&[],
cx,
)
.unwrap()
});
});
executor.run_until_parked();
cx.update_editor(|editor, _, _| {
assert_eq!(
editor.active_diagnostics, None,
"After no diagnostics set to the editor, no diagnostics should be active"
);
});
cx.assert_editor_state(indoc! {"
fn func(abcˇ def: i32) -> u32 {
}
"});
cx.update_editor(|editor, window, cx| {
editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
assert_eq!(
editor.active_diagnostics, None,
"Should be no diagnostics to go to and activate"
);
});
cx.assert_editor_state(indoc! {"
fn func(abcˇ def: i32) -> u32 {
}
"});
}
#[gpui::test]
async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -11145,7 +11039,7 @@ async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext)
.unindent(),
);
cx.set_head_text(&diff_base);
cx.set_diff_base(&diff_base);
executor.run_until_parked();
cx.update_editor(|editor, window, cx| {
@@ -12637,7 +12531,7 @@ async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
three
"#};
cx.set_head_text(base_text);
cx.set_diff_base(base_text);
cx.set_state("\nˇ\n");
cx.executor().run_until_parked();
cx.update_editor(|editor, _window, cx| {
@@ -13274,7 +13168,7 @@ async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut
.unindent(),
);
cx.set_head_text(&diff_base);
cx.set_diff_base(&diff_base);
executor.run_until_parked();
cx.update_editor(|editor, window, cx| {
@@ -13408,7 +13302,7 @@ async fn test_diff_base_change_with_expanded_diff_hunks(
.unindent(),
);
cx.set_head_text(&diff_base);
cx.set_diff_base(&diff_base);
executor.run_until_parked();
cx.update_editor(|editor, window, cx| {
@@ -13436,7 +13330,7 @@ async fn test_diff_base_change_with_expanded_diff_hunks(
.unindent(),
);
cx.set_head_text("new diff base!");
cx.set_diff_base("new diff base!");
executor.run_until_parked();
cx.assert_state_with_diff(
r#"
@@ -13736,7 +13630,7 @@ async fn test_edits_around_expanded_insertion_hunks(
.unindent(),
);
cx.set_head_text(&diff_base);
cx.set_diff_base(&diff_base);
executor.run_until_parked();
cx.update_editor(|editor, window, cx| {
@@ -13884,7 +13778,7 @@ async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
cx.set_head_text(indoc! { "
cx.set_diff_base(indoc! { "
one
two
three
@@ -14007,7 +13901,7 @@ async fn test_edits_around_expanded_deletion_hunks(
.unindent(),
);
cx.set_head_text(&diff_base);
cx.set_diff_base(&diff_base);
executor.run_until_parked();
cx.update_editor(|editor, window, cx| {
@@ -14130,7 +14024,7 @@ async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &m
.unindent(),
);
cx.set_head_text(&base_text);
cx.set_diff_base(&base_text);
executor.run_until_parked();
cx.update_editor(|editor, window, cx| {
@@ -14212,7 +14106,7 @@ async fn test_edit_after_expanded_modification_hunk(
.unindent(),
);
cx.set_head_text(&diff_base);
cx.set_diff_base(&diff_base);
executor.run_until_parked();
cx.update_editor(|editor, window, cx| {
editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
@@ -14947,7 +14841,7 @@ async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestApp
"#
.unindent(),
);
cx.set_head_text(&diff_base);
cx.set_diff_base(&diff_base);
cx.update_editor(|editor, window, cx| {
editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
});
@@ -15084,240 +14978,6 @@ async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestApp
);
}
#[gpui::test]
async fn test_toggle_deletion_hunk_at_start_of_file(
executor: BackgroundExecutor,
cx: &mut TestAppContext,
) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let diff_base = r#"
a
b
c
"#
.unindent();
cx.set_state(
&r#"
ˇb
c
"#
.unindent(),
);
cx.set_head_text(&diff_base);
cx.update_editor(|editor, window, cx| {
editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
});
executor.run_until_parked();
let hunk_expanded = r#"
- a
ˇb
c
"#
.unindent();
cx.assert_state_with_diff(hunk_expanded.clone());
let hunk_ranges = cx.update_editor(|editor, window, cx| {
let snapshot = editor.snapshot(window, cx);
let hunks = editor
.diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
.collect::<Vec<_>>();
let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
let buffer_id = hunks[0].buffer_id;
hunks
.into_iter()
.map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
.collect::<Vec<_>>()
});
assert_eq!(hunk_ranges.len(), 1);
cx.update_editor(|editor, _, cx| {
editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
});
executor.run_until_parked();
let hunk_collapsed = r#"
ˇb
c
"#
.unindent();
cx.assert_state_with_diff(hunk_collapsed);
cx.update_editor(|editor, _, cx| {
editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
});
executor.run_until_parked();
cx.assert_state_with_diff(hunk_expanded.clone());
}
#[gpui::test]
async fn test_display_diff_hunks(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/test"),
json!({
".git": {},
"file-1": "ONE\n",
"file-2": "TWO\n",
"file-3": "THREE\n",
}),
)
.await;
fs.set_head_for_repo(
path!("/test/.git").as_ref(),
&[
("file-1".into(), "one\n".into()),
("file-2".into(), "two\n".into()),
("file-3".into(), "three\n".into()),
],
);
let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
let mut buffers = vec![];
for i in 1..=3 {
let buffer = project
.update(cx, |project, cx| {
let path = format!(path!("/test/file-{}"), i);
project.open_local_buffer(path, cx)
})
.await
.unwrap();
buffers.push(buffer);
}
let multibuffer = cx.new(|cx| {
let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
multibuffer.set_all_diff_hunks_expanded(cx);
for buffer in &buffers {
let snapshot = buffer.read(cx).snapshot();
multibuffer.set_excerpts_for_path(
PathKey::namespaced("", buffer.read(cx).file().unwrap().path().clone()),
buffer.clone(),
vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
DEFAULT_MULTIBUFFER_CONTEXT,
cx,
);
}
multibuffer
});
let editor = cx.add_window(|window, cx| {
Editor::new(
EditorMode::Full,
multibuffer,
Some(project),
true,
window,
cx,
)
});
cx.run_until_parked();
let snapshot = editor
.update(cx, |editor, window, cx| editor.snapshot(window, cx))
.unwrap();
let hunks = snapshot
.display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
.map(|hunk| match hunk {
DisplayDiffHunk::Unfolded {
display_row_range, ..
} => display_row_range,
DisplayDiffHunk::Folded { .. } => unreachable!(),
})
.collect::<Vec<_>>();
assert_eq!(
hunks,
[
DisplayRow(3)..DisplayRow(5),
DisplayRow(10)..DisplayRow(12),
DisplayRow(17)..DisplayRow(19),
]
);
}
#[gpui::test]
async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
cx.set_head_text(indoc! { "
one
two
three
four
five
"
});
cx.set_index_text(indoc! { "
one
two
three
four
five
"
});
cx.set_state(indoc! {"
one
TWO
ˇTHREE
FOUR
five
"});
cx.run_until_parked();
cx.update_editor(|editor, window, cx| {
editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
});
cx.run_until_parked();
cx.assert_index_text(Some(indoc! {"
one
TWO
THREE
FOUR
five
"}));
cx.set_state(indoc! { "
one
TWO
ˇTHREE-HUNDRED
FOUR
five
"});
cx.run_until_parked();
cx.update_editor(|editor, window, cx| {
let snapshot = editor.snapshot(window, cx);
let hunks = editor
.diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
.collect::<Vec<_>>();
assert_eq!(hunks.len(), 1);
assert_eq!(
hunks[0].status(),
DiffHunkStatus {
kind: DiffHunkStatusKind::Modified,
secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
}
);
editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
});
cx.run_until_parked();
cx.assert_index_text(Some(indoc! {"
one
TWO
THREE-HUNDRED
FOUR
five
"}));
}
#[gpui::test]
fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -15677,12 +15337,11 @@ async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
});
editor.update_in(cx, |editor, window, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx);
editor.tasks.insert(
(buffer.read(cx).remote_id(), 3),
RunnableTasks {
templates: vec![],
offset: snapshot.anchor_before(43),
offset: MultiBufferOffset(43),
column: 0,
extra_variables: HashMap::default(),
context_range: BufferOffset(43)..BufferOffset(85),
@@ -15692,7 +15351,7 @@ async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
(buffer.read(cx).remote_id(), 8),
RunnableTasks {
templates: vec![],
offset: snapshot.anchor_before(86),
offset: MultiBufferOffset(86),
column: 0,
extra_variables: HashMap::default(),
context_range: BufferOffset(86)..BufferOffset(191),
@@ -15716,7 +15375,7 @@ async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
}
#[gpui::test]
async fn test_folding_buffers(cx: &mut TestAppContext) {
async fn test_multi_buffer_folding(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
@@ -15823,7 +15482,7 @@ async fn test_folding_buffers(cx: &mut TestAppContext) {
let multi_buffer_editor = cx.new_window_entity(|window, cx| {
Editor::new(
EditorMode::Full,
multi_buffer.clone(),
multi_buffer,
Some(project.clone()),
true,
window,
@@ -15831,9 +15490,10 @@ async fn test_folding_buffers(cx: &mut TestAppContext) {
)
});
let full_text = "\n\n\naaaa\nbbbb\ncccc\n\n\n\nffff\ngggg\n\n\n\njjjj\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n";
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\naaaa\nbbbb\ncccc\n\n\n\nffff\ngggg\n\n\n\njjjj\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n",
full_text,
);
multi_buffer_editor.update(cx, |editor, cx| {
@@ -15879,25 +15539,12 @@ async fn test_folding_buffers(cx: &mut TestAppContext) {
"After unfolding the second buffer, its text should be displayed"
);
// Typing inside of buffer 1 causes that buffer to be unfolded.
multi_buffer_editor.update_in(cx, |editor, window, cx| {
assert_eq!(
multi_buffer
.read(cx)
.snapshot(cx)
.text_for_range(Point::new(1, 0)..Point::new(1, 4))
.collect::<String>(),
"bbbb"
);
editor.change_selections(None, window, cx, |selections| {
selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
});
editor.handle_input("B", window, cx);
multi_buffer_editor.update(cx, |editor, cx| {
editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\nB\n\n\n\n\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n",
"\n\n\naaaa\nbbbb\ncccc\n\n\n\nffff\ngggg\n\n\n\njjjj\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n",
"After unfolding the first buffer, its and 2nd buffer's text should be displayed"
);
@@ -15906,13 +15553,13 @@ async fn test_folding_buffers(cx: &mut TestAppContext) {
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\nB\n\n\n\n\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n",
full_text,
"After unfolding the all buffers, all original text should be displayed"
);
}
#[gpui::test]
async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
async fn test_multi_buffer_single_excerpts_folding(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let sample_text_1 = "1111\n2222\n3333".to_string();
@@ -16061,7 +15708,7 @@ async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
}
#[gpui::test]
async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
async fn test_multi_buffer_with_single_excerpt_folding(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
@@ -16693,8 +16340,7 @@ fn assert_hunk_revert(
cx: &mut EditorLspTestContext,
) {
cx.set_state(not_reverted_text_with_selections);
cx.set_head_text(base_text);
cx.clear_index_text();
cx.set_diff_base(base_text);
cx.executor().run_until_parked();
let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {

View File

@@ -12,25 +12,23 @@ use crate::{
hover_popover::{
self, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
},
inlay_hint_settings,
items::BufferSearchHighlights,
mouse_context_menu::{self, MenuPosition, MouseContextMenu},
scroll::{axis_pair, scroll_amount::ScrollAmount, AxisPair},
BlockId, ChunkReplacement, CursorShape, CustomBlockId, DisplayDiffHunk, DisplayPoint,
DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode, Editor, EditorMode,
BlockId, ChunkReplacement, CursorShape, CustomBlockId, DisplayPoint, DisplayRow,
DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode, Editor, EditorMode,
EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GoToHunk,
GoToPrevHunk, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor,
InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineUp, OpenExcerpts, PageDown,
PageUp, Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection, SoftWrap,
StickyHeaderExcerpt, ToPoint, ToggleFold, COLUMNAR_SELECTION_MODIFIERS, CURSORS_VISIBLE_FOR,
FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN,
MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
InlineCompletion, JumpData, LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point, RowExt,
RowRangeExt, SelectPhase, SelectedTextHighlight, Selection, SoftWrap, StickyHeaderExcerpt,
ToPoint, ToggleFold, COLUMNAR_SELECTION_MODIFIERS, CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT,
GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
};
use buffer_diff::{DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
use client::ParticipantIndex;
use collections::{BTreeMap, HashMap, HashSet};
use file_icons::FileIcons;
use git::{blame::BlameEntry, status::FileStatus, Oid};
use git::{blame::BlameEntry, Oid};
use gpui::{
anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, point, px, quad,
relative, size, svg, transparent_black, Action, AnyElement, App, AvailableSpace, Axis, Bounds,
@@ -52,7 +50,7 @@ use language::{
use lsp::DiagnosticSeverity;
use multi_buffer::{
Anchor, ExcerptId, ExcerptInfo, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow,
RowInfo,
RowInfo, ToOffset,
};
use project::project_settings::{self, GitGutterSetting, ProjectSettings};
use settings::Settings;
@@ -75,12 +73,25 @@ use ui::{
POPOVER_Y_PADDING,
};
use unicode_segmentation::UnicodeSegmentation;
use util::{debug_panic, maybe, RangeExt, ResultExt};
use util::{debug_panic, RangeExt, ResultExt};
use workspace::{item::Item, notifications::NotifyTaskExt};
const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 7.;
const MIN_SCROLL_THUMB_SIZE: f32 = 25.;
#[derive(Debug, Clone, PartialEq, Eq)]
enum DisplayDiffHunk {
Folded {
display_row: DisplayRow,
},
Unfolded {
diff_base_byte_range: Range<usize>,
display_row_range: Range<DisplayRow>,
multi_buffer_range: Range<Anchor>,
status: DiffHunkStatus,
},
}
struct SelectionLayout {
head: DisplayPoint,
cursor_shape: CursorShape,
@@ -395,7 +406,7 @@ impl EditorElement {
register_action(editor, window, Editor::toggle_relative_line_numbers);
register_action(editor, window, Editor::toggle_indent_guides);
register_action(editor, window, Editor::toggle_inlay_hints);
register_action(editor, window, Editor::toggle_edit_predictions);
register_action(editor, window, Editor::toggle_inline_completions);
register_action(editor, window, Editor::toggle_inline_diagnostics);
register_action(editor, window, hover_popover::hover);
register_action(editor, window, Editor::reveal_in_finder);
@@ -507,25 +518,6 @@ impl EditorElement {
return;
}
editor.update(cx, |editor, cx| {
let inlay_hint_settings = inlay_hint_settings(
editor.selections.newest_anchor().head(),
&editor.buffer.read(cx).snapshot(cx),
cx,
);
if let Some(inlay_modifiers) = inlay_hint_settings
.toggle_on_modifiers_press
.as_ref()
.filter(|modifiers| modifiers.modified())
{
editor.refresh_inlay_hints(
InlayHintRefreshReason::ModifiersChanged(
inlay_modifiers == &event.modifiers,
),
cx,
);
}
if editor.hover_state.focused(window, cx) {
return;
}
@@ -1561,11 +1553,50 @@ impl EditorElement {
window: &mut Window,
cx: &mut App,
) -> Vec<(DisplayDiffHunk, Option<Hitbox>)> {
let buffer_start = DisplayPoint::new(display_rows.start, 0).to_point(snapshot);
let buffer_end = DisplayPoint::new(display_rows.end, 0).to_point(snapshot);
let mut display_hunks = Vec::<(DisplayDiffHunk, Option<Hitbox>)>::new();
let folded_buffers = self.editor.read(cx).folded_buffers(cx);
let mut display_hunks = snapshot
.display_diff_hunks_for_rows(display_rows, folded_buffers)
.map(|hunk| (hunk, None))
.collect::<Vec<_>>();
for hunk in snapshot
.buffer_snapshot
.diff_hunks_in_range(buffer_start..buffer_end)
{
if folded_buffers.contains(&hunk.buffer_id) {
continue;
}
let hunk_start_point = Point::new(hunk.row_range.start.0, 0);
let hunk_end_point = Point::new(hunk.row_range.end.0, 0);
let hunk_display_start = snapshot.point_to_display_point(hunk_start_point, Bias::Left);
let hunk_display_end = snapshot.point_to_display_point(hunk_end_point, Bias::Right);
let display_hunk = if hunk_display_start.column() != 0 {
DisplayDiffHunk::Folded {
display_row: hunk_display_start.row(),
}
} else {
let mut end_row = hunk_display_end.row();
if hunk_display_end.column() > 0 {
end_row.0 += 1;
}
DisplayDiffHunk::Unfolded {
status: hunk.status(),
diff_base_byte_range: hunk.diff_base_byte_range,
display_row_range: hunk_display_start.row()..end_row,
multi_buffer_range: Anchor::range_in_buffer(
hunk.excerpt_id,
hunk.buffer_id,
hunk.buffer_range,
),
}
};
display_hunks.push((display_hunk, None));
}
let git_gutter_setting = ProjectSettings::get_global(cx)
.git
.git_gutter
@@ -2031,22 +2062,21 @@ impl EditorElement {
None
};
let offset_range_start =
snapshot.display_point_to_point(DisplayPoint::new(range.start, 0), Bias::Left);
let offset_range_end =
snapshot.display_point_to_point(DisplayPoint::new(range.end, 0), Bias::Right);
let offset_range_start = snapshot
.display_point_to_anchor(DisplayPoint::new(range.start, 0), Bias::Left)
.to_offset(&snapshot.buffer_snapshot);
let offset_range_end = snapshot
.display_point_to_anchor(DisplayPoint::new(range.end, 0), Bias::Right)
.to_offset(&snapshot.buffer_snapshot);
editor
.tasks
.iter()
.filter_map(|(_, tasks)| {
let multibuffer_point = tasks.offset.to_point(&snapshot.buffer_snapshot);
if multibuffer_point < offset_range_start
|| multibuffer_point > offset_range_end
{
if tasks.offset.0 < offset_range_start || tasks.offset.0 >= offset_range_end {
return None;
}
let multibuffer_point = tasks.offset.0.to_point(&snapshot.buffer_snapshot);
let multibuffer_row = MultiBufferRow(multibuffer_point.row);
let buffer_folded = snapshot
.buffer_snapshot
@@ -2667,21 +2697,6 @@ impl EditorElement {
window: &mut Window,
cx: &mut App,
) -> Div {
let file_status = maybe!({
let project = self.editor.read(cx).project.as_ref()?.read(cx);
let (repo, path) =
project.repository_and_path_for_buffer_id(for_excerpt.buffer_id, cx)?;
let status = repo.read(cx).repository_entry.status_for_path(&path)?;
Some(status.status)
})
.filter(|_| {
self.editor
.read(cx)
.buffer
.read(cx)
.all_diff_hunks_expanded()
});
let include_root = self
.editor
.read(cx)
@@ -2785,36 +2800,12 @@ impl EditorElement {
h_flex()
.gap_2()
.child(
Label::new(
filename
.map(SharedString::from)
.unwrap_or_else(|| "untitled".into()),
)
.single_line()
.when_some(
file_status,
|el, status| {
el.color(if status.is_conflicted() {
Color::Conflict
} else if status.is_modified() {
Color::Modified
} else if status.is_deleted() {
Color::Disabled
} else {
Color::Created
})
.when(status.is_deleted(), |el| el.strikethrough())
},
),
filename
.map(SharedString::from)
.unwrap_or_else(|| "untitled".into()),
)
.when_some(parent_path, |then, path| {
then.child(div().child(path).text_color(
if file_status.is_some_and(FileStatus::is_deleted) {
colors.text_disabled
} else {
colors.text_muted
},
))
then.child(div().child(path).text_color(colors.text_muted))
}),
)
.when(is_selected, |el| {
@@ -8780,12 +8771,11 @@ fn diff_hunk_controls(
})
.on_click({
let editor = editor.clone();
move |_event, window, cx| {
move |_event, _, cx| {
editor.update(cx, |editor, cx| {
editor.stage_or_unstage_diff_hunks(
false,
&[hunk_range.start..hunk_range.start],
window,
cx,
);
});
@@ -8810,12 +8800,11 @@ fn diff_hunk_controls(
})
.on_click({
let editor = editor.clone();
move |_event, window, cx| {
move |_event, _, cx| {
editor.update(cx, |editor, cx| {
editor.stage_or_unstage_diff_hunks(
true,
&[hunk_range.start..hunk_range.start],
window,
cx,
);
});

View File

@@ -1271,7 +1271,6 @@ mod tests {
show_parameter_hints: true,
show_other_hints: true,
show_background: false,
toggle_on_modifiers_press: None,
})
});

View File

@@ -15,7 +15,7 @@ use itertools::Itertools;
use language::{DiagnosticEntry, Language, LanguageRegistry};
use lsp::DiagnosticSeverity;
use markdown::{Markdown, MarkdownStyle};
use multi_buffer::{MultiOrSingleBufferOffsetRange, ToOffset};
use multi_buffer::ToOffset;
use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart};
use settings::Settings;
use std::{borrow::Cow, cell::RefCell};
@@ -447,13 +447,11 @@ fn show_hover(
})
.or_else(|| {
let snapshot = &snapshot.buffer_snapshot;
match snapshot.syntax_ancestor(anchor..anchor)?.1 {
MultiOrSingleBufferOffsetRange::Multi(range) => Some(
snapshot.anchor_before(range.start)
..snapshot.anchor_after(range.end),
),
MultiOrSingleBufferOffsetRange::Single(_) => None,
}
let offset_range = snapshot.syntax_ancestor(anchor..anchor)?.1;
Some(
snapshot.anchor_before(offset_range.start)
..snapshot.anchor_after(offset_range.end),
)
})
.unwrap_or_else(|| anchor..anchor);
@@ -1536,7 +1534,6 @@ mod tests {
show_parameter_hints: true,
show_other_hints: true,
show_background: false,
toggle_on_modifiers_press: None,
})
});

View File

@@ -36,7 +36,6 @@ pub struct InlayHintCache {
allowed_hint_kinds: HashSet<Option<InlayHintKind>>,
version: usize,
pub(super) enabled: bool,
modifiers_override: bool,
enabled_in_settings: bool,
update_tasks: HashMap<ExcerptId, TasksForRanges>,
refresh_task: Task<()>,
@@ -266,7 +265,6 @@ impl InlayHintCache {
Self {
allowed_hint_kinds: inlay_hint_settings.enabled_inlay_hint_kinds(),
enabled: inlay_hint_settings.enabled,
modifiers_override: false,
enabled_in_settings: inlay_hint_settings.enabled,
hints: HashMap::default(),
update_tasks: HashMap::default(),
@@ -297,9 +295,8 @@ impl InlayHintCache {
// visibility would not change when updating the setting if they were ever toggled.
if new_hint_settings.enabled != self.enabled_in_settings {
self.enabled = new_hint_settings.enabled;
self.enabled_in_settings = new_hint_settings.enabled;
self.modifiers_override = false;
};
self.enabled_in_settings = new_hint_settings.enabled;
self.invalidate_debounce = debounce_value(new_hint_settings.edit_debounce_ms);
self.append_debounce = debounce_value(new_hint_settings.scroll_debounce_ms);
let new_allowed_hint_kinds = new_hint_settings.enabled_inlay_hint_kinds();
@@ -326,7 +323,6 @@ impl InlayHintCache {
}
}
(true, false) => {
self.modifiers_override = false;
self.allowed_hint_kinds = new_allowed_hint_kinds;
if self.hints.is_empty() {
ControlFlow::Break(None)
@@ -339,39 +335,12 @@ impl InlayHintCache {
}
}
(false, true) => {
self.modifiers_override = false;
self.allowed_hint_kinds = new_allowed_hint_kinds;
ControlFlow::Continue(())
}
}
}
pub(super) fn modifiers_override(&mut self, new_override: bool) -> Option<bool> {
if self.modifiers_override == new_override {
return None;
}
self.modifiers_override = new_override;
if (self.enabled && self.modifiers_override) || (!self.enabled && !self.modifiers_override)
{
self.clear();
Some(false)
} else {
Some(true)
}
}
pub(super) fn toggle(&mut self, enabled: bool) -> bool {
if self.enabled == enabled {
return false;
}
self.enabled = enabled;
self.modifiers_override = false;
if !enabled {
self.clear();
}
true
}
/// If needed, queries LSP for new inlay hints, using the invalidation strategy given.
/// To reduce inlay hint jumping, attempts to query a visible range of the editor(s) first,
/// followed by the delayed queries of the same range above and below the visible one.
@@ -384,8 +353,7 @@ impl InlayHintCache {
ignore_debounce: bool,
cx: &mut Context<Editor>,
) -> Option<InlaySplice> {
if (self.enabled && self.modifiers_override) || (!self.enabled && !self.modifiers_override)
{
if !self.enabled {
return None;
}
let mut invalidated_hints = Vec::new();
@@ -1320,7 +1288,6 @@ pub mod tests {
show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
show_other_hints: allowed_hint_kinds.contains(&None),
show_background: false,
toggle_on_modifiers_press: None,
})
});
let (_, editor, fake_server) = prepare_test_objects(cx, |fake_server, file_with_hints| {
@@ -1424,7 +1391,6 @@ pub mod tests {
show_parameter_hints: true,
show_other_hints: true,
show_background: false,
toggle_on_modifiers_press: None,
})
});
@@ -1527,7 +1493,6 @@ pub mod tests {
show_parameter_hints: true,
show_other_hints: true,
show_background: false,
toggle_on_modifiers_press: None,
})
});
@@ -1747,7 +1712,6 @@ pub mod tests {
show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
show_other_hints: allowed_hint_kinds.contains(&None),
show_background: false,
toggle_on_modifiers_press: None,
})
});
@@ -1907,7 +1871,6 @@ pub mod tests {
.contains(&Some(InlayHintKind::Parameter)),
show_other_hints: new_allowed_hint_kinds.contains(&None),
show_background: false,
toggle_on_modifiers_press: None,
})
});
cx.executor().run_until_parked();
@@ -1950,7 +1913,6 @@ pub mod tests {
.contains(&Some(InlayHintKind::Parameter)),
show_other_hints: another_allowed_hint_kinds.contains(&None),
show_background: false,
toggle_on_modifiers_press: None,
})
});
cx.executor().run_until_parked();
@@ -2005,7 +1967,6 @@ pub mod tests {
.contains(&Some(InlayHintKind::Parameter)),
show_other_hints: final_allowed_hint_kinds.contains(&None),
show_background: false,
toggle_on_modifiers_press: None,
})
});
cx.executor().run_until_parked();
@@ -2077,7 +2038,6 @@ pub mod tests {
show_parameter_hints: true,
show_other_hints: true,
show_background: false,
toggle_on_modifiers_press: None,
})
});
@@ -2209,7 +2169,6 @@ pub mod tests {
show_parameter_hints: true,
show_other_hints: true,
show_background: false,
toggle_on_modifiers_press: None,
})
});
@@ -2508,7 +2467,6 @@ pub mod tests {
show_parameter_hints: true,
show_other_hints: true,
show_background: false,
toggle_on_modifiers_press: None,
})
});
@@ -2853,7 +2811,6 @@ pub mod tests {
show_parameter_hints: false,
show_other_hints: false,
show_background: false,
toggle_on_modifiers_press: None,
})
});
@@ -3035,7 +2992,6 @@ pub mod tests {
show_parameter_hints: true,
show_other_hints: true,
show_background: false,
toggle_on_modifiers_press: None,
})
});
cx.executor().run_until_parked();
@@ -3067,7 +3023,6 @@ pub mod tests {
show_parameter_hints: true,
show_other_hints: true,
show_background: false,
toggle_on_modifiers_press: None,
})
});
@@ -3159,7 +3114,6 @@ pub mod tests {
show_parameter_hints: true,
show_other_hints: true,
show_background: false,
toggle_on_modifiers_press: None,
})
});
@@ -3233,7 +3187,6 @@ pub mod tests {
show_parameter_hints: true,
show_other_hints: true,
show_background: false,
toggle_on_modifiers_press: None,
})
});
cx.executor().run_until_parked();
@@ -3293,7 +3246,6 @@ pub mod tests {
show_parameter_hints: true,
show_other_hints: true,
show_background: false,
toggle_on_modifiers_press: None,
})
});

View File

@@ -1739,7 +1739,6 @@ mod tests {
let file = TestFile {
path: Path::new("").into(),
root_name: String::new(),
local_root: None,
};
assert_eq!(path_for_file(&file, 0, false, cx), None);
}

View File

@@ -214,7 +214,6 @@ pub fn indented_line_beginning(
map: &DisplaySnapshot,
display_point: DisplayPoint,
stop_at_soft_boundaries: bool,
stop_at_indent: bool,
) -> DisplayPoint {
let point = display_point.to_point(map);
let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
@@ -230,7 +229,7 @@ pub fn indented_line_beginning(
if stop_at_soft_boundaries && soft_line_start > indent_start && display_point != soft_line_start
{
soft_line_start
} else if stop_at_indent && display_point != indent_start {
} else if stop_at_soft_boundaries && display_point != indent_start {
indent_start
} else {
line_start
@@ -238,7 +237,7 @@ pub fn indented_line_beginning(
}
/// Returns a position of the end of line.
///
/// If `stop_at_soft_boundaries` is true, the returned position is that of the
/// displayed line (e.g. it could actually be in the middle of a text line if that line is soft-wrapped).
/// Otherwise it's always going to be the end of a logical line.
@@ -468,6 +467,31 @@ pub fn end_of_excerpt(
}
}
pub fn multibuffer_header(
map: &DisplaySnapshot,
display_point: DisplayPoint,
direction: Direction,
) -> DisplayPoint {
let point = map.display_point_to_point(display_point, Bias::Left);
// It seems likely that the better way to implement this is to reuse block logic via
// `map.blocks_in_range` and add support for `reversed_blocks_in_range`. I haven't evaluated in
// depth whether this will work.
//
// Before thinking of that, implementation plan was to:
//
// * For `Direction::Prev`, use `reversed_excerpts_at` to iterate over the excerpts in reverse
// order.
//
// * For `Direction::Next`, use `excerpts_at` to iterate over the excerpts.
//
// * Find boundaries by checking when `buffer_id` changes similar to the block_map logic
// [here](https://github.com/zed-industries/zed/blob/e5b61949148cd87e08ae38e80949bce9b4ede9f7/crates/editor/src/display_map/block_map.rs#L845).
//
// Another alternative might be to use `excerpt_before` and `excerpt_after` methods to walk the
// excerpts.
todo!();
}
/// Scans for a boundary preceding the given start point `from` until a boundary is found,
/// indicated by the given predicate returning true.
/// The predicate is called with the character to the left and right of the candidate boundary location.

View File

@@ -3,6 +3,7 @@ pub(crate) mod autoscroll;
pub(crate) mod scroll_amount;
use crate::editor_settings::{ScrollBeyondLastLine, ScrollbarAxes};
use crate::EditPredictionPreview;
use crate::{
display_map::{DisplaySnapshot, ToDisplayPoint},
hover_popover::hide_hover,
@@ -495,8 +496,14 @@ impl Editor {
hide_hover(self, cx);
let workspace_id = self.workspace.as_ref().and_then(|workspace| workspace.1);
self.edit_prediction_preview
.set_previous_scroll_position(None);
if let EditPredictionPreview::Active {
previous_scroll_position,
} = &mut self.edit_prediction_preview
{
if !autoscroll {
previous_scroll_position.take();
}
}
self.scroll_manager.set_scroll_position(
scroll_position,

View File

@@ -843,14 +843,14 @@ impl<'a> MutableSelectionsCollection<'a> {
}
}
impl Deref for MutableSelectionsCollection<'_> {
impl<'a> Deref for MutableSelectionsCollection<'a> {
type Target = SelectionsCollection;
fn deref(&self) -> &Self::Target {
self.collection
}
}
impl DerefMut for MutableSelectionsCollection<'_> {
impl<'a> DerefMut for MutableSelectionsCollection<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.collection
}

View File

@@ -1,73 +1,93 @@
use crate::Editor;
use gpui::{App, Task, Window};
use gpui::{App, AppContext as _, Task as AsyncTask, Window};
use project::Location;
use task::{TaskContext, TaskVariables, VariableName};
use text::{ToOffset, ToPoint};
use workspace::Workspace;
impl Editor {
pub fn task_context(&self, window: &mut Window, cx: &mut App) -> Task<Option<TaskContext>> {
let Some(project) = self.project.clone() else {
return Task::ready(None);
fn task_context_with_editor(
editor: &mut Editor,
window: &mut Window,
cx: &mut App,
) -> AsyncTask<Option<TaskContext>> {
let Some(project) = editor.project.clone() else {
return AsyncTask::ready(None);
};
let (selection, buffer, editor_snapshot) = {
let selection = editor.selections.newest_adjusted(cx);
let Some((buffer, _)) = editor
.buffer()
.read(cx)
.point_to_buffer_offset(selection.start, cx)
else {
return AsyncTask::ready(None);
};
let (selection, buffer, editor_snapshot) = {
let selection = self.selections.newest_adjusted(cx);
let Some((buffer, _)) = self
.buffer()
.read(cx)
.point_to_buffer_offset(selection.start, cx)
else {
return Task::ready(None);
};
let snapshot = self.snapshot(window, cx);
(selection, buffer, snapshot)
};
let selection_range = selection.range();
let start = editor_snapshot
.display_snapshot
.buffer_snapshot
.anchor_after(selection_range.start)
.text_anchor;
let end = editor_snapshot
.display_snapshot
.buffer_snapshot
.anchor_after(selection_range.end)
.text_anchor;
let location = Location {
buffer,
range: start..end,
};
let captured_variables = {
let mut variables = TaskVariables::default();
let buffer = location.buffer.read(cx);
let buffer_id = buffer.remote_id();
let snapshot = buffer.snapshot();
let starting_point = location.range.start.to_point(&snapshot);
let starting_offset = starting_point.to_offset(&snapshot);
for (_, tasks) in self
.tasks
.range((buffer_id, 0)..(buffer_id, starting_point.row + 1))
let snapshot = editor.snapshot(window, cx);
(selection, buffer, snapshot)
};
let selection_range = selection.range();
let start = editor_snapshot
.display_snapshot
.buffer_snapshot
.anchor_after(selection_range.start)
.text_anchor;
let end = editor_snapshot
.display_snapshot
.buffer_snapshot
.anchor_after(selection_range.end)
.text_anchor;
let location = Location {
buffer,
range: start..end,
};
let captured_variables = {
let mut variables = TaskVariables::default();
let buffer = location.buffer.read(cx);
let buffer_id = buffer.remote_id();
let snapshot = buffer.snapshot();
let starting_point = location.range.start.to_point(&snapshot);
let starting_offset = starting_point.to_offset(&snapshot);
for (_, tasks) in editor
.tasks
.range((buffer_id, 0)..(buffer_id, starting_point.row + 1))
{
if !tasks
.context_range
.contains(&crate::BufferOffset(starting_offset))
{
if !tasks
.context_range
.contains(&crate::BufferOffset(starting_offset))
{
continue;
}
for (capture_name, value) in tasks.extra_variables.iter() {
variables.insert(
VariableName::Custom(capture_name.to_owned().into()),
value.clone(),
);
}
continue;
}
variables
};
for (capture_name, value) in tasks.extra_variables.iter() {
variables.insert(
VariableName::Custom(capture_name.to_owned().into()),
value.clone(),
);
}
}
variables
};
project.update(cx, |project, cx| {
project.task_store().update(cx, |task_store, cx| {
task_store.task_context_for_location(captured_variables, location, cx)
})
project.update(cx, |project, cx| {
project.task_store().update(cx, |task_store, cx| {
task_store.task_context_for_location(captured_variables, location, cx)
})
}
})
}
pub fn task_context(
workspace: &Workspace,
window: &mut Window,
cx: &mut App,
) -> AsyncTask<TaskContext> {
let Some(editor) = workspace
.active_item(cx)
.and_then(|item| item.act_as::<Editor>(cx))
else {
return AsyncTask::ready(TaskContext::default());
};
editor.update(cx, |editor, cx| {
let context_task = task_context_with_editor(editor, window, cx);
cx.background_spawn(async move { context_task.await.unwrap_or_default() })
})
}

View File

@@ -285,7 +285,7 @@ impl EditorTestContext {
snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
}
pub fn set_head_text(&mut self, diff_base: &str) {
pub fn set_diff_base(&mut self, diff_base: &str) {
self.cx.run_until_parked();
let fs = self.update_editor(|editor, _, cx| {
editor.project.as_ref().unwrap().read(cx).fs().as_fake()
@@ -298,28 +298,6 @@ impl EditorTestContext {
self.cx.run_until_parked();
}
pub fn clear_index_text(&mut self) {
self.cx.run_until_parked();
let fs = self.update_editor(|editor, _, cx| {
editor.project.as_ref().unwrap().read(cx).fs().as_fake()
});
fs.set_index_for_repo(&Self::root_path().join(".git"), &[]);
self.cx.run_until_parked();
}
pub fn set_index_text(&mut self, diff_base: &str) {
self.cx.run_until_parked();
let fs = self.update_editor(|editor, _, cx| {
editor.project.as_ref().unwrap().read(cx).fs().as_fake()
});
let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone());
fs.set_index_for_repo(
&Self::root_path().join(".git"),
&[(path.into(), diff_base.to_string())],
);
self.cx.run_until_parked();
}
#[track_caller]
pub fn assert_index_text(&mut self, expected: Option<&str>) {
let fs = self.update_editor(|editor, _, cx| {

View File

@@ -275,11 +275,7 @@ async fn run_evaluation(
let db_path = Path::new(EVAL_DB_PATH);
let api_key = std::env::var("OPENAI_API_KEY").unwrap();
let git_hosting_provider_registry = Arc::new(GitHostingProviderRegistry::new());
let fs = Arc::new(RealFs::new(
git_hosting_provider_registry,
None,
PathBuf::from("/non/existent/askpass"),
)) as Arc<dyn Fs>;
let fs = Arc::new(RealFs::new(git_hosting_provider_registry, None)) as Arc<dyn Fs>;
let clock = Arc::new(RealSystemClock);
let client = cx
.update(|cx| {

View File

@@ -181,10 +181,7 @@ pub fn register_extension(build_extension: fn() -> Box<dyn Extension>) {
}
fn extension() -> &'static mut dyn Extension {
#[expect(static_mut_refs)]
unsafe {
EXTENSION.as_deref_mut().unwrap()
}
unsafe { EXTENSION.as_deref_mut().unwrap() }
}
static mut EXTENSION: Option<Box<dyn Extension>> = None;

View File

@@ -78,9 +78,10 @@ impl HeadlessExtensionStore {
if e.dev {
return true;
}
self.loaded_extensions
!self
.loaded_extensions
.get(e.id.as_str())
.is_none_or(|loaded| loaded.as_ref() != e.version.as_str())
.is_some_and(|loaded| loaded.as_ref() == e.version.as_str())
})
.collect();

View File

@@ -348,7 +348,7 @@ impl Extension {
.call_labels_for_completions(
store,
&language_server_id.0,
&completions.into_iter().collect::<Vec<_>>(),
&completions.into_iter().map(Into::into).collect::<Vec<_>>(),
)
.await?
.map(|labels| {
@@ -402,7 +402,7 @@ impl Extension {
.call_labels_for_symbols(
store,
&language_server_id.0,
&symbols.into_iter().collect::<Vec<_>>(),
&symbols.into_iter().map(Into::into).collect::<Vec<_>>(),
)
.await?
.map(|labels| {

View File

@@ -909,9 +909,7 @@ impl FileFinderDelegate {
(normal, small)
};
let budget = full_path_budget(&file_name, normal_em, small_em, max_width);
// If the computed budget is zero, we certainly won't be able to achieve it,
// so no point trying to elide the path.
if budget > 0 && full_path.len() > budget {
if full_path.len() > budget {
let components = PathComponentSlice::new(&full_path);
if let Some(elided_range) =
components.elision_range(budget - 1, &full_path_positions)
@@ -1195,7 +1193,6 @@ impl PickerDelegate for FileFinderDelegate {
None,
true,
allow_preview,
true,
window,
cx,
)
@@ -1451,9 +1448,9 @@ impl<'a> PathComponentSlice<'a> {
matches.next();
}
if is_first_normal || is_last || !is_normal || contains_match {
if longest
if !longest
.as_ref()
.is_none_or(|old| old.end - old.start <= cur.end - cur.start)
.is_some_and(|old| old.end - old.start > cur.end - cur.start)
{
longest = Some(cur);
}
@@ -1462,9 +1459,9 @@ impl<'a> PathComponentSlice<'a> {
cur.end = i + 1;
}
}
if longest
if !longest
.as_ref()
.is_none_or(|old| old.end - old.start <= cur.end - cur.start)
.is_some_and(|old| old.end - old.start > cur.end - cur.start)
{
longest = Some(cur);
}

View File

@@ -108,7 +108,7 @@ impl Match {
fn styled_text(&self, project: &Project, window: &Window, cx: &App) -> StyledText {
let mut text = "./".to_string();
let mut highlights = Vec::new();
let mut offset = text.len();
let mut offset = text.as_bytes().len();
let separator = '/';
let dir_indicator = "[…]";
@@ -125,7 +125,7 @@ impl Match {
highlights.push((range.start + offset..range.end + offset, style))
}
text.push(separator);
offset = text.len();
offset = text.as_bytes().len();
if let Some(suffix) = &self.suffix {
text.push_str(suffix);
@@ -140,10 +140,10 @@ impl Match {
Color::Created
};
highlights.push((
offset..offset + suffix.len(),
offset..offset + suffix.as_bytes().len(),
HighlightStyle::color(color.color(cx)),
));
offset += suffix.len();
offset += suffix.as_bytes().len();
if entry.is_some_and(|e| e.is_dir()) {
text.push(separator);
offset += separator.len_utf8();
@@ -165,7 +165,7 @@ impl Match {
text.push_str(suffix);
let existing_prefix_len = self
.existing_prefix(project, cx)
.map(|prefix| prefix.to_string_lossy().len())
.map(|prefix| prefix.to_string_lossy().as_bytes().len())
.unwrap_or(0);
if existing_prefix_len > 0 {
@@ -175,14 +175,14 @@ impl Match {
));
}
highlights.push((
offset + existing_prefix_len..offset + suffix.len(),
offset + existing_prefix_len..offset + suffix.as_bytes().len(),
HighlightStyle::color(if self.entry(project, cx).is_some() {
Color::Conflict.color(cx)
} else {
Color::Created.color(cx)
}),
));
offset += suffix.len();
offset += suffix.as_bytes().len();
if suffix.ends_with('/') {
text.push_str(dir_indicator);
highlights.push((

View File

@@ -5,20 +5,12 @@ mod mac_watcher;
pub mod fs_watcher;
use anyhow::{anyhow, Context as _, Result};
#[cfg(any(test, feature = "test-support"))]
use collections::HashMap;
#[cfg(any(test, feature = "test-support"))]
use git::status::StatusCode;
#[cfg(any(test, feature = "test-support"))]
use git::status::TrackedStatus;
use git::GitHostingProviderRegistry;
#[cfg(any(test, feature = "test-support"))]
use git::{repository::RepoPath, status::FileStatus};
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
use ashpd::desktop::trash;
#[cfg(any(test, feature = "test-support"))]
use std::collections::HashSet;
#[cfg(unix)]
use std::os::fd::AsFd;
#[cfg(unix)]
@@ -248,7 +240,6 @@ impl From<MTime> for proto::Timestamp {
pub struct RealFs {
git_hosting_provider_registry: Arc<GitHostingProviderRegistry>,
git_binary_path: Option<PathBuf>,
askpass_path: PathBuf,
}
pub trait FileHandle: Send + Sync + std::fmt::Debug {
@@ -303,12 +294,10 @@ impl RealFs {
pub fn new(
git_hosting_provider_registry: Arc<GitHostingProviderRegistry>,
git_binary_path: Option<PathBuf>,
askpass_path: PathBuf,
) -> Self {
Self {
git_hosting_provider_registry,
git_binary_path,
askpass_path,
}
}
}
@@ -772,7 +761,6 @@ impl Fs for RealFs {
Some(Arc::new(RealGitRepository::new(
repo,
self.git_binary_path.clone(),
self.askpass_path.to_owned(),
self.git_hosting_provider_registry.clone(),
)))
}
@@ -1265,7 +1253,7 @@ impl FakeFs {
self.with_git_state(dot_git, true, |state| {
let branch = branch.map(Into::into);
state.branches.extend(branch.clone());
state.current_branch_name = branch
state.current_branch_name = branch.map(Into::into)
})
}
@@ -1304,105 +1292,6 @@ impl FakeFs {
});
}
pub fn set_git_content_for_repo(
&self,
dot_git: &Path,
head_state: &[(RepoPath, String, Option<String>)],
) {
self.with_git_state(dot_git, true, |state| {
state.head_contents.clear();
state.head_contents.extend(
head_state
.iter()
.map(|(path, head_content, _)| (path.clone(), head_content.clone())),
);
state.index_contents.clear();
state.index_contents.extend(head_state.iter().map(
|(path, head_content, index_content)| {
(
path.clone(),
index_content.as_ref().unwrap_or(head_content).clone(),
)
},
));
});
self.recalculate_git_status(dot_git);
}
pub fn recalculate_git_status(&self, dot_git: &Path) {
let git_files: HashMap<_, _> = self
.files()
.iter()
.filter_map(|path| {
let repo_path =
RepoPath::new(path.strip_prefix(dot_git.parent().unwrap()).ok()?.into());
let content = self
.read_file_sync(path)
.ok()
.map(|content| String::from_utf8(content).unwrap());
Some((repo_path, content?))
})
.collect();
self.with_git_state(dot_git, false, |state| {
state.statuses.clear();
let mut paths: HashSet<_> = state.head_contents.keys().collect();
paths.extend(state.index_contents.keys());
paths.extend(git_files.keys());
for path in paths {
let head = state.head_contents.get(path);
let index = state.index_contents.get(path);
let fs = git_files.get(path);
let status = match (head, index, fs) {
(Some(head), Some(index), Some(fs)) => FileStatus::Tracked(TrackedStatus {
index_status: if head == index {
StatusCode::Unmodified
} else {
StatusCode::Modified
},
worktree_status: if fs == index {
StatusCode::Unmodified
} else {
StatusCode::Modified
},
}),
(Some(head), Some(index), None) => FileStatus::Tracked(TrackedStatus {
index_status: if head == index {
StatusCode::Unmodified
} else {
StatusCode::Modified
},
worktree_status: StatusCode::Deleted,
}),
(Some(_), None, Some(_)) => FileStatus::Tracked(TrackedStatus {
index_status: StatusCode::Deleted,
worktree_status: StatusCode::Added,
}),
(Some(_), None, None) => FileStatus::Tracked(TrackedStatus {
index_status: StatusCode::Deleted,
worktree_status: StatusCode::Deleted,
}),
(None, Some(index), Some(fs)) => FileStatus::Tracked(TrackedStatus {
index_status: StatusCode::Added,
worktree_status: if fs == index {
StatusCode::Unmodified
} else {
StatusCode::Modified
},
}),
(None, Some(_), None) => FileStatus::Tracked(TrackedStatus {
index_status: StatusCode::Added,
worktree_status: StatusCode::Deleted,
}),
(None, None, Some(_)) => FileStatus::Untracked,
(None, None, None) => {
unreachable!();
}
};
state.statuses.insert(path.clone(), status);
}
});
}
pub fn set_blame_for_repo(&self, dot_git: &Path, blames: Vec<(RepoPath, git::blame::Blame)>) {
self.with_git_state(dot_git, true, |state| {
state.blames.clear();
@@ -1448,10 +1337,7 @@ impl FakeFs {
pub fn paths(&self, include_dot_git: bool) -> Vec<PathBuf> {
let mut result = Vec::new();
let mut queue = collections::VecDeque::new();
queue.push_back((
PathBuf::from(util::path!("/")),
self.state.lock().root.clone(),
));
queue.push_back((PathBuf::from("/"), self.state.lock().root.clone()));
while let Some((path, entry)) = queue.pop_front() {
if let FakeFsEntry::Dir { entries, .. } = &*entry.lock() {
for (name, entry) in entries {
@@ -1472,10 +1358,7 @@ impl FakeFs {
pub fn directories(&self, include_dot_git: bool) -> Vec<PathBuf> {
let mut result = Vec::new();
let mut queue = collections::VecDeque::new();
queue.push_back((
PathBuf::from(util::path!("/")),
self.state.lock().root.clone(),
));
queue.push_back((PathBuf::from("/"), self.state.lock().root.clone()));
while let Some((path, entry)) = queue.pop_front() {
if let FakeFsEntry::Dir { entries, .. } = &*entry.lock() {
for (name, entry) in entries {
@@ -2137,11 +2020,7 @@ pub async fn copy_recursive<'a>(
let Ok(item_relative_path) = item.strip_prefix(source) else {
continue;
};
let target_item = if item_relative_path == Path::new("") {
target.to_path_buf()
} else {
target.join(item_relative_path)
};
let target_item = target.join(item_relative_path);
if is_dir {
if !options.overwrite && fs.metadata(&target_item).await.is_ok_and(|m| m.is_some()) {
if options.ignore_if_exists {
@@ -2295,142 +2174,6 @@ mod tests {
);
}
#[gpui::test]
async fn test_copy_recursive_with_single_file(executor: BackgroundExecutor) {
let fs = FakeFs::new(executor.clone());
fs.insert_tree(
path!("/outer"),
json!({
"a": "A",
"b": "B",
"inner": {}
}),
)
.await;
assert_eq!(
fs.files(),
vec![
PathBuf::from(path!("/outer/a")),
PathBuf::from(path!("/outer/b")),
]
);
let source = Path::new(path!("/outer/a"));
let target = Path::new(path!("/outer/a copy"));
copy_recursive(fs.as_ref(), source, target, Default::default())
.await
.unwrap();
assert_eq!(
fs.files(),
vec![
PathBuf::from(path!("/outer/a")),
PathBuf::from(path!("/outer/a copy")),
PathBuf::from(path!("/outer/b")),
]
);
let source = Path::new(path!("/outer/a"));
let target = Path::new(path!("/outer/inner/a copy"));
copy_recursive(fs.as_ref(), source, target, Default::default())
.await
.unwrap();
assert_eq!(
fs.files(),
vec![
PathBuf::from(path!("/outer/a")),
PathBuf::from(path!("/outer/a copy")),
PathBuf::from(path!("/outer/b")),
PathBuf::from(path!("/outer/inner/a copy")),
]
);
}
#[gpui::test]
async fn test_copy_recursive_with_single_dir(executor: BackgroundExecutor) {
let fs = FakeFs::new(executor.clone());
fs.insert_tree(
path!("/outer"),
json!({
"a": "A",
"empty": {},
"non-empty": {
"b": "B",
}
}),
)
.await;
assert_eq!(
fs.files(),
vec![
PathBuf::from(path!("/outer/a")),
PathBuf::from(path!("/outer/non-empty/b")),
]
);
assert_eq!(
fs.directories(false),
vec![
PathBuf::from(path!("/")),
PathBuf::from(path!("/outer")),
PathBuf::from(path!("/outer/empty")),
PathBuf::from(path!("/outer/non-empty")),
]
);
let source = Path::new(path!("/outer/empty"));
let target = Path::new(path!("/outer/empty copy"));
copy_recursive(fs.as_ref(), source, target, Default::default())
.await
.unwrap();
assert_eq!(
fs.files(),
vec![
PathBuf::from(path!("/outer/a")),
PathBuf::from(path!("/outer/non-empty/b")),
]
);
assert_eq!(
fs.directories(false),
vec![
PathBuf::from(path!("/")),
PathBuf::from(path!("/outer")),
PathBuf::from(path!("/outer/empty")),
PathBuf::from(path!("/outer/empty copy")),
PathBuf::from(path!("/outer/non-empty")),
]
);
let source = Path::new(path!("/outer/non-empty"));
let target = Path::new(path!("/outer/non-empty copy"));
copy_recursive(fs.as_ref(), source, target, Default::default())
.await
.unwrap();
assert_eq!(
fs.files(),
vec![
PathBuf::from(path!("/outer/a")),
PathBuf::from(path!("/outer/non-empty/b")),
PathBuf::from(path!("/outer/non-empty copy/b")),
]
);
assert_eq!(
fs.directories(false),
vec![
PathBuf::from(path!("/")),
PathBuf::from(path!("/outer")),
PathBuf::from(path!("/outer/empty")),
PathBuf::from(path!("/outer/empty copy")),
PathBuf::from(path!("/outer/non-empty")),
PathBuf::from(path!("/outer/non-empty copy")),
]
);
}
#[gpui::test]
async fn test_copy_recursive(executor: BackgroundExecutor) {
let fs = FakeFs::new(executor.clone());
@@ -2442,8 +2185,7 @@ mod tests {
"b": "B",
"inner3": {
"d": "D",
},
"inner4": {}
}
},
"inner2": {
"c": "C",
@@ -2461,17 +2203,6 @@ mod tests {
PathBuf::from(path!("/outer/inner1/inner3/d")),
]
);
assert_eq!(
fs.directories(false),
vec![
PathBuf::from(path!("/")),
PathBuf::from(path!("/outer")),
PathBuf::from(path!("/outer/inner1")),
PathBuf::from(path!("/outer/inner2")),
PathBuf::from(path!("/outer/inner1/inner3")),
PathBuf::from(path!("/outer/inner1/inner4")),
]
);
let source = Path::new(path!("/outer"));
let target = Path::new(path!("/outer/inner1/outer"));
@@ -2492,22 +2223,6 @@ mod tests {
PathBuf::from(path!("/outer/inner1/outer/inner1/inner3/d")),
]
);
assert_eq!(
fs.directories(false),
vec![
PathBuf::from(path!("/")),
PathBuf::from(path!("/outer")),
PathBuf::from(path!("/outer/inner1")),
PathBuf::from(path!("/outer/inner2")),
PathBuf::from(path!("/outer/inner1/inner3")),
PathBuf::from(path!("/outer/inner1/inner4")),
PathBuf::from(path!("/outer/inner1/outer")),
PathBuf::from(path!("/outer/inner1/outer/inner1")),
PathBuf::from(path!("/outer/inner1/outer/inner2")),
PathBuf::from(path!("/outer/inner1/outer/inner1/inner3")),
PathBuf::from(path!("/outer/inner1/outer/inner1/inner4")),
]
);
}
#[gpui::test]

View File

@@ -140,7 +140,7 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
let query_char_bag = CharBag::from(&lowercase_query[..]);
let num_cpus = executor.num_cpus().min(path_count);
let segment_size = path_count.div_ceil(num_cpus);
let segment_size = (path_count + num_cpus - 1) / num_cpus;
let mut segment_results = (0..num_cpus)
.map(|_| Vec::with_capacity(max_results))
.collect::<Vec<_>>();

View File

@@ -145,7 +145,7 @@ pub async fn match_strings(
let query_char_bag = CharBag::from(&lowercase_query[..]);
let num_cpus = executor.num_cpus().min(candidates.len());
let segment_size = candidates.len().div_ceil(num_cpus);
let segment_size = (candidates.len() + num_cpus - 1) / num_cpus;
let mut segment_results = (0..num_cpus)
.map(|_| Vec::with_capacity(max_results.min(candidates.len())))
.collect::<Vec<_>>();

View File

@@ -30,7 +30,6 @@ schemars.workspace = true
serde.workspace = true
smol.workspace = true
sum_tree.workspace = true
tempfile.workspace = true
text.workspace = true
time.workspace = true
url.workspace = true

View File

@@ -344,7 +344,7 @@ mod tests {
have_json.push('\n');
let update = std::env::var("UPDATE_GOLDEN")
.map(|val| val.eq_ignore_ascii_case("true"))
.map(|val| val.to_ascii_lowercase() == "true")
.unwrap_or(false);
if update {

View File

@@ -10,11 +10,8 @@ use rope::Rope;
use schemars::JsonSchema;
use serde::Deserialize;
use std::borrow::Borrow;
use std::env::temp_dir;
use std::io::Write as _;
use std::os::unix::fs::PermissionsExt as _;
use std::os::unix::net::UnixListener;
use std::process::{Command, Stdio};
use std::process::Stdio;
use std::sync::LazyLock;
use std::{
cmp::Ordering,
@@ -203,7 +200,6 @@ impl std::fmt::Debug for dyn GitRepository {
pub struct RealGitRepository {
pub repository: Mutex<git2::Repository>,
pub git_binary_path: PathBuf,
pub askpass_path: PathBuf,
hosting_provider_registry: Arc<GitHostingProviderRegistry>,
}
@@ -211,13 +207,11 @@ impl RealGitRepository {
pub fn new(
repository: git2::Repository,
git_binary_path: Option<PathBuf>,
askpass_path: PathBuf,
hosting_provider_registry: Arc<GitHostingProviderRegistry>,
) -> Self {
Self {
repository: Mutex::new(repository),
git_binary_path: git_binary_path.unwrap_or_else(|| PathBuf::from("git")),
askpass_path,
hosting_provider_registry,
}
}
@@ -364,30 +358,24 @@ impl GitRepository for RealGitRepository {
log::debug!("indexing SHA: {sha}, path {path:?}");
let output = new_std_command(&self.git_binary_path)
let status = new_std_command(&self.git_binary_path)
.current_dir(&working_directory)
.args(["update-index", "--add", "--cacheinfo", "100644", &sha])
.arg(path.as_ref())
.output()?;
.status()?;
if !output.status.success() {
return Err(anyhow!(
"Failed to stage:\n{}",
String::from_utf8_lossy(&output.stderr)
));
if !status.success() {
return Err(anyhow!("Failed to add to index: {status:?}"));
}
} else {
let output = new_std_command(&self.git_binary_path)
let status = new_std_command(&self.git_binary_path)
.current_dir(&working_directory)
.args(["update-index", "--force-remove"])
.arg(path.as_ref())
.output()?;
.status()?;
if !output.status.success() {
return Err(anyhow!(
"Failed to unstage:\n{}",
String::from_utf8_lossy(&output.stderr)
));
if !status.success() {
return Err(anyhow!("Failed to remove from index: {status:?}"));
}
}
@@ -614,10 +602,7 @@ impl GitRepository for RealGitRepository {
) -> Result<()> {
let working_directory = self.working_directory()?;
// We don't use the bundled git, so we can ensure that system
// credential management and transfer mechanisms are respected
let output = new_std_command("git")
.env("GIT_ASKPASS", &self.askpass_path)
let output = new_std_command(&self.git_binary_path)
.current_dir(&working_directory)
.args(["push", "--quiet"])
.args(options.map(|option| match option {
@@ -633,20 +618,18 @@ impl GitRepository for RealGitRepository {
"Failed to push:\n{}",
String::from_utf8_lossy(&output.stderr)
));
} else {
Ok(())
}
// TODO: Get remote response out of this and show it to the user
Ok(())
}
fn pull(&self, branch_name: &str, remote_name: &str) -> Result<()> {
let working_directory = self.working_directory()?;
// We don't use the bundled git, so we can ensure that system
// credential management and transfer mechanisms are respected
let output = new_std_command("git")
.env("GIT_ASKPASS", &self.askpass_path)
let output = new_std_command(&self.git_binary_path)
.current_dir(&working_directory)
.args(["pull"])
.args(["pull", "--quiet"])
.arg(remote_name)
.arg(branch_name)
.output()?;
@@ -656,18 +639,16 @@ impl GitRepository for RealGitRepository {
"Failed to pull:\n{}",
String::from_utf8_lossy(&output.stderr)
));
} else {
return Ok(());
}
// TODO: Get remote response out of this and show it to the user
Ok(())
}
fn fetch(&self) -> Result<()> {
let working_directory = self.working_directory()?;
// We don't use the bundled git, so we can ensure that system
// credential management and transfer mechanisms are respected
let output = new_std_command("git")
.env("GIT_ASKPASS", &self.askpass_path)
let output = new_std_command(&self.git_binary_path)
.current_dir(&working_directory)
.args(["fetch", "--quiet", "--all"])
.output()?;
@@ -677,9 +658,10 @@ impl GitRepository for RealGitRepository {
"Failed to fetch:\n{}",
String::from_utf8_lossy(&output.stderr)
));
} else {
return Ok(());
}
// TODO: Get remote response out of this and show it to the user
Ok(())
}
fn get_remotes(&self, branch_name: Option<&str>) -> Result<Vec<Remote>> {
@@ -1034,7 +1016,7 @@ impl Borrow<Path> for RepoPath {
#[derive(Debug)]
pub struct RepoPathDescendants<'a>(pub &'a Path);
impl MapSeekTarget<RepoPath> for RepoPathDescendants<'_> {
impl<'a> MapSeekTarget<RepoPath> for RepoPathDescendants<'a> {
fn cmp_cursor(&self, key: &RepoPath) -> Ordering {
if key.starts_with(self.0) {
Ordering::Greater

View File

@@ -12,10 +12,6 @@ workspace = true
name = "git_ui"
path = "src/git_ui.rs"
[features]
default = []
test-support = ["multi_buffer/test-support"]
[dependencies]
anyhow.workspace = true
buffer_diff.workspace = true
@@ -51,10 +47,5 @@ zed_actions.workspace = true
[target.'cfg(windows)'.dependencies]
windows.workspace = true
[dev-dependencies]
editor = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
project = { workspace = true, features = ["test-support"] }
settings = { workspace = true, features = ["test-support"] }
unindent.workspace = true
workspace = { workspace = true, features = ["test-support"] }
[features]
default = []

View File

@@ -1,18 +1,16 @@
use anyhow::{Context as _, Result};
use anyhow::{anyhow, Context as _, Result};
use fuzzy::{StringMatch, StringMatchCandidate};
use git::repository::Branch;
use gpui::{
rems, App, AsyncApp, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
Task, Window,
Task, WeakEntity, Window,
};
use picker::{Picker, PickerDelegate};
use project::{Project, ProjectPath};
use project::ProjectPath;
use std::sync::Arc;
use ui::{
prelude::*, HighlightedLabel, ListItem, ListItemSpacing, PopoverMenuHandle, TriggerablePopover,
};
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
use util::ResultExt;
use workspace::notifications::DetachAndPromptErr;
use workspace::{ModalView, Workspace};
@@ -25,29 +23,19 @@ pub fn init(cx: &mut App) {
}
pub fn open(
workspace: &mut Workspace,
_: &mut Workspace,
_: &zed_actions::git::Branch,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
let project = workspace.project().clone();
let this = cx.entity();
let style = BranchListStyle::Modal;
let this = cx.entity().clone();
cx.spawn_in(window, |_, mut cx| async move {
// Modal branch picker has a longer trailoff than a popover one.
let delegate = BranchListDelegate::new(project.clone(), style, 70, &cx).await?;
let delegate = BranchListDelegate::new(this.clone(), 70, &cx).await?;
this.update_in(&mut cx, move |workspace, window, cx| {
this.update_in(&mut cx, |workspace, window, cx| {
workspace.toggle_modal(window, cx, |window, cx| {
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
let _subscription = cx.subscribe(&picker, |_, _, _, cx| {
cx.emit(DismissEvent);
});
let mut list = BranchList::new(project, style, 34., cx);
list._subscription = Some(_subscription);
list.picker = Some(picker);
list
BranchList::new(delegate, 34., window, cx)
})
})?;
@@ -56,86 +44,34 @@ pub fn open(
.detach_and_prompt_err("Failed to read branches", window, cx, |_, _, _| None)
}
pub fn popover(project: Entity<Project>, window: &mut Window, cx: &mut App) -> Entity<BranchList> {
cx.new(|cx| {
let mut list = BranchList::new(project, BranchListStyle::Popover, 15., cx);
list.reload_branches(window, cx);
list
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum BranchListStyle {
Modal,
Popover,
}
pub struct BranchList {
pub picker: Entity<Picker<BranchListDelegate>>,
rem_width: f32,
popover_handle: PopoverMenuHandle<Self>,
default_focus_handle: FocusHandle,
project: Entity<Project>,
style: BranchListStyle,
pub picker: Option<Entity<Picker<BranchListDelegate>>>,
_subscription: Option<Subscription>,
}
impl TriggerablePopover for BranchList {
fn menu_handle(
&mut self,
_window: &mut Window,
_cx: &mut gpui::Context<Self>,
) -> PopoverMenuHandle<Self> {
self.popover_handle.clone()
}
_subscription: Subscription,
}
impl BranchList {
fn new(project: Entity<Project>, style: BranchListStyle, rem_width: f32, cx: &mut App) -> Self {
let popover_handle = PopoverMenuHandle::default();
pub fn new(
delegate: BranchListDelegate,
rem_width: f32,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
let _subscription = cx.subscribe(&picker, |_, _, _, cx| cx.emit(DismissEvent));
Self {
project,
picker: None,
picker,
rem_width,
popover_handle,
default_focus_handle: cx.focus_handle(),
style,
_subscription: None,
_subscription,
}
}
fn reload_branches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let project = self.project.clone();
let style = self.style;
cx.spawn_in(window, |this, mut cx| async move {
let delegate = BranchListDelegate::new(project, style, 20, &cx).await?;
let picker =
cx.new_window_entity(|window, cx| Picker::uniform_list(delegate, window, cx))?;
this.update(&mut cx, |branch_list, cx| {
let subscription =
cx.subscribe(&picker, |_, _, _: &DismissEvent, cx| cx.emit(DismissEvent));
branch_list.picker = Some(picker);
branch_list._subscription = Some(subscription);
cx.notify();
})?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
}
impl ModalView for BranchList {}
impl EventEmitter<DismissEvent> for BranchList {}
impl Focusable for BranchList {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.picker
.as_ref()
.map(|picker| picker.focus_handle(cx))
.unwrap_or_else(|| self.default_focus_handle.clone())
self.picker.focus_handle(cx)
}
}
@@ -143,25 +79,12 @@ impl Render for BranchList {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.w(rems(self.rem_width))
.map(|parent| match self.picker.as_ref() {
Some(picker) => parent.child(picker.clone()).on_mouse_down_out({
let picker = picker.clone();
cx.listener(move |_, _, window, cx| {
picker.update(cx, |this, cx| {
this.cancel(&Default::default(), window, cx);
})
})
}),
None => parent.child(
h_flex()
.id("branch-picker-error")
.on_click(
cx.listener(|this, _, window, cx| this.reload_branches(window, cx)),
)
.child("Could not load branches.")
.child("Click to retry"),
),
})
.child(self.picker.clone())
.on_mouse_down_out(cx.listener(|this, _, window, cx| {
this.picker.update(cx, |this, cx| {
this.cancel(&Default::default(), window, cx);
})
}))
}
}
@@ -185,8 +108,7 @@ impl BranchEntry {
pub struct BranchListDelegate {
matches: Vec<BranchEntry>,
all_branches: Vec<Branch>,
project: Entity<Project>,
style: BranchListStyle,
workspace: WeakEntity<Workspace>,
selected_index: usize,
last_query: String,
/// Max length of branch name before we truncate it and add a trailing `...`.
@@ -194,14 +116,13 @@ pub struct BranchListDelegate {
}
impl BranchListDelegate {
async fn new(
project: Entity<Project>,
style: BranchListStyle,
pub async fn new(
workspace: Entity<Workspace>,
branch_name_trailoff_after: usize,
cx: &AsyncApp,
) -> Result<Self> {
let all_branches_request = cx.update(|cx| {
let project = project.read(cx);
let project = workspace.read(cx).project().read(cx);
let first_worktree = project
.visible_worktrees(cx)
.next()
@@ -214,8 +135,7 @@ impl BranchListDelegate {
Ok(Self {
matches: vec![],
project,
style,
workspace: workspace.downgrade(),
all_branches,
selected_index: 0,
last_query: Default::default(),
@@ -334,12 +254,18 @@ impl PickerDelegate for BranchListDelegate {
return;
};
let current_branch = self.project.update(cx, |project, cx| {
project
.active_repository(cx)
.and_then(|repo| repo.read(cx).current_branch())
.map(|branch| branch.name.to_string())
});
let current_branch = self
.workspace
.update(cx, |workspace, cx| {
workspace
.project()
.read(cx)
.active_repository(cx)
.and_then(|repo| repo.read(cx).current_branch())
.map(|branch| branch.name.to_string())
})
.ok()
.flatten();
if current_branch == Some(branch.name().to_string()) {
cx.emit(DismissEvent);
@@ -350,7 +276,13 @@ impl PickerDelegate for BranchListDelegate {
let branch = branch.clone();
|picker, mut cx| async move {
let branch_change_task = picker.update(&mut cx, |this, cx| {
let project = this.delegate.project.read(cx);
let workspace = this
.delegate
.workspace
.upgrade()
.ok_or_else(|| anyhow!("workspace was dropped"))?;
let project = workspace.read(cx).project().read(cx);
let branch_to_checkout = match branch {
BranchEntry::Branch(branch) => branch.string,
BranchEntry::History(string) => string,
@@ -395,10 +327,6 @@ impl PickerDelegate for BranchListDelegate {
Some(
ListItem::new(SharedString::from(format!("vcs-menu-{ix}")))
.inset(true)
.spacing(match self.style {
BranchListStyle::Modal => ListItemSpacing::default(),
BranchListStyle::Popover => ListItemSpacing::ExtraDense,
})
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.when(matches!(hit, BranchEntry::History(_)), |el| {

View File

@@ -1,59 +1,28 @@
// #![allow(unused, dead_code)]
#![allow(unused, dead_code)]
use crate::branch_picker::{self, BranchList};
use crate::git_panel::{commit_message_editor, GitPanel};
use crate::repository_selector::RepositorySelector;
use anyhow::Result;
use git::Commit;
use panel::{panel_button, panel_editor_style, panel_filled_button};
use project::Project;
use ui::{prelude::*, KeybindingHint, PopoverButton, Tooltip, TriggerablePopover};
use editor::{Editor, EditorElement};
use gpui::*;
use util::ResultExt;
use workspace::{
dock::{Dock, PanelHandle},
ModalView, Workspace,
use language::language_settings::LanguageSettings;
use language::Buffer;
use panel::{
panel_button, panel_editor_container, panel_editor_style, panel_filled_button,
panel_icon_button,
};
use settings::Settings;
use theme::ThemeSettings;
use ui::{prelude::*, KeybindingHint, Tooltip};
// nate: It is a pain to get editors to size correctly and not overflow.
//
// this can get replaced with a simple flex layout with more time/a more thoughtful approach.
#[derive(Debug, Clone, Copy)]
pub struct ModalContainerProperties {
pub modal_width: f32,
pub editor_height: f32,
pub footer_height: f32,
pub container_padding: f32,
pub modal_border_radius: f32,
}
use editor::{Direction, Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer};
use gpui::*;
use project::git::Repository;
use project::{Fs, Project};
use std::sync::Arc;
use workspace::dock::{Dock, DockPosition, PanelHandle};
use workspace::{ModalView, Workspace};
impl ModalContainerProperties {
pub fn new(window: &Window, preferred_char_width: usize) -> Self {
let container_padding = 5.0;
// Calculate width based on character width
let mut modal_width = 460.0;
let style = window.text_style().clone();
let font_id = window.text_system().resolve_font(&style.font());
let font_size = style.font_size.to_pixels(window.rem_size());
if let Ok(em_width) = window.text_system().em_width(font_id, font_size) {
modal_width = preferred_char_width as f32 * em_width.0 + (container_padding * 2.0);
}
Self {
modal_width,
editor_height: 300.0,
footer_height: 24.0,
container_padding,
modal_border_radius: 12.0,
}
}
pub fn editor_border_radius(&self) -> Pixels {
px(self.modal_border_radius - self.container_padding / 2.0)
}
}
// actions!(commit_modal, [NextSuggestion, PrevSuggestion]);
pub fn init(cx: &mut App) {
cx.observe_new(|workspace: &mut Workspace, window, cx| {
@@ -66,11 +35,11 @@ pub fn init(cx: &mut App) {
}
pub struct CommitModal {
branch_list: Entity<BranchList>,
git_panel: Entity<GitPanel>,
commit_editor: Entity<Editor>,
restore_dock: RestoreDock,
properties: ModalContainerProperties,
current_suggestion: Option<usize>,
suggested_messages: Vec<SharedString>,
}
impl Focusable for CommitModal {
@@ -89,15 +58,12 @@ impl ModalView for CommitModal {
self.git_panel.update(cx, |git_panel, cx| {
git_panel.set_modal_open(false, cx);
});
self.restore_dock
.dock
.update(cx, |dock, cx| {
if let Some(active_index) = self.restore_dock.active_index {
dock.activate_panel(active_index, window, cx)
}
dock.set_open(self.restore_dock.is_open, window, cx)
})
.log_err();
self.restore_dock.dock.update(cx, |dock, cx| {
if let Some(active_index) = self.restore_dock.active_index {
dock.activate_panel(active_index, window, cx)
}
dock.set_open(self.restore_dock.is_open, window, cx)
});
workspace::DismissDecision::Dismiss(true)
}
}
@@ -109,13 +75,13 @@ struct RestoreDock {
}
impl CommitModal {
pub fn register(workspace: &mut Workspace, _: &mut Window, _cx: &mut Context<Workspace>) {
pub fn register(workspace: &mut Workspace, _: &mut Window, cx: &mut Context<Workspace>) {
workspace.register_action(|workspace, _: &Commit, window, cx| {
let Some(git_panel) = workspace.panel::<GitPanel>(cx) else {
return;
};
let (can_commit, conflict) = git_panel.update(cx, |git_panel, _cx| {
let (can_commit, conflict) = git_panel.update(cx, |git_panel, cx| {
let can_commit = git_panel.can_commit();
let conflict = git_panel.has_unstaged_conflicts();
(can_commit, conflict)
@@ -142,11 +108,9 @@ impl CommitModal {
is_open,
active_index,
};
let project = workspace.project().clone();
workspace.open_panel::<GitPanel>(window, cx);
workspace.toggle_modal(window, cx, move |window, cx| {
CommitModal::new(git_panel, restore_dock_position, project, window, cx)
CommitModal::new(git_panel, restore_dock_position, window, cx)
})
});
}
@@ -154,7 +118,6 @@ impl CommitModal {
fn new(
git_panel: Entity<GitPanel>,
restore_dock: RestoreDock,
project: Entity<Project>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@@ -165,7 +128,7 @@ impl CommitModal {
git_panel.set_modal_open(true, cx);
let buffer = git_panel.commit_message_buffer(cx).clone();
let project = git_panel.project.clone();
cx.new(|cx| commit_message_editor(buffer, None, project.clone(), false, window, cx))
cx.new(|cx| commit_message_editor(buffer, project.clone(), false, window, cx))
});
let commit_message = commit_editor.read(cx).text(cx);
@@ -186,77 +149,147 @@ impl CommitModal {
}
}
let focus_handle = commit_editor.focus_handle(cx);
cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
if !this
.branch_list
.focus_handle(cx)
.contains_focused(window, cx)
{
cx.emit(DismissEvent);
}
})
.detach();
let properties = ModalContainerProperties::new(window, 50);
Self {
branch_list: branch_picker::popover(project.clone(), window, cx),
git_panel,
commit_editor,
restore_dock,
properties,
current_suggestion: None,
suggested_messages: vec![],
}
}
/// Returns container `(width, x padding, border radius)`
fn container_properties(&self, window: &mut Window, cx: &mut Context<Self>) -> (f32, f32, f32) {
// TODO: Let's set the width based on your set wrap guide if possible
// let settings = EditorSettings::get_global(cx);
// let first_wrap_guide = self
// .commit_editor
// .read(cx)
// .wrap_guides(cx)
// .iter()
// .next()
// .map(|(guide, active)| if *active { Some(*guide) } else { None })
// .flatten();
// let preferred_width = if let Some(guide) = first_wrap_guide {
// guide
// } else {
// 80
// };
let border_radius = 16.0;
let preferred_width = 50; // (chars wide)
let mut width = 460.0;
let padding_x = 16.0;
let mut snapshot = self
.commit_editor
.update(cx, |editor, cx| editor.snapshot(window, cx));
let style = window.text_style().clone();
let font_id = window.text_system().resolve_font(&style.font());
let font_size = style.font_size.to_pixels(window.rem_size());
let line_height = style.line_height_in_pixels(window.rem_size());
if let Ok(em_width) = window.text_system().em_width(font_id, font_size) {
width = preferred_width as f32 * em_width.0 + (padding_x * 2.0);
cx.notify();
}
// cx.notify();
(width, padding_x, border_radius)
}
// fn cycle_suggested_messages(&mut self, direction: Direction, cx: &mut Context<Self>) {
// let new_index = match direction {
// Direction::Next => {
// (self.current_suggestion.unwrap_or(0) + 1).rem_euclid(self.suggested_messages.len())
// }
// Direction::Prev => {
// (self.current_suggestion.unwrap_or(0) + self.suggested_messages.len() - 1)
// .rem_euclid(self.suggested_messages.len())
// }
// };
// self.current_suggestion = Some(new_index);
// cx.notify();
// }
// fn next_suggestion(&mut self, _: &NextSuggestion, window: &mut Window, cx: &mut Context<Self>) {
// self.current_suggestion = Some(1);
// self.apply_suggestion(window, cx);
// }
// fn prev_suggestion(&mut self, _: &PrevSuggestion, window: &mut Window, cx: &mut Context<Self>) {
// self.current_suggestion = Some(0);
// self.apply_suggestion(window, cx);
// }
// fn set_commit_message(&mut self, message: &str, window: &mut Window, cx: &mut Context<Self>) {
// self.commit_editor.update(cx, |editor, cx| {
// editor.set_text(message.to_string(), window, cx)
// });
// self.current_suggestion = Some(0);
// cx.notify();
// }
// fn apply_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) {
// let suggested_messages = self.suggested_messages.clone();
// if let Some(suggestion) = self.current_suggestion {
// let suggested_message = &suggested_messages[suggestion];
// self.set_commit_message(suggested_message, window, cx);
// }
// cx.notify();
// }
fn commit_editor_element(&self, window: &mut Window, cx: &mut Context<Self>) -> EditorElement {
let mut editor = self.commit_editor.clone();
let editor_style = panel_editor_style(true, window, cx);
EditorElement::new(&self.commit_editor, editor_style)
}
pub fn render_commit_editor(
&self,
name_and_email: Option<(SharedString, SharedString)>,
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
let properties = self.properties;
let padding_t = 3.0;
let padding_b = 6.0;
// magic number for editor not to overflow the container??
let extra_space_hack = 1.5 * window.line_height();
let (width, padding_x, modal_border_radius) = self.container_properties(window, cx);
v_flex()
.h(px(properties.editor_height + padding_b + padding_t) + extra_space_hack)
.w_full()
.flex_none()
.rounded(properties.editor_border_radius())
.overflow_hidden()
.px_1p5()
.pt(px(padding_t))
.pb(px(padding_b))
.child(
div()
.h(px(properties.editor_height))
.w_full()
.child(self.commit_editor_element(window, cx)),
)
}
let border_radius = modal_border_radius - padding_x / 2.0;
pub fn render_footer(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let git_panel = self.git_panel.clone();
let editor = self.commit_editor.clone();
let editor_focus_handle = editor.focus_handle(cx);
let settings = ThemeSettings::get_global(cx);
let line_height = relative(settings.buffer_line_height.value())
.to_pixels(settings.buffer_font_size(cx).into(), window.rem_size());
let mut snapshot = self
.commit_editor
.update(cx, |editor, cx| editor.snapshot(window, cx));
let style = window.text_style().clone();
let font_id = window.text_system().resolve_font(&style.font());
let font_size = style.font_size.to_pixels(window.rem_size());
let line_height = style.line_height_in_pixels(window.rem_size());
let em_width = window.text_system().em_width(font_id, font_size);
let (branch, tooltip, commit_label, co_authors) =
self.git_panel.update(cx, |git_panel, cx| {
let branch = git_panel
.active_repository
.as_ref()
.and_then(|repo| {
repo.read(cx)
.repository_entry
.branch()
.map(|b| b.name.clone())
})
.and_then(|repo| repo.read(cx).current_branch().map(|b| b.name.clone()))
.unwrap_or_else(|| "<no branch>".into());
let tooltip = if git_panel.has_staged_changes() {
"Commit staged changes"
@@ -266,13 +299,13 @@ impl CommitModal {
let title = if git_panel.has_staged_changes() {
"Commit"
} else {
"Commit All"
"Commit Tracked"
};
let co_authors = git_panel.render_co_authors(cx);
(branch, tooltip, title, co_authors)
});
let branch_picker_button = panel_button(branch)
let branch_selector = panel_button(branch)
.icon(IconName::GitBranch)
.icon_size(IconSize::Small)
.icon_color(Color::Placeholder)
@@ -287,12 +320,7 @@ impl CommitModal {
}))
.style(ButtonStyle::Transparent);
let branch_picker = PopoverButton::new(
self.branch_list.clone(),
Corner::BottomLeft,
branch_picker_button,
Tooltip::for_action_title("Switch Branch", &zed_actions::git::Branch),
);
let changes_count = self.git_panel.read(cx).total_staged_count();
let close_kb_hint =
if let Some(close_kb) = ui::KeyBinding::for_action(&menu::Cancel, window, cx) {
@@ -304,49 +332,151 @@ impl CommitModal {
None
};
let (panel_editor_focus_handle, can_commit) = git_panel.update(cx, |git_panel, cx| {
(git_panel.editor_focus_handle(cx), git_panel.can_commit())
let fake_commit_kb =
ui::KeyBinding::new(gpui::KeyBinding::new("cmd-enter", gpui::NoAction, None), cx);
let commit_hint =
KeybindingHint::new(fake_commit_kb, cx.theme().colors().editor_background)
.suffix(commit_label);
let focus_handle = self.focus_handle(cx);
// let next_suggestion_kb =
// ui::KeyBinding::for_action_in(&NextSuggestion, &focus_handle.clone(), window, cx);
// let next_suggestion_hint = next_suggestion_kb.map(|kb| {
// KeybindingHint::new(kb, cx.theme().colors().editor_background).suffix("Next Suggestion")
// });
// let prev_suggestion_kb =
// ui::KeyBinding::for_action_in(&PrevSuggestion, &focus_handle.clone(), window, cx);
// let prev_suggestion_hint = prev_suggestion_kb.map(|kb| {
// KeybindingHint::new(kb, cx.theme().colors().editor_background)
// .suffix("Previous Suggestion")
// });
v_flex()
.id("editor-container")
.bg(cx.theme().colors().editor_background)
.flex_1()
.size_full()
.rounded(px(border_radius))
.overflow_hidden()
.border_1()
.border_color(cx.theme().colors().border_variant)
.py_2()
.px_3()
.on_click(cx.listener(move |_, _: &ClickEvent, window, _cx| {
window.focus(&editor_focus_handle);
}))
.child(
div()
.size_full()
.flex_1()
.child(self.commit_editor_element(window, cx)),
)
.child(
h_flex()
.group("commit_editor_footer")
.flex_none()
.w_full()
.items_center()
.justify_between()
.w_full()
.pt_2()
.pb_0p5()
.gap_1()
.child(h_flex().gap_1().child(branch_selector).children(co_authors))
.child(div().flex_1())
.child(
h_flex()
.opacity(0.7)
.group_hover("commit_editor_footer", |this| this.opacity(1.0))
.items_center()
.justify_end()
.flex_none()
.px_1()
.gap_4()
.children(close_kb_hint)
// .children(next_suggestion_hint)
.child(commit_hint),
),
)
}
pub fn render_footer(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let (branch, tooltip, title, co_authors) = self.git_panel.update(cx, |git_panel, cx| {
let branch = git_panel
.active_repository
.as_ref()
.and_then(|repo| {
repo.read(cx)
.repository_entry
.branch()
.map(|b| b.name.clone())
})
.unwrap_or_else(|| "<no branch>".into());
let tooltip = if git_panel.has_staged_changes() {
"Commit staged changes"
} else {
"Commit changes to tracked files"
};
let title = if git_panel.has_staged_changes() {
"Commit"
} else {
"Commit All"
};
let co_authors = git_panel.render_co_authors(cx);
(branch, tooltip, title, co_authors)
});
let commit_button = panel_filled_button(commit_label)
.tooltip(move |window, cx| {
Tooltip::for_action_in(tooltip, &Commit, &panel_editor_focus_handle, window, cx)
})
.disabled(!can_commit)
.on_click(cx.listener(move |this, _: &ClickEvent, window, cx| {
this.git_panel
.update(cx, |git_panel, cx| git_panel.commit_changes(window, cx));
cx.emit(DismissEvent);
}));
let branch_selector = panel_button(branch)
.icon(IconName::GitBranch)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.tooltip(Tooltip::for_action_title(
"Switch Branch",
&zed_actions::git::Branch,
))
.on_click(cx.listener(|_, _, window, cx| {
window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx);
}))
.style(ButtonStyle::Transparent);
let changes_count = self.git_panel.read(cx).total_staged_count();
let close_kb_hint =
if let Some(close_kb) = ui::KeyBinding::for_action(&menu::Cancel, window, cx) {
Some(
KeybindingHint::new(close_kb, cx.theme().colors().editor_background)
.suffix("Cancel"),
)
} else {
None
};
h_flex()
.group("commit_editor_footer")
.flex_none()
.w_full()
.items_center()
.justify_between()
.h(px(36.0))
.w_full()
.h(px(self.properties.footer_height))
.gap_1()
.justify_between()
.px_3()
.child(h_flex().child(branch_selector))
.child(
h_flex()
.gap_1()
.child(branch_picker.render(window, cx))
.children(co_authors),
)
.child(div().flex_1())
.child(
h_flex()
.items_center()
.justify_end()
.flex_none()
.px_1()
.gap_4()
.children(close_kb_hint)
.child(commit_button),
h_flex().gap_1p5().children(co_authors).child(
Button::new("stage-button", title)
.tooltip(Tooltip::for_action_title(tooltip, &git::Commit))
.on_click(cx.listener(|this, _, window, cx| {
this.commit(&Default::default(), window, cx);
})),
),
)
}
fn border_radius(&self) -> f32 {
8.0
}
fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
cx.emit(DismissEvent);
}
@@ -359,57 +489,33 @@ impl CommitModal {
impl Render for CommitModal {
fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
let properties = self.properties;
let width = px(properties.modal_width);
let container_padding = px(properties.container_padding);
let border_radius = properties.modal_border_radius;
let editor_focus_handle = self.commit_editor.focus_handle(cx);
let (width, _, border_radius) = self.container_properties(window, cx);
v_flex()
.id("commit-modal")
.key_context("GitCommit")
.on_action(cx.listener(Self::dismiss))
.on_action(cx.listener(Self::commit))
.on_action(
cx.listener(|this, _: &zed_actions::git::Branch, window, cx| {
this.branch_list.update(cx, |branch_list, cx| {
branch_list.menu_handle(window, cx).toggle(window, cx);
})
}),
)
.elevation_3(cx)
.overflow_hidden()
.flex_none()
.on_action(cx.listener(Self::dismiss))
.on_action(cx.listener(Self::commit))
// .on_action(cx.listener(Self::next_suggestion))
// .on_action(cx.listener(Self::prev_suggestion))
.relative()
.justify_between()
.bg(cx.theme().colors().elevated_surface_background)
.rounded(px(border_radius))
.border_1()
.border_color(cx.theme().colors().border)
.w(width)
.p(container_padding)
.w(px(width))
.h(px(360.))
.flex_1()
.overflow_hidden()
.child(
v_flex()
.id("editor-container")
.justify_between()
.flex_1()
.p_2()
.size_full()
.gap_2()
.rounded(properties.editor_border_radius())
.overflow_hidden()
.cursor_text()
.bg(cx.theme().colors().editor_background)
.border_1()
.border_color(cx.theme().colors().border_variant)
.on_click(cx.listener(move |_, _: &ClickEvent, window, _cx| {
window.focus(&editor_focus_handle);
}))
.child(
div()
.flex_1()
.size_full()
.child(self.render_commit_editor(window, cx)),
)
.child(self.render_footer(window, cx)),
.child(self.render_commit_editor(None, window, cx)),
)
// .child(self.render_footer(window, cx))
}
}

View File

@@ -5,6 +5,7 @@ use crate::{
git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
};
use crate::{picker_prompt, project_diff, ProjectDiff};
use collections::HashMap;
use db::kvp::KEY_VALUE_STORE;
use editor::commit_tooltip::CommitTooltip;
use editor::{
@@ -29,12 +30,12 @@ use settings::Settings as _;
use std::cell::RefCell;
use std::future::Future;
use std::rc::Rc;
use std::{collections::HashSet, sync::Arc, time::Duration, usize};
use std::{collections::HashSet, path::PathBuf, sync::Arc, time::Duration, usize};
use strum::{IntoEnumIterator, VariantNames};
use time::OffsetDateTime;
use ui::{
prelude::*, ButtonLike, Checkbox, ContextMenu, Divider, DividerColor, ElevationIndex, ListItem,
ListItemSpacing, PopoverMenu, Scrollbar, ScrollbarState, Tooltip,
ListItemSpacing, Scrollbar, ScrollbarState, Tooltip,
};
use util::{maybe, post_inc, ResultExt, TryFutureExt};
use workspace::{
@@ -80,6 +81,17 @@ pub fn init(cx: &mut App) {
workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
workspace.toggle_panel_focus::<GitPanel>(window, cx);
});
// workspace.register_action(|workspace, _: &Commit, window, cx| {
// workspace.open_panel::<GitPanel>(window, cx);
// if let Some(git_panel) = workspace.panel::<GitPanel>(cx) {
// git_panel
// .read(cx)
// .commit_editor
// .focus_handle(cx)
// .focus(window);
// }
// });
},
)
.detach();
@@ -143,6 +155,8 @@ impl GitListEntry {
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct GitStatusEntry {
pub(crate) depth: usize,
pub(crate) display_name: String,
pub(crate) repo_path: RepoPath,
pub(crate) status: FileStatus,
pub(crate) is_staged: Option<bool>,
@@ -170,12 +184,13 @@ pub struct GitPanel {
pending_remote_operations: RemoteOperations,
pub(crate) active_repository: Option<Entity<Repository>>,
commit_editor: Entity<Editor>,
pub(crate) suggested_commit_message: Option<String>,
suggested_commit_message: Option<String>,
conflicted_count: usize,
conflicted_staged_count: usize,
current_modifiers: Modifiers,
add_coauthors: bool,
entries: Vec<GitListEntry>,
entries_by_path: collections::HashMap<RepoPath, usize>,
focus_handle: FocusHandle,
fs: Arc<dyn Fs>,
hide_scrollbar_task: Option<Task<()>>,
@@ -212,7 +227,6 @@ impl Drop for RemoteOperationGuard {
pub(crate) fn commit_message_editor(
commit_message_buffer: Entity<Buffer>,
placeholder: Option<&str>,
project: Entity<Project>,
in_panel: bool,
window: &mut Window,
@@ -233,8 +247,7 @@ pub(crate) fn commit_message_editor(
commit_editor.set_show_gutter(false, cx);
commit_editor.set_show_wrap_guides(false, cx);
commit_editor.set_show_indent_guides(false, cx);
let placeholder = placeholder.unwrap_or("Enter commit message");
commit_editor.set_placeholder_text(placeholder, cx);
commit_editor.set_placeholder_text("Enter commit message", cx);
commit_editor
}
@@ -262,7 +275,7 @@ impl GitPanel {
// Once the active git repo is set, this buffer will be replaced.
let temporary_buffer = cx.new(|cx| Buffer::local("", cx));
let commit_editor = cx.new(|cx| {
commit_message_editor(temporary_buffer, None, project.clone(), true, window, cx)
commit_message_editor(temporary_buffer, project.clone(), true, window, cx)
});
commit_editor.update(cx, |editor, cx| {
editor.clear(window, cx);
@@ -302,6 +315,7 @@ impl GitPanel {
current_modifiers: window.modifiers(),
add_coauthors: true,
entries: Vec::new(),
entries_by_path: HashMap::default(),
focus_handle: cx.focus_handle(),
fs,
hide_scrollbar_task: None,
@@ -330,80 +344,6 @@ impl GitPanel {
})
}
pub fn entry_by_path(&self, path: &RepoPath) -> Option<usize> {
fn binary_search<F>(mut low: usize, mut high: usize, is_target: F) -> Option<usize>
where
F: Fn(usize) -> std::cmp::Ordering,
{
while low < high {
let mid = low + (high - low) / 2;
match is_target(mid) {
std::cmp::Ordering::Equal => return Some(mid),
std::cmp::Ordering::Less => low = mid + 1,
std::cmp::Ordering::Greater => high = mid,
}
}
None
}
if self.conflicted_count > 0 {
let conflicted_start = 1;
if let Some(ix) = binary_search(
conflicted_start,
conflicted_start + self.conflicted_count,
|ix| {
self.entries[ix]
.status_entry()
.unwrap()
.repo_path
.cmp(&path)
},
) {
return Some(ix);
}
}
if self.tracked_count > 0 {
let tracked_start = if self.conflicted_count > 0 {
1 + self.conflicted_count
} else {
0
} + 1;
if let Some(ix) =
binary_search(tracked_start, tracked_start + self.tracked_count, |ix| {
self.entries[ix]
.status_entry()
.unwrap()
.repo_path
.cmp(&path)
})
{
return Some(ix);
}
}
if self.new_count > 0 {
let untracked_start = if self.conflicted_count > 0 {
1 + self.conflicted_count
} else {
0
} + if self.tracked_count > 0 {
1 + self.tracked_count
} else {
0
} + 1;
if let Some(ix) =
binary_search(untracked_start, untracked_start + self.new_count, |ix| {
self.entries[ix]
.status_entry()
.unwrap()
.repo_path
.cmp(&path)
})
{
return Some(ix);
}
}
None
}
pub fn select_entry_by_path(
&mut self,
path: ProjectPath,
@@ -416,10 +356,10 @@ impl GitPanel {
let Some(repo_path) = git_repo.read(cx).project_path_to_repo_path(&path) else {
return;
};
let Some(ix) = self.entry_by_path(&repo_path) else {
let Some(ix) = self.entries_by_path.get(&repo_path) else {
return;
};
self.selected_entry = Some(ix);
self.selected_entry = Some(*ix);
cx.notify();
}
@@ -543,6 +483,31 @@ impl GitPanel {
cx.notify();
}
fn calculate_depth_and_difference(
repo_path: &RepoPath,
visible_entries: &HashSet<RepoPath>,
) -> (usize, usize) {
let ancestors = repo_path.ancestors().skip(1);
for ancestor in ancestors {
if let Some(parent_entry) = visible_entries.get(ancestor) {
let entry_component_count = repo_path.components().count();
let parent_component_count = parent_entry.components().count();
let difference = entry_component_count - parent_component_count;
let parent_depth = parent_entry
.ancestors()
.skip(1) // Skip the parent itself
.filter(|ancestor| visible_entries.contains(*ancestor))
.count();
return (parent_depth + 1, difference);
}
}
(0, 0)
}
fn scroll_to_selected_entry(&mut self, cx: &mut Context<Self>) {
if let Some(selected_entry) = self.selected_entry {
self.scroll_handle
@@ -553,7 +518,7 @@ impl GitPanel {
}
fn select_first(&mut self, _: &SelectFirst, _window: &mut Window, cx: &mut Context<Self>) {
if !self.entries.is_empty() {
if self.entries.first().is_some() {
self.selected_entry = Some(1);
self.scroll_to_selected_entry(cx);
}
@@ -623,10 +588,6 @@ impl GitPanel {
}
}
pub(crate) fn editor_focus_handle(&self, cx: &mut Context<Self>) -> FocusHandle {
self.commit_editor.focus_handle(cx).clone()
}
fn focus_editor(&mut self, _: &FocusEditor, window: &mut Window, cx: &mut Context<Self>) {
self.commit_editor.update(cx, |editor, cx| {
window.focus(&editor.focus_handle(cx));
@@ -695,7 +656,7 @@ impl GitPanel {
self.workspace
.update(cx, |workspace, cx| {
workspace
.open_path_preview(path, None, false, false, true, window, cx)
.open_path_preview(path, None, false, false, window, cx)
.detach_and_prompt_err("Failed to open file", window, cx, |e, _, _| {
Some(format!("{e}"))
});
@@ -842,7 +803,7 @@ impl GitPanel {
.detach();
}
fn restore_tracked_files(
fn discard_tracked_changes(
&mut self,
_: &RestoreTrackedFiles,
window: &mut Window,
@@ -860,20 +821,16 @@ impl GitPanel {
1 => return self.revert_entry(&entries[0], window, cx),
_ => {}
}
let mut details = entries
let details = entries
.iter()
.filter_map(|entry| entry.repo_path.0.file_name())
.map(|filename| filename.to_string_lossy())
.take(5)
.join("\n");
if entries.len() > 5 {
details.push_str(&format!("\nand {} more…", entries.len() - 5))
}
#[derive(strum::EnumIter, strum::VariantNames)]
#[strum(serialize_all = "title_case")]
enum RestoreCancel {
RestoreTrackedFiles,
enum DiscardCancel {
DiscardTrackedChanges,
Cancel,
}
let prompt = prompt(
@@ -884,7 +841,7 @@ impl GitPanel {
);
cx.spawn(|this, mut cx| async move {
match prompt.await {
Ok(RestoreCancel::RestoreTrackedFiles) => {
Ok(DiscardCancel::DiscardTrackedChanges) => {
this.update(&mut cx, |this, cx| {
let repo_paths = entries.into_iter().map(|entry| entry.repo_path).collect();
this.perform_checkout(repo_paths, cx);
@@ -918,7 +875,7 @@ impl GitPanel {
_ => {}
};
let mut details = to_delete
let details = to_delete
.iter()
.map(|entry| {
entry
@@ -928,13 +885,8 @@ impl GitPanel {
.map(|f| f.to_string_lossy())
.unwrap_or_default()
})
.take(5)
.join("\n");
if to_delete.len() > 5 {
details.push_str(&format!("\nand {} more…", to_delete.len() - 5))
}
let prompt = prompt("Trash these files?", Some(&details), window, cx);
cx.spawn_in(window, |this, mut cx| async move {
match prompt.await? {
@@ -1120,9 +1072,8 @@ impl GitPanel {
.contains_focused(window, cx)
{
self.commit_changes(window, cx)
} else {
cx.propagate();
}
cx.propagate();
}
pub(crate) fn commit_changes(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -1257,50 +1208,51 @@ impl GitPanel {
/// Suggests a commit message based on the changed files and their statuses
pub fn suggest_commit_message(&self) -> Option<String> {
if self.total_staged_count() != 1 {
return None;
}
let entry = self
let entries = self
.entries
.iter()
.find(|entry| match entry.status_entry() {
Some(entry) => entry.is_staged.unwrap_or(false),
_ => false,
})?;
.filter_map(|entry| {
if let GitListEntry::GitStatusEntry(status_entry) = entry {
Some(status_entry)
} else {
None
}
})
.collect::<Vec<&GitStatusEntry>>();
let GitListEntry::GitStatusEntry(git_status_entry) = entry.clone() else {
return None;
};
if entries.is_empty() {
None
} else if entries.len() == 1 {
let entry = &entries[0];
let file_name = entry
.repo_path
.file_name()
.unwrap_or_default()
.to_string_lossy();
let action_text = if git_status_entry.status.is_deleted() {
Some("Delete")
} else if git_status_entry.status.is_created() {
Some("Create")
} else if git_status_entry.status.is_modified() {
Some("Update")
if entry.status.is_deleted() {
Some(format!("Delete {}", file_name))
} else if entry.status.is_created() {
Some(format!("Create {}", file_name))
} else if entry.status.is_modified() {
Some(format!("Update {}", file_name))
} else {
None
}
} else {
None
};
let file_name = git_status_entry
.repo_path
.file_name()
.unwrap_or_default()
.to_string_lossy();
Some(format!("{} {}", action_text?, file_name))
}
}
fn update_editor_placeholder(&mut self, cx: &mut Context<Self>) {
let suggested_commit_message = self.suggest_commit_message();
let suggested_commit_message = suggested_commit_message
.as_deref()
.unwrap_or("Enter commit message");
self.suggested_commit_message = suggested_commit_message.clone();
self.commit_editor.update(cx, |editor, cx| {
editor.set_placeholder_text(Arc::from(suggested_commit_message), cx)
});
if let Some(suggested_commit_message) = suggested_commit_message {
self.commit_editor.update(cx, |editor, cx| {
editor.set_placeholder_text(Arc::from(suggested_commit_message), cx)
});
}
cx.notify();
}
@@ -1387,14 +1339,14 @@ impl GitPanel {
};
let mut current_remotes: Vec<Remote> = repo
.update(&mut cx, |repo, _| {
.update(&mut cx, |repo, cx| {
let Some(current_branch) = repo.current_branch() else {
return Err(anyhow::anyhow!("No active branch"));
};
Ok(repo.get_remotes(Some(current_branch.name.to_string())))
Ok(repo.get_remotes(Some(current_branch.name.to_string()), cx))
})??
.await??;
.await?;
if current_remotes.len() == 0 {
return Err(anyhow::anyhow!("No active remote"));
@@ -1582,14 +1534,7 @@ impl GitPanel {
!= Some(&buffer)
{
git_panel.commit_editor = cx.new(|cx| {
commit_message_editor(
buffer,
git_panel.suggested_commit_message.as_deref(),
git_panel.project.clone(),
true,
window,
cx,
)
commit_message_editor(buffer, git_panel.project.clone(), true, window, cx)
});
}
})
@@ -1603,6 +1548,7 @@ impl GitPanel {
fn update_visible_entries(&mut self, cx: &mut Context<Self>) {
self.entries.clear();
self.entries_by_path.clear();
let mut changed_entries = Vec::new();
let mut new_entries = Vec::new();
let mut conflict_entries = Vec::new();
@@ -1615,9 +1561,13 @@ impl GitPanel {
// First pass - collect all paths
let repo = repo.read(cx);
let path_set = HashSet::from_iter(repo.status().map(|entry| entry.repo_path));
// Second pass - create entries with proper depth calculation
for entry in repo.status() {
let (depth, difference) =
Self::calculate_depth_and_difference(&entry.repo_path, &path_set);
let is_conflict = repo.has_conflict(&entry.repo_path);
let is_new = entry.status.is_created();
let is_staged = entry.status.is_staged();
@@ -1630,7 +1580,28 @@ impl GitPanel {
continue;
}
let display_name = if difference > 1 {
// Show partial path for deeply nested files
entry
.repo_path
.as_ref()
.iter()
.skip(entry.repo_path.components().count() - difference)
.collect::<PathBuf>()
.to_string_lossy()
.into_owned()
} else {
// Just show filename
entry
.repo_path
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_default()
};
let entry = GitStatusEntry {
depth,
display_name,
repo_path: entry.repo_path.clone(),
status: entry.status,
is_staged,
@@ -1645,6 +1616,11 @@ impl GitPanel {
}
}
// Sort entries by path to maintain consistent order
conflict_entries.sort_by(|a, b| a.repo_path.cmp(&b.repo_path));
changed_entries.sort_by(|a, b| a.repo_path.cmp(&b.repo_path));
new_entries.sort_by(|a, b| a.repo_path.cmp(&b.repo_path));
if conflict_entries.len() > 0 {
self.entries.push(GitListEntry::Header(GitHeaderEntry {
header: Section::Conflict,
@@ -1674,6 +1650,12 @@ impl GitPanel {
.extend(new_entries.into_iter().map(GitListEntry::GitStatusEntry));
}
for (ix, entry) in self.entries.iter().enumerate() {
if let Some(status_entry) = entry.status_entry() {
self.entries_by_path
.insert(status_entry.repo_path.clone(), ix);
}
}
self.update_counts(repo);
self.select_first_entry_if_none(cx);
@@ -1851,8 +1833,7 @@ impl GitPanel {
cx.dispatch_action(&Diff);
})
}),
)
.child(self.render_overflow_menu()),
),
),
)
} else {
@@ -1874,13 +1855,6 @@ impl GitPanel {
})
}
pub fn render_overflow_menu(&self) -> impl IntoElement {
PopoverMenu::new("overflow-menu")
.trigger(IconButton::new("overflow-menu-trigger", IconName::Ellipsis))
.menu(move |window, cx| Some(Self::panel_context_menu(window, cx)))
.anchor(Corner::TopRight)
}
pub fn render_sync_button(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
let active_repository = self.project.read(cx).active_repository(cx);
active_repository.as_ref().map(|_| {
@@ -2237,8 +2211,8 @@ impl GitPanel {
) -> Option<AnyElement> {
let repo = self.active_repository.as_ref()?.read(cx);
let repo_path = repo.worktree_id_path_to_repo_path(file.worktree_id(cx), file.path())?;
let ix = self.entry_by_path(&repo_path)?;
let entry = self.entries.get(ix)?;
let ix = self.entries_by_path.get(&repo_path)?;
let entry = self.entries.get(*ix)?;
let is_staged = self.entry_is_staged(entry.status_entry()?);
@@ -2365,9 +2339,7 @@ impl GitPanel {
let Some(repo) = self.active_repository.clone() else {
return Task::ready(Err(anyhow::anyhow!("no active repo")));
};
let show = repo.read(cx).show(sha);
cx.spawn(|_, _| async move { show.await? })
repo.update(cx, |repo, cx| repo.show(sha, cx))
}
fn deploy_entry_context_menu(
@@ -2385,15 +2357,17 @@ impl GitPanel {
} else {
"Stage File"
};
let restore_title = if entry.status.is_created() {
"Trash File"
let revert_title = if entry.status.is_deleted() {
"Restore file"
} else if entry.status.is_created() {
"Trash file"
} else {
"Restore File"
"Discard changes"
};
let context_menu = ContextMenu::build(window, cx, |context_menu, _, _| {
context_menu
.action(stage_title, ToggleStaged.boxed_clone())
.action(restore_title, git::RestoreFile.boxed_clone())
.action(revert_title, git::RestoreFile.boxed_clone())
.separator()
.action("Open Diff", Confirm.boxed_clone())
.action("Open File", SecondaryConfirm.boxed_clone())
@@ -2402,26 +2376,21 @@ impl GitPanel {
self.set_context_menu(context_menu, position, window, cx);
}
fn panel_context_menu(window: &mut Window, cx: &mut App) -> Entity<ContextMenu> {
ContextMenu::build(window, cx, |context_menu, _, _| {
context_menu
.action("Stage All", StageAll.boxed_clone())
.action("Unstage All", UnstageAll.boxed_clone())
.separator()
.action("Open Diff", project_diff::Diff.boxed_clone())
.separator()
.action("Restore Tracked Files", RestoreTrackedFiles.boxed_clone())
.action("Trash Untracked Files", TrashUntrackedFiles.boxed_clone())
})
}
fn deploy_panel_context_menu(
&mut self,
position: Point<Pixels>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let context_menu = Self::panel_context_menu(window, cx);
let context_menu = ContextMenu::build(window, cx, |context_menu, _, _| {
context_menu
.action("Stage All", StageAll.boxed_clone())
.action("Unstage All", UnstageAll.boxed_clone())
.action("Open Diff", project_diff::Diff.boxed_clone())
.separator()
.action("Discard Tracked Changes", RestoreTrackedFiles.boxed_clone())
.action("Trash Untracked Files", TrashUntrackedFiles.boxed_clone())
});
self.set_context_menu(context_menu, position, window, cx);
}
@@ -2494,7 +2463,6 @@ impl GitPanel {
let id: ElementId = ElementId::Name(format!("entry_{}", display_name).into());
let is_entry_staged = self.entry_is_staged(entry);
let mut is_staged: ToggleState = self.entry_is_staged(entry).into();
if !self.has_staged_changes() && !self.has_conflicts() && !entry.status.is_created() {
@@ -2521,15 +2489,8 @@ impl GitPanel {
let start_slot = h_flex()
.id(("start-slot", ix))
.gap(DynamicSpacing::Base04.rems(cx))
.child(checkbox.tooltip(move |window, cx| {
let tooltip_name = if is_entry_staged.unwrap_or(false) {
"Unstage"
} else {
"Stage"
};
Tooltip::for_action(tooltip_name, &ToggleStaged, window, cx)
}))
.child(checkbox)
.tooltip(|window, cx| Tooltip::for_action("Stage File", &ToggleStaged, window, cx))
.child(git_status_icon(status, cx))
.on_mouse_down(MouseButton::Left, |_, _, cx| {
// prevent the list item active state triggering when toggling checkbox
@@ -2694,7 +2655,7 @@ impl Render for GitPanel {
.on_action(cx.listener(Self::toggle_staged_for_selected))
.on_action(cx.listener(Self::stage_all))
.on_action(cx.listener(Self::unstage_all))
.on_action(cx.listener(Self::restore_tracked_files))
.on_action(cx.listener(Self::discard_tracked_changes))
.on_action(cx.listener(Self::clean_all))
.on_action(cx.listener(Self::fetch))
.on_action(cx.listener(Self::pull))

Some files were not shown because too many files have changed in this diff Show More