Compare commits
178 Commits
gpui-selec
...
edit-syste
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e72b64547 | ||
|
|
45f12b9426 | ||
|
|
4f9ba28a25 | ||
|
|
0c2d71f1ac | ||
|
|
901cb8b3d2 | ||
|
|
9cef0ac869 | ||
|
|
79b5556267 | ||
|
|
dd67bda595 | ||
|
|
4762e52d31 | ||
|
|
50c45c7897 | ||
|
|
27ed0f4273 | ||
|
|
a3e75540af | ||
|
|
aa5113cd92 | ||
|
|
bca639bda9 | ||
|
|
bff1d8b142 | ||
|
|
95e246ac1c | ||
|
|
ba25e371be | ||
|
|
c73ef1a5f3 | ||
|
|
8b5a0cff10 | ||
|
|
f0af508ae5 | ||
|
|
5fe4070501 | ||
|
|
981a143e9b | ||
|
|
5e06ce4df3 | ||
|
|
3bd53d0441 | ||
|
|
76535578e9 | ||
|
|
fdcedf15b7 | ||
|
|
bd6d385817 | ||
|
|
5df1481297 | ||
|
|
ddaaaee973 | ||
|
|
9772b7ac33 | ||
|
|
2e0811e113 | ||
|
|
1b292d2fb3 | ||
|
|
adecbd1815 | ||
|
|
a7aa2578e1 | ||
|
|
24ffa0fcf3 | ||
|
|
b0494d1c05 | ||
|
|
a89dc8c42e | ||
|
|
d103903229 | ||
|
|
ec3aabe2c2 | ||
|
|
4b98c35d68 | ||
|
|
5103995c32 | ||
|
|
fb4c6dbaa7 | ||
|
|
91c1716858 | ||
|
|
0933426e63 | ||
|
|
689e4aef2f | ||
|
|
dbebb40956 | ||
|
|
d2cec0221b | ||
|
|
724acaab61 | ||
|
|
ffa2d90dc3 | ||
|
|
5c2ec1705d | ||
|
|
2671b9c63c | ||
|
|
68fe2bb776 | ||
|
|
65f7238777 | ||
|
|
ca680f07f7 | ||
|
|
9d681bda8d | ||
|
|
1669ff80df | ||
|
|
45aca348b8 | ||
|
|
07942bbdfe | ||
|
|
47c12c6563 | ||
|
|
63a5f46df4 | ||
|
|
6ddcff25e3 | ||
|
|
1cf40d77e2 | ||
|
|
33a72219c0 | ||
|
|
c77dd8b9e0 | ||
|
|
3d9f0087ff | ||
|
|
6a64b732b6 | ||
|
|
47ca343803 | ||
|
|
768b63a497 | ||
|
|
0d2f65ac13 | ||
|
|
953acb0f6d | ||
|
|
d64106e01b | ||
|
|
c260f7d5ac | ||
|
|
b038fb3729 | ||
|
|
4eedbdedae | ||
|
|
33d4c563fb | ||
|
|
e2907983d1 | ||
|
|
ceab446409 | ||
|
|
72c47b7f01 | ||
|
|
c77d2eb73f | ||
|
|
fc4ea55d3c | ||
|
|
bcf7bc9de8 | ||
|
|
5a7b8f7fe3 | ||
|
|
0c11d841e8 | ||
|
|
4eca7875ae | ||
|
|
843d299d9a | ||
|
|
88c4e0b2d8 | ||
|
|
a64e20ed96 | ||
|
|
f2a415135b | ||
|
|
96a3021b12 | ||
|
|
d6a6330419 | ||
|
|
da6a6ec36b | ||
|
|
f3fffc25c4 | ||
|
|
2e6d044bac | ||
|
|
530bc5c99e | ||
|
|
9edd81c740 | ||
|
|
11bc28080f | ||
|
|
fd3831861b | ||
|
|
68a0035264 | ||
|
|
9a60c0a059 | ||
|
|
5486c3dc93 | ||
|
|
3018a64a1b | ||
|
|
8633909347 | ||
|
|
091e7cb395 | ||
|
|
bb1817ff31 | ||
|
|
8871fec2a8 | ||
|
|
32b59bfa0e | ||
|
|
f658af5903 | ||
|
|
f99b24acca | ||
|
|
e4f13dd561 | ||
|
|
056c785f4e | ||
|
|
970a5957cc | ||
|
|
b25eb9afe2 | ||
|
|
0aab6d8bdc | ||
|
|
8caca6db29 | ||
|
|
d0c95c2f43 | ||
|
|
910963e5f3 | ||
|
|
237cc9b4a9 | ||
|
|
38a50a042a | ||
|
|
01aa7688c5 | ||
|
|
d4636481ac | ||
|
|
29c675ba17 | ||
|
|
bc5f82d40c | ||
|
|
80733e919d | ||
|
|
27a9498cb0 | ||
|
|
593f0e0c3e | ||
|
|
6e2be283dd | ||
|
|
cf6c2daaa2 | ||
|
|
283d424485 | ||
|
|
c68b700312 | ||
|
|
08c9157a1e | ||
|
|
1e84f01041 | ||
|
|
5a71d8c7f1 | ||
|
|
14c7782ce6 | ||
|
|
1a9b0536a2 | ||
|
|
9ec0927701 | ||
|
|
89039f6f34 | ||
|
|
6964302d89 | ||
|
|
335c307b93 | ||
|
|
02a859fb08 | ||
|
|
f576bd3aaf | ||
|
|
15299dcf80 | ||
|
|
75a545308d | ||
|
|
d62943930b | ||
|
|
0969f314b9 | ||
|
|
8d390f986d | ||
|
|
b2582a7b1b | ||
|
|
a497c49fb8 | ||
|
|
3e5dcd1bec | ||
|
|
6bdcfad6ad | ||
|
|
86696d88cf | ||
|
|
dccf6dae01 | ||
|
|
6563330239 | ||
|
|
610968815c | ||
|
|
ff56ca7280 | ||
|
|
899f7113ba | ||
|
|
beee79a9e7 | ||
|
|
b3d969ef3c | ||
|
|
55555bb41f | ||
|
|
f5e155b5a9 | ||
|
|
36055505cd | ||
|
|
9348e6f7fb | ||
|
|
f987ff05fd | ||
|
|
2306e3cd50 | ||
|
|
f252d9cf67 | ||
|
|
5ce45908b1 | ||
|
|
cd03e473c8 | ||
|
|
e69e25c171 | ||
|
|
61a60d37a2 | ||
|
|
4024b9ac4d | ||
|
|
b523ee6980 | ||
|
|
5f0046b923 | ||
|
|
155a80c6a5 | ||
|
|
7bc1025d91 | ||
|
|
b58bf64f0a | ||
|
|
47b38a0428 | ||
|
|
1915a756a0 | ||
|
|
43ad470e58 | ||
|
|
1abd58070b |
4
.github/pull_request_template.md
vendored
@@ -6,6 +6,8 @@ Release Notes:
|
||||
|
||||
Optionally, include screenshots / media showcasing your addition that can be included in the release notes.
|
||||
|
||||
**or**
|
||||
### Or...
|
||||
|
||||
Release Notes:
|
||||
|
||||
- N/A
|
||||
|
||||
35
.github/workflows/ci.yml
vendored
@@ -104,23 +104,19 @@ jobs:
|
||||
# todo(linux): Actually run the tests
|
||||
linux_tests:
|
||||
name: (Linux) Run Clippy and tests
|
||||
runs-on: ubuntu-latest
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- deploy
|
||||
steps:
|
||||
- name: Add Rust to the PATH
|
||||
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: configure linux
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: script/linux
|
||||
|
||||
- name: cargo clippy
|
||||
run: cargo xtask clippy
|
||||
|
||||
@@ -130,7 +126,7 @@ jobs:
|
||||
# todo(windows): Actually run the tests
|
||||
windows_tests:
|
||||
name: (Windows) Run Clippy and tests
|
||||
runs-on: windows-latest
|
||||
runs-on: hosted-windows-1
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
@@ -262,26 +258,25 @@ jobs:
|
||||
|
||||
bundle-linux:
|
||||
name: Create a Linux bundle
|
||||
runs-on: ubuntu-22.04 # keep the version fixed to avoid libc and dynamic linked library issues
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- deploy
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
needs: [linux_tests]
|
||||
env:
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
steps:
|
||||
- name: Add Rust to the PATH
|
||||
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Configure linux
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: script/linux
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 100
|
||||
|
||||
- name: Determine version and release channel
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
|
||||
35
.github/workflows/deploy_docs.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: Deploy Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy-docs:
|
||||
name: Deploy Docs
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
- name: Setup mdBook
|
||||
uses: peaceiris/actions-mdbook@v2
|
||||
with:
|
||||
mdbook-version: "0.4.37"
|
||||
|
||||
- name: Build book
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p target/deploy
|
||||
mdbook build ./docs --dest-dir=../target/deploy/docs/
|
||||
|
||||
- name: Deploy
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: pages deploy target/deploy --project-name=docs
|
||||
14
.github/workflows/release_nightly.yml
vendored
@@ -96,7 +96,9 @@ jobs:
|
||||
bundle-deb:
|
||||
name: Create a Linux *.tar.gz bundle
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-22.04 # keep the version fixed to avoid libc and dynamic linked library issues
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- deploy
|
||||
needs: tests
|
||||
env:
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
@@ -109,14 +111,8 @@ jobs:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Configure linux
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: script/linux
|
||||
- name: Add Rust to the PATH
|
||||
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Set release channel to nightly
|
||||
run: |
|
||||
|
||||
9
.mailmap
@@ -15,8 +15,12 @@ Christian Bergschneider <christian.bergschneider@gmx.de>
|
||||
Christian Bergschneider <christian.bergschneider@gmx.de> <magiclake@gmx.de>
|
||||
Conrad Irwin <conrad@zed.dev>
|
||||
Conrad Irwin <conrad@zed.dev> <conrad.irwin@gmail.com>
|
||||
Fernando Tagawa <tagawafernando@gmail.com>
|
||||
Fernando Tagawa <tagawafernando@gmail.com> <fernando.tagawa.gamail.com@gmail.com>
|
||||
Greg Morenz <greg-morenz@droid.cafe>
|
||||
Greg Morenz <greg-morenz@droid.cafe> <morenzg@gmail.com>
|
||||
Ivan Žužak <izuzak@gmail.com>
|
||||
Ivan Žužak <izuzak@gmail.com> <ivan.zuzak@github.com>
|
||||
Joseph T. Lyons <JosephTLyons@gmail.com>
|
||||
Joseph T. Lyons <JosephTLyons@gmail.com> <JosephTLyons@users.noreply.github.com>
|
||||
Julia <floc@unpromptedtirade.com>
|
||||
@@ -29,6 +33,9 @@ Kirill Bulatov <kirill@zed.dev>
|
||||
Kirill Bulatov <kirill@zed.dev> <mail4score@gmail.com>
|
||||
Kyle Caverly <kylebcaverly@gmail.com>
|
||||
Kyle Caverly <kylebcaverly@gmail.com> <kyle@zed.dev>
|
||||
LoganDark <contact@logandark.mozmail.com>
|
||||
LoganDark <contact@logandark.mozmail.com> <git@logandark.mozmail.com>
|
||||
LoganDark <contact@logandark.mozmail.com> <github@logandark.mozmail.com>
|
||||
Marshall Bowers <elliott.codes@gmail.com>
|
||||
Marshall Bowers <elliott.codes@gmail.com> <marshall@zed.dev>
|
||||
Max Brunsfeld <maxbrunsfeld@gmail.com>
|
||||
@@ -41,6 +48,8 @@ Nate Butler <iamnbutler@gmail.com> <nate@zed.dev>
|
||||
Nathan Sobo <nathan@zed.dev>
|
||||
Nathan Sobo <nathan@zed.dev> <nathan@warp.dev>
|
||||
Nathan Sobo <nathan@zed.dev> <nathansobo@gmail.com>
|
||||
Petros Amoiridis <petros@hey.com>
|
||||
Petros Amoiridis <petros@hey.com> <petros@zed.dev>
|
||||
Piotr Osiewicz <piotr@zed.dev>
|
||||
Piotr Osiewicz <piotr@zed.dev> <24362066+osiewicz@users.noreply.github.com>
|
||||
Robert Clover <git@clo4.net>
|
||||
|
||||
294
Cargo.lock
generated
@@ -377,6 +377,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"assets",
|
||||
"assistant_tooling",
|
||||
"chrono",
|
||||
"client",
|
||||
"collections",
|
||||
"editor",
|
||||
@@ -384,15 +385,17 @@ dependencies = [
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"language",
|
||||
"languages",
|
||||
"log",
|
||||
"nanoid",
|
||||
"node_runtime",
|
||||
"open_ai",
|
||||
"picker",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"rich_text",
|
||||
"schemars",
|
||||
@@ -412,10 +415,20 @@ name = "assistant_tooling"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collections",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"log",
|
||||
"project",
|
||||
"repair_json",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"sum_tree",
|
||||
"ui",
|
||||
"unindent",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -473,6 +486,7 @@ version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a116f46a969224200a0a97f29cfd4c50e7534e4b4826bd23ea2c3c533039c82c"
|
||||
dependencies = [
|
||||
"deflate64",
|
||||
"flate2",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
@@ -806,6 +820,19 @@ dependencies = [
|
||||
"tungstenite 0.16.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async_zip"
|
||||
version = "0.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b9f7252833d5ed4b00aa9604b563529dd5e11de9c23615de2dcdf91eb87b52"
|
||||
dependencies = [
|
||||
"async-compression",
|
||||
"crc32fast",
|
||||
"futures-lite 2.2.0",
|
||||
"pin-project",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atoi"
|
||||
version = "2.0.0"
|
||||
@@ -1481,7 +1508,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "blade-graphics"
|
||||
version = "0.4.0"
|
||||
source = "git+https://github.com/kvark/blade?rev=e82eec97691c3acdb43494484be60d661edfebf3#e82eec97691c3acdb43494484be60d661edfebf3"
|
||||
source = "git+https://github.com/kvark/blade?rev=e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c#e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c"
|
||||
dependencies = [
|
||||
"ash",
|
||||
"ash-window",
|
||||
@@ -1511,7 +1538,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "blade-macros"
|
||||
version = "0.2.1"
|
||||
source = "git+https://github.com/kvark/blade?rev=e82eec97691c3acdb43494484be60d661edfebf3#e82eec97691c3acdb43494484be60d661edfebf3"
|
||||
source = "git+https://github.com/kvark/blade?rev=e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c#e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2092,7 +2119,11 @@ dependencies = [
|
||||
"clap 4.4.4",
|
||||
"core-foundation",
|
||||
"core-services",
|
||||
"exec",
|
||||
"fork",
|
||||
"ipc-channel",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"plist",
|
||||
"release_channel",
|
||||
"serde",
|
||||
@@ -2280,6 +2311,7 @@ dependencies = [
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"git",
|
||||
"git_hosting_providers",
|
||||
"google_ai",
|
||||
"gpui",
|
||||
"headless",
|
||||
@@ -2316,6 +2348,7 @@ dependencies = [
|
||||
"sha2 0.10.7",
|
||||
"sqlx",
|
||||
"subtle",
|
||||
"supermaven_api",
|
||||
"telemetry_events",
|
||||
"text",
|
||||
"theme",
|
||||
@@ -2360,6 +2393,7 @@ dependencies = [
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"recent_projects",
|
||||
"release_channel",
|
||||
"rich_text",
|
||||
"rpc",
|
||||
"schemars",
|
||||
@@ -2511,30 +2545,10 @@ dependencies = [
|
||||
"async-compression",
|
||||
"async-std",
|
||||
"async-tar",
|
||||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"language",
|
||||
"lsp",
|
||||
"node_runtime",
|
||||
"parking_lot",
|
||||
"rpc",
|
||||
"serde",
|
||||
"settings",
|
||||
"smol",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "copilot_ui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"copilot",
|
||||
"editor",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
@@ -2543,14 +2557,18 @@ dependencies = [
|
||||
"language",
|
||||
"lsp",
|
||||
"menu",
|
||||
"node_runtime",
|
||||
"parking_lot",
|
||||
"project",
|
||||
"rpc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smol",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
"zed_actions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3114,6 +3132,12 @@ dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deflate64"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83ace6c86376be0b6cdcf3fb41882e81d94b31587573d1cfa9d01cd06bba210d"
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.6.1"
|
||||
@@ -3409,6 +3433,7 @@ dependencies = [
|
||||
"smol",
|
||||
"snippet",
|
||||
"sum_tree",
|
||||
"task",
|
||||
"text",
|
||||
"theme",
|
||||
"time",
|
||||
@@ -3554,6 +3579,17 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
|
||||
dependencies = [
|
||||
"errno-dragonfly",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.8"
|
||||
@@ -3564,6 +3600,16 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno-dragonfly"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "etagere"
|
||||
version = "0.2.8"
|
||||
@@ -3642,6 +3688,16 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exec"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "886b70328cba8871bfc025858e1de4be16b1d5088f2ba50b57816f4210672615"
|
||||
dependencies = [
|
||||
"errno 0.2.8",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "extension"
|
||||
version = "0.1.0"
|
||||
@@ -4042,6 +4098,15 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
|
||||
|
||||
[[package]]
|
||||
name = "fork"
|
||||
version = "0.1.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60e74d3423998a57e9d906e49252fb79eb4a04d5cdfe188fb1b7ff9fc076a8ed"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.1"
|
||||
@@ -4398,14 +4463,16 @@ name = "git"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"clock",
|
||||
"collections",
|
||||
"derive_more",
|
||||
"git2",
|
||||
"gpui",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"parking_lot",
|
||||
"pretty_assertions",
|
||||
"regex",
|
||||
"rope",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4432,6 +4499,25 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "git_hosting_providers"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"futures 0.3.28",
|
||||
"git",
|
||||
"gpui",
|
||||
"isahc",
|
||||
"pretty_assertions",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"unindent",
|
||||
"url",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.1"
|
||||
@@ -4618,6 +4704,7 @@ dependencies = [
|
||||
"wayland-client",
|
||||
"wayland-cursor",
|
||||
"wayland-protocols",
|
||||
"wayland-protocols-plasma",
|
||||
"windows 0.53.0",
|
||||
"x11rb",
|
||||
"xkbcommon",
|
||||
@@ -4745,7 +4832,6 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"ctrlc",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
@@ -4757,6 +4843,7 @@ dependencies = [
|
||||
"rpc",
|
||||
"settings",
|
||||
"shellexpand",
|
||||
"signal-hook",
|
||||
"util",
|
||||
]
|
||||
|
||||
@@ -5142,6 +5229,30 @@ dependencies = [
|
||||
"syn 2.0.59",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inline_completion_button"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"copilot",
|
||||
"editor",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"language",
|
||||
"lsp",
|
||||
"project",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"supermaven",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
"zed_actions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.9.6"
|
||||
@@ -5547,6 +5658,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"collections",
|
||||
"copilot",
|
||||
"editor",
|
||||
"env_logger",
|
||||
"futures 0.3.28",
|
||||
@@ -5929,6 +6041,27 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assets",
|
||||
"env_logger",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"language",
|
||||
"languages",
|
||||
"linkify",
|
||||
"log",
|
||||
"node_runtime",
|
||||
"pulldown-cmark",
|
||||
"settings",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown_preview"
|
||||
version = "0.1.0"
|
||||
@@ -6295,15 +6428,20 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-compression",
|
||||
"async-std",
|
||||
"async-tar",
|
||||
"async-trait",
|
||||
"async_zip",
|
||||
"futures 0.3.28",
|
||||
"log",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smol",
|
||||
"tempfile",
|
||||
"util",
|
||||
"walkdir",
|
||||
"windows 0.53.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7421,7 +7559,6 @@ dependencies = [
|
||||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
"copilot",
|
||||
"env_logger",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
@@ -7958,6 +8095,15 @@ dependencies = [
|
||||
"bytecheck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "repair_json"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ee191e184125fe72cb59b74160e25584e3908f2aaa84cbda1e161347102aa15"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.11.20"
|
||||
@@ -8298,7 +8444,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"errno",
|
||||
"errno 0.3.8",
|
||||
"io-lifetimes 1.0.11",
|
||||
"libc",
|
||||
"linux-raw-sys 0.3.8",
|
||||
@@ -8312,7 +8458,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"errno",
|
||||
"errno 0.3.8",
|
||||
"itoa",
|
||||
"libc",
|
||||
"linux-raw-sys 0.4.12",
|
||||
@@ -8326,7 +8472,7 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a25c3aad9fc1424eb82c88087789a7d938e1829724f3e4043163baf0d13cfc12"
|
||||
dependencies = [
|
||||
"errno",
|
||||
"errno 0.3.8",
|
||||
"libc",
|
||||
"rustix 0.38.32",
|
||||
]
|
||||
@@ -8704,7 +8850,12 @@ dependencies = [
|
||||
"sha2 0.10.7",
|
||||
"smol",
|
||||
"tempfile",
|
||||
"theme",
|
||||
"tree-sitter",
|
||||
"ui",
|
||||
"unindent",
|
||||
"util",
|
||||
"workspace",
|
||||
"worktree",
|
||||
]
|
||||
|
||||
@@ -9591,6 +9742,43 @@ dependencies = [
|
||||
"rayon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "supermaven"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"collections",
|
||||
"editor",
|
||||
"env_logger",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"language",
|
||||
"log",
|
||||
"postage",
|
||||
"project",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smol",
|
||||
"supermaven_api",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "supermaven_api"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures 0.3.28",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smol",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sval"
|
||||
version = "2.8.0"
|
||||
@@ -9824,6 +10012,7 @@ dependencies = [
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"hex",
|
||||
"parking_lot",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json_lenient",
|
||||
@@ -9836,7 +10025,6 @@ dependencies = [
|
||||
name = "tasks_ui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"editor",
|
||||
"file_icons",
|
||||
"fuzzy",
|
||||
@@ -10051,18 +10239,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.48"
|
||||
version = "1.0.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7"
|
||||
checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.48"
|
||||
version = "1.0.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35"
|
||||
checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -11208,9 +11396,9 @@ checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.4.0"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
@@ -11728,6 +11916,19 @@ dependencies = [
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols-plasma"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols-wlr"
|
||||
version = "0.2.0"
|
||||
@@ -11795,12 +11996,12 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"copilot_ui",
|
||||
"db",
|
||||
"editor",
|
||||
"extensions_ui",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"inline_completion_button",
|
||||
"install_cli",
|
||||
"picker",
|
||||
"project",
|
||||
@@ -12658,7 +12859,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.135.0"
|
||||
version = "0.136.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
@@ -12675,12 +12876,10 @@ dependencies = [
|
||||
"clap 4.4.4",
|
||||
"cli",
|
||||
"client",
|
||||
"clock",
|
||||
"collab_ui",
|
||||
"collections",
|
||||
"command_palette",
|
||||
"copilot",
|
||||
"copilot_ui",
|
||||
"db",
|
||||
"dev_server_projects",
|
||||
"diagnostics",
|
||||
@@ -12693,10 +12892,13 @@ dependencies = [
|
||||
"file_icons",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"git",
|
||||
"git_hosting_providers",
|
||||
"go_to_line",
|
||||
"gpui",
|
||||
"headless",
|
||||
"image_viewer",
|
||||
"inline_completion_button",
|
||||
"install_cli",
|
||||
"isahc",
|
||||
"journal",
|
||||
@@ -12704,6 +12906,7 @@ dependencies = [
|
||||
"language_selector",
|
||||
"language_tools",
|
||||
"languages",
|
||||
"libc",
|
||||
"log",
|
||||
"markdown_preview",
|
||||
"menu",
|
||||
@@ -12727,6 +12930,7 @@ dependencies = [
|
||||
"settings",
|
||||
"simplelog",
|
||||
"smol",
|
||||
"supermaven",
|
||||
"tab_switcher",
|
||||
"task",
|
||||
"tasks_ui",
|
||||
@@ -12790,7 +12994,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_elixir"
|
||||
version = "0.0.2"
|
||||
version = "0.0.4"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
@@ -12924,7 +13128,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_toml"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
@@ -12945,7 +13149,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_zig"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
29
Cargo.toml
@@ -4,8 +4,8 @@ members = [
|
||||
"crates/anthropic",
|
||||
"crates/assets",
|
||||
"crates/assistant",
|
||||
"crates/assistant_tooling",
|
||||
"crates/assistant2",
|
||||
"crates/assistant_tooling",
|
||||
"crates/audio",
|
||||
"crates/auto_update",
|
||||
"crates/breadcrumbs",
|
||||
@@ -20,7 +20,6 @@ members = [
|
||||
"crates/command_palette",
|
||||
"crates/command_palette_hooks",
|
||||
"crates/copilot",
|
||||
"crates/copilot_ui",
|
||||
"crates/db",
|
||||
"crates/diagnostics",
|
||||
"crates/editor",
|
||||
@@ -36,12 +35,14 @@ members = [
|
||||
"crates/fsevent",
|
||||
"crates/fuzzy",
|
||||
"crates/git",
|
||||
"crates/git_hosting_providers",
|
||||
"crates/go_to_line",
|
||||
"crates/google_ai",
|
||||
"crates/gpui",
|
||||
"crates/gpui_macros",
|
||||
"crates/headless",
|
||||
"crates/image_viewer",
|
||||
"crates/inline_completion_button",
|
||||
"crates/install_cli",
|
||||
"crates/journal",
|
||||
"crates/language",
|
||||
@@ -51,6 +52,7 @@ members = [
|
||||
"crates/live_kit_client",
|
||||
"crates/live_kit_server",
|
||||
"crates/lsp",
|
||||
"crates/markdown",
|
||||
"crates/markdown_preview",
|
||||
"crates/media",
|
||||
"crates/menu",
|
||||
@@ -86,6 +88,8 @@ members = [
|
||||
"crates/storybook",
|
||||
"crates/sum_tree",
|
||||
"crates/tab_switcher",
|
||||
"crates/supermaven",
|
||||
"crates/supermaven_api",
|
||||
"crates/terminal",
|
||||
"crates/terminal_view",
|
||||
"crates/text",
|
||||
@@ -159,7 +163,6 @@ color = { path = "crates/color" }
|
||||
command_palette = { path = "crates/command_palette" }
|
||||
command_palette_hooks = { path = "crates/command_palette_hooks" }
|
||||
copilot = { path = "crates/copilot" }
|
||||
copilot_ui = { path = "crates/copilot_ui" }
|
||||
db = { path = "crates/db" }
|
||||
diagnostics = { path = "crates/diagnostics" }
|
||||
editor = { path = "crates/editor" }
|
||||
@@ -173,6 +176,7 @@ fs = { path = "crates/fs" }
|
||||
fsevent = { path = "crates/fsevent" }
|
||||
fuzzy = { path = "crates/fuzzy" }
|
||||
git = { path = "crates/git" }
|
||||
git_hosting_providers = { path = "crates/git_hosting_providers" }
|
||||
go_to_line = { path = "crates/go_to_line" }
|
||||
google_ai = { path = "crates/google_ai" }
|
||||
gpui = { path = "crates/gpui" }
|
||||
@@ -180,6 +184,7 @@ gpui_macros = { path = "crates/gpui_macros" }
|
||||
headless = { path = "crates/headless" }
|
||||
install_cli = { path = "crates/install_cli" }
|
||||
image_viewer = { path = "crates/image_viewer" }
|
||||
inline_completion_button = { path = "crates/inline_completion_button" }
|
||||
journal = { path = "crates/journal" }
|
||||
language = { path = "crates/language" }
|
||||
language_selector = { path = "crates/language_selector" }
|
||||
@@ -188,6 +193,7 @@ languages = { path = "crates/languages" }
|
||||
live_kit_client = { path = "crates/live_kit_client" }
|
||||
live_kit_server = { path = "crates/live_kit_server" }
|
||||
lsp = { path = "crates/lsp" }
|
||||
markdown = { path = "crates/markdown" }
|
||||
markdown_preview = { path = "crates/markdown_preview" }
|
||||
media = { path = "crates/media" }
|
||||
menu = { path = "crates/menu" }
|
||||
@@ -220,6 +226,8 @@ settings = { path = "crates/settings" }
|
||||
snippet = { path = "crates/snippet" }
|
||||
sqlez = { path = "crates/sqlez" }
|
||||
sqlez_macros = { path = "crates/sqlez_macros" }
|
||||
supermaven = { path = "crates/supermaven" }
|
||||
supermaven_api = { path = "crates/supermaven_api" }
|
||||
story = { path = "crates/story" }
|
||||
storybook = { path = "crates/storybook" }
|
||||
sum_tree = { path = "crates/sum_tree" }
|
||||
@@ -249,20 +257,23 @@ async-fs = "1.6"
|
||||
async-recursion = "1.0.0"
|
||||
async-tar = "0.4.2"
|
||||
async-trait = "0.1"
|
||||
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
|
||||
bitflags = "2.4.2"
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e82eec97691c3acdb43494484be60d661edfebf3" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "e82eec97691c3acdb43494484be60d661edfebf3" }
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c" }
|
||||
cap-std = "3.0"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
clickhouse = { version = "0.11.6" }
|
||||
ctor = "0.2.6"
|
||||
ctrlc = "3.4.4"
|
||||
signal-hook = "0.3.17"
|
||||
core-foundation = { version = "0.9.3" }
|
||||
core-foundation-sys = "0.8.6"
|
||||
derive_more = "0.99.17"
|
||||
emojis = "0.6.1"
|
||||
env_logger = "0.9"
|
||||
exec = "0.3.1"
|
||||
fork = "0.1.23"
|
||||
futures = "0.3"
|
||||
futures-batch = "0.6.1"
|
||||
futures-lite = "1.13"
|
||||
@@ -281,10 +292,12 @@ isahc = { version = "1.7.2", default-features = false, features = [
|
||||
] }
|
||||
itertools = "0.11.0"
|
||||
lazy_static = "1.4.0"
|
||||
libc = "0.2"
|
||||
linkify = "0.10.0"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
nanoid = "0.4"
|
||||
nix = "0.28"
|
||||
once_cell = "1.19.0"
|
||||
ordered-float = "2.1.1"
|
||||
palette = { version = "0.7.5", default-features = false, features = ["std"] }
|
||||
parking_lot = "0.12.1"
|
||||
@@ -298,6 +311,7 @@ pulldown-cmark = { version = "0.10.0", default-features = false }
|
||||
rand = "0.8.5"
|
||||
refineable = { path = "./crates/refineable" }
|
||||
regex = "1.5"
|
||||
repair_json = "0.1.0"
|
||||
rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
|
||||
rust-embed = { version = "8.0", features = ["include-exclude"] }
|
||||
schemars = "0.8"
|
||||
@@ -377,6 +391,8 @@ version = "0.53.0"
|
||||
features = [
|
||||
"implement",
|
||||
"Foundation_Numerics",
|
||||
"System",
|
||||
"System_Threading",
|
||||
"Wdk_System_SystemServices",
|
||||
"Win32_Globalization",
|
||||
"Win32_Graphics_Direct2D",
|
||||
@@ -400,6 +416,7 @@ features = [
|
||||
"Win32_System_SystemServices",
|
||||
"Win32_System_Threading",
|
||||
"Win32_System_Time",
|
||||
"Win32_System_WinRT",
|
||||
"Win32_UI_Controls",
|
||||
"Win32_UI_HiDpi",
|
||||
"Win32_UI_Input_Ime",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax = docker/dockerfile:1.2
|
||||
|
||||
FROM rust:1.77-bookworm as builder
|
||||
FROM rust:1.78-bookworm as builder
|
||||
WORKDIR app
|
||||
COPY . .
|
||||
|
||||
|
||||
100
README.md
@@ -1,50 +1,50 @@
|
||||
# Zed
|
||||
|
||||
[](https://github.com/zed-industries/zed/actions/workflows/ci.yml)
|
||||
|
||||
Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter).
|
||||
|
||||
## Installation
|
||||
|
||||
You can [download](https://zed.dev/download) Zed today for macOS (v10.15+).
|
||||
|
||||
Support for additional platforms is on our [roadmap](https://zed.dev/roadmap):
|
||||
|
||||
- Linux ([tracking issue](https://github.com/zed-industries/zed/issues/7015))
|
||||
- Windows ([tracking issue](https://github.com/zed-industries/zed/issues/5394))
|
||||
- Web ([tracking issue](https://github.com/zed-industries/zed/issues/5396))
|
||||
|
||||
For macOS users, you can also install Zed using [Homebrew](https://brew.sh/):
|
||||
|
||||
```sh
|
||||
brew install --cask zed
|
||||
```
|
||||
|
||||
Alternatively, to install the Preview release:
|
||||
|
||||
```sh
|
||||
brew install --cask zed@preview
|
||||
```
|
||||
|
||||
## Developing Zed
|
||||
|
||||
- [Building Zed for macOS](./docs/src/developing_zed__building_zed_macos.md)
|
||||
- [Building Zed for Linux](./docs/src/developing_zed__building_zed_linux.md)
|
||||
- [Building Zed for Windows](./docs/src/developing_zed__building_zed_windows.md)
|
||||
- [Running Collaboration Locally](./docs/src/developing_zed__local_collaboration.md)
|
||||
|
||||
## Contributing
|
||||
|
||||
See [CONTRIBUTING.md](./CONTRIBUTING.md) for ways you can contribute to Zed.
|
||||
|
||||
Also... we're hiring! Check out our [jobs](https://zed.dev/jobs) page for open roles.
|
||||
|
||||
## Licensing
|
||||
|
||||
License information for third party dependencies must be correctly provided for CI to pass.
|
||||
|
||||
We use [`cargo-about`](https://github.com/EmbarkStudios/cargo-about) to automatically comply with open source licenses. If CI is failing, check the following:
|
||||
|
||||
- Is it showing a `no license specified` error for a crate you've created? If so, add `publish = false` under `[package]` in your crate's Cargo.toml.
|
||||
- Is the error `failed to satisfy license requirements` for a dependency? If so, first determine what license the project has and whether this system is sufficient to comply with this license's requirements. If you're unsure, ask a lawyer. Once you've verified that this system is acceptable add the license's SPDX identifier to the `accepted` array in `script/licenses/zed-licenses.toml`.
|
||||
- Is `cargo-about` unable to find the license for a dependency? If so, add a clarification field at the end of `script/licenses/zed-licenses.toml`, as specified in the [cargo-about book](https://embarkstudios.github.io/cargo-about/cli/generate/config.html#crate-configuration).
|
||||
# Zed
|
||||
|
||||
[](https://github.com/zed-industries/zed/actions/workflows/ci.yml)
|
||||
|
||||
Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter).
|
||||
|
||||
## Installation
|
||||
|
||||
You can [download](https://zed.dev/download) Zed today for macOS (v10.15+).
|
||||
|
||||
Support for additional platforms is on our [roadmap](https://zed.dev/roadmap):
|
||||
|
||||
- Linux ([tracking issue](https://github.com/zed-industries/zed/issues/7015))
|
||||
- Windows ([tracking issue](https://github.com/zed-industries/zed/issues/5394))
|
||||
- Web ([tracking issue](https://github.com/zed-industries/zed/issues/5396))
|
||||
|
||||
For macOS users, you can also install Zed using [Homebrew](https://brew.sh/):
|
||||
|
||||
```sh
|
||||
brew install --cask zed
|
||||
```
|
||||
|
||||
Alternatively, to install the Preview release:
|
||||
|
||||
```sh
|
||||
brew install --cask zed@preview
|
||||
```
|
||||
|
||||
## Developing Zed
|
||||
|
||||
- [Building Zed for macOS](./docs/src/development/macos.md)
|
||||
- [Building Zed for Linux](./docs/src/development/linux.md)
|
||||
- [Building Zed for Windows](./docs/src/development/windows.md)
|
||||
- [Running Collaboration Locally](./docs/src/development/local-collaboration.md)
|
||||
|
||||
## Contributing
|
||||
|
||||
See [CONTRIBUTING.md](./CONTRIBUTING.md) for ways you can contribute to Zed.
|
||||
|
||||
Also... we're hiring! Check out our [jobs](https://zed.dev/jobs) page for open roles.
|
||||
|
||||
## Licensing
|
||||
|
||||
License information for third party dependencies must be correctly provided for CI to pass.
|
||||
|
||||
We use [`cargo-about`](https://github.com/EmbarkStudios/cargo-about) to automatically comply with open source licenses. If CI is failing, check the following:
|
||||
|
||||
- Is it showing a `no license specified` error for a crate you've created? If so, add `publish = false` under `[package]` in your crate's Cargo.toml.
|
||||
- Is the error `failed to satisfy license requirements` for a dependency? If so, first determine what license the project has and whether this system is sufficient to comply with this license's requirements. If you're unsure, ask a lawyer. Once you've verified that this system is acceptable add the license's SPDX identifier to the `accepted` array in `script/licenses/zed-licenses.toml`.
|
||||
- Is `cargo-about` unable to find the license for a dependency? If so, add a clarification field at the end of `script/licenses/zed-licenses.toml`, as specified in the [cargo-about book](https://embarkstudios.github.io/cargo-about/cli/generate/config.html#crate-configuration).
|
||||
|
||||
3
assets/icons/indicator.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.5 15C13.433 15 15 13.433 15 11.5C15 9.567 13.433 8 11.5 8C9.567 8 8 9.567 8 11.5C8 13.433 9.567 15 11.5 15Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 240 B |
3
assets/icons/indicator_x.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.4662 14.9152C13.5801 15.0291 13.7648 15.0291 13.8787 14.9152L14.9145 13.8793C15.0284 13.7654 15.0284 13.5807 14.9145 13.4667L12.9483 11.5004L14.9145 9.53392C15.0285 9.42004 15.0285 9.23533 14.9145 9.12137L13.8787 8.08547C13.7648 7.97154 13.5801 7.97154 13.4662 8.08547L11.5 10.0519L9.53376 8.08545C9.41988 7.97152 9.23517 7.97152 9.12124 8.08545L8.08543 9.12136C7.97152 9.23533 7.97152 9.42004 8.08543 9.53392L10.0517 11.5004L8.08545 13.4667C7.97155 13.5807 7.97155 13.7654 8.08545 13.8793L9.12126 14.9152C9.23517 15.0292 9.41988 15.0292 9.53376 14.9152L11.5 12.9489L13.4662 14.9152Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 756 B |
3
assets/icons/strikethrough.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 4L13 12" stroke="black" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 181 B |
8
assets/icons/supermaven.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.30859 13.0703C3.80693 13.0703 4.21094 12.6663 4.21094 12.168C4.21094 11.6696 3.80693 11.2656 3.30859 11.2656C2.81025 11.2656 2.40625 11.6696 2.40625 12.168C2.40625 12.6663 2.81025 13.0703 3.30859 13.0703Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.53516 8.03849L4.10799 12.6055L2.51562 11.7584L4.94279 7.19141L6.53516 8.03849Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.38281 2.62443L4.93916 7.19141L3.33594 6.34432L5.77959 1.77734L7.38281 2.62443Z" fill="black"/>
|
||||
<path d="M6.5625 3.08984C7.06084 3.08984 7.46484 2.68585 7.46484 2.1875C7.46484 1.68915 7.06084 1.28516 6.5625 1.28516C6.06416 1.28516 5.66016 1.68915 5.66016 2.1875C5.66016 2.68585 6.06416 3.08984 6.5625 3.08984Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.882 1.31204C11.2842 1.41224 11.5664 1.7732 11.5664 2.18737V12.168H9.76084V5.8056L8.12938 8.87176L6.53516 8.02471L9.86653 1.76385C10.0611 1.39816 10.4799 1.21184 10.882 1.31204Z" fill="black"/>
|
||||
<path d="M10.6641 13.0703C11.1624 13.0703 11.5664 12.6663 11.5664 12.168C11.5664 11.6696 11.1624 11.2656 10.6641 11.2656C10.1657 11.2656 9.76172 11.6696 9.76172 12.168C9.76172 12.6663 10.1657 13.0703 10.6641 13.0703Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
15
assets/icons/supermaven_disabled.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g opacity="0.5">
|
||||
<path d="M3.78125 14.9375C4.35078 14.9375 4.8125 14.4758 4.8125 13.9062C4.8125 13.3367 4.35078 12.875 3.78125 12.875C3.21172 12.875 2.75 13.3367 2.75 13.9062C2.75 14.4758 3.21172 14.9375 3.78125 14.9375Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.46875 9.18684L4.69484 14.4062L2.875 13.4382L5.64891 8.21875L7.46875 9.18684Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.4375 2.99935L5.64475 8.21875L3.8125 7.25066L6.60525 2.03125L8.4375 2.99935Z" fill="white"/>
|
||||
<path d="M7.5 3.53125C8.06953 3.53125 8.53125 3.06954 8.53125 2.5C8.53125 1.93046 8.06953 1.46875 7.5 1.46875C6.93047 1.46875 6.46875 1.93046 6.46875 2.5C6.46875 3.06954 6.93047 3.53125 7.5 3.53125Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.4366 1.49947C12.8962 1.61399 13.2188 2.02651 13.2188 2.49985V13.9063H11.1552V6.63497L9.29072 10.1392L7.46875 9.17109L11.276 2.01583C11.4984 1.59789 11.977 1.38496 12.4366 1.49947Z" fill="white"/>
|
||||
<path d="M12.1875 14.9375C12.757 14.9375 13.2188 14.4758 13.2188 13.9062C13.2188 13.3367 12.757 12.875 12.1875 12.875C11.618 12.875 11.1562 13.3367 11.1562 13.9062C11.1562 14.4758 11.618 14.9375 12.1875 14.9375Z" fill="white"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M0.906311 6.42261L1.75155 4.60999L15.3462 10.9493L14.5009 12.7619L0.906311 6.42261Z" fill="white"/>
|
||||
<circle cx="14.7841" cy="11.7906" r="1" transform="rotate(-65 14.7841 11.7906)" fill="white"/>
|
||||
<circle cx="1.32893" cy="5.51631" r="1" transform="rotate(-65 1.32893 5.51631)" fill="white"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
11
assets/icons/supermaven_error.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g opacity="0.5">
|
||||
<path d="M3.78125 14.9375C4.35078 14.9375 4.8125 14.4758 4.8125 13.9062C4.8125 13.3367 4.35078 12.875 3.78125 12.875C3.21172 12.875 2.75 13.3367 2.75 13.9062C2.75 14.4758 3.21172 14.9375 3.78125 14.9375Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.46875 9.18684L4.69484 14.4062L2.875 13.4382L5.64891 8.21875L7.46875 9.18684Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.4375 2.99935L5.64475 8.21875L3.8125 7.25066L6.60525 2.03125L8.4375 2.99935Z" fill="white"/>
|
||||
<path d="M7.5 3.53125C8.06953 3.53125 8.53125 3.06954 8.53125 2.5C8.53125 1.93046 8.06953 1.46875 7.5 1.46875C6.93047 1.46875 6.46875 1.93046 6.46875 2.5C6.46875 3.06954 6.93047 3.53125 7.5 3.53125Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.4366 1.49947C12.8962 1.61399 13.2188 2.02651 13.2188 2.49985V13.9063H11.1552V6.63497L9.29072 10.1392L7.46875 9.17109L11.276 2.01583C11.4984 1.59789 11.977 1.38496 12.4366 1.49947Z" fill="white"/>
|
||||
<path d="M12.1875 14.9375C12.757 14.9375 13.2188 14.4758 13.2188 13.9062C13.2188 13.3367 12.757 12.875 12.1875 12.875C11.618 12.875 11.1562 13.3367 11.1562 13.9062C11.1562 14.4758 11.618 14.9375 12.1875 14.9375Z" fill="white"/>
|
||||
</g>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.6847 15.9265C14.7823 16.0241 14.9406 16.0241 15.0382 15.9265L15.9259 15.0387C16.0235 14.9411 16.0235 14.7828 15.9259 14.6851L14.2408 12.9999L15.9259 11.3146C16.0236 11.217 16.0236 11.0587 15.9259 10.961L15.0382 10.0733C14.9406 9.97561 14.7823 9.97561 14.6847 10.0733L12.9996 11.7585L11.3145 10.0732C11.2169 9.97559 11.0586 9.97559 10.9609 10.0732L10.0732 10.961C9.97559 11.0587 9.97559 11.217 10.0732 11.3146L11.7584 12.9999L10.0732 14.6851C9.97562 14.7828 9.97562 14.9411 10.0732 15.0387L10.9609 15.9265C11.0586 16.0242 11.2169 16.0242 11.3145 15.9265L12.9996 14.2413L14.6847 15.9265Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
11
assets/icons/supermaven_init.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g opacity="0.5">
|
||||
<path d="M3.78125 14.9375C4.35078 14.9375 4.8125 14.4758 4.8125 13.9062C4.8125 13.3367 4.35078 12.875 3.78125 12.875C3.21172 12.875 2.75 13.3367 2.75 13.9062C2.75 14.4758 3.21172 14.9375 3.78125 14.9375Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.46875 9.18684L4.69484 14.4062L2.875 13.4382L5.64891 8.21875L7.46875 9.18684Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.4375 2.99935L5.64475 8.21875L3.8125 7.25066L6.60525 2.03125L8.4375 2.99935Z" fill="white"/>
|
||||
<path d="M7.5 3.53125C8.06953 3.53125 8.53125 3.06954 8.53125 2.5C8.53125 1.93046 8.06953 1.46875 7.5 1.46875C6.93047 1.46875 6.46875 1.93046 6.46875 2.5C6.46875 3.06954 6.93047 3.53125 7.5 3.53125Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.4366 1.49947C12.8962 1.61399 13.2188 2.02651 13.2188 2.49985V13.9063H11.1552V6.63497L9.29072 10.1392L7.46875 9.17109L11.276 2.01583C11.4984 1.59789 11.977 1.38496 12.4366 1.49947Z" fill="white"/>
|
||||
<path d="M12.1875 14.9375C12.757 14.9375 13.2188 14.4758 13.2188 13.9062C13.2188 13.3367 12.757 12.875 12.1875 12.875C11.618 12.875 11.1562 13.3367 11.1562 13.9062C11.1562 14.4758 11.618 14.9375 12.1875 14.9375Z" fill="white"/>
|
||||
</g>
|
||||
<circle cx="13" cy="13" r="3" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -39,13 +39,13 @@
|
||||
"cmd-shift-left": "editor::SelectToBeginningOfLine",
|
||||
"cmd-shift-right": "editor::SelectToEndOfLine",
|
||||
"alt-shift-left": [
|
||||
"editor::SelectToBeginningOfLine",
|
||||
"editor::SelectToPreviousWordStart",
|
||||
{
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
],
|
||||
"alt-shift-right": [
|
||||
"editor::SelectToEndOfLine",
|
||||
"editor::SelectToNextWordEnd",
|
||||
{
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
|
||||
@@ -117,6 +117,9 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"m": ["vim::PushOperator", "Mark"],
|
||||
"'": ["vim::PushOperator", { "Jump": { "line": true } }],
|
||||
"`": ["vim::PushOperator", { "Jump": { "line": false } }],
|
||||
";": "vim::RepeatFind",
|
||||
",": "vim::RepeatFindReversed",
|
||||
"ctrl-o": "pane::GoBack",
|
||||
@@ -128,6 +131,7 @@
|
||||
"shift-v": "vim::ToggleVisualLine",
|
||||
"ctrl-v": "vim::ToggleVisualBlock",
|
||||
"ctrl-q": "vim::ToggleVisualBlock",
|
||||
"shift-k": "editor::Hover",
|
||||
"shift-r": "vim::ToggleReplace",
|
||||
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
|
||||
"ctrl-f": "vim::PageDown",
|
||||
@@ -236,6 +240,9 @@
|
||||
],
|
||||
"g ]": "editor::GoToDiagnostic",
|
||||
"g [": "editor::GoToPrevDiagnostic",
|
||||
"g i": ["workspace::SendKeystrokes", "` ^ i"],
|
||||
"g ,": "vim::ChangeListNewer",
|
||||
"g ;": "vim::ChangeListOlder",
|
||||
"shift-h": "vim::WindowTop",
|
||||
"shift-m": "vim::WindowMiddle",
|
||||
"shift-l": "vim::WindowBottom",
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
"base_keymap": "VSCode",
|
||||
// Features that can be globally enabled or disabled
|
||||
"features": {
|
||||
// Show Copilot icon in status bar
|
||||
"copilot": true
|
||||
// Which inline completion provider to use.
|
||||
"inline_completion_provider": "copilot"
|
||||
},
|
||||
// The name of a font to use for rendering text in the editor
|
||||
"buffer_font_family": "Zed Mono",
|
||||
@@ -316,6 +316,8 @@
|
||||
"autosave": "off",
|
||||
// Settings related to the editor's tab bar.
|
||||
"tab_bar": {
|
||||
// Whether or not to show the tab bar in the editor
|
||||
"show": true,
|
||||
// Whether or not to show the navigation history buttons.
|
||||
"show_nav_history_buttons": true
|
||||
},
|
||||
@@ -370,11 +372,13 @@
|
||||
//
|
||||
// 1. Do not soft wrap.
|
||||
// "soft_wrap": "none",
|
||||
// 2. Soft wrap lines that overflow the editor:
|
||||
// 2. Prefer a single line generally, unless an overly long line is encountered.
|
||||
// "soft_wrap": "prefer_line",
|
||||
// 3. Soft wrap lines that overflow the editor:
|
||||
// "soft_wrap": "editor_width",
|
||||
// 3. Soft wrap lines at the preferred line length
|
||||
// 4. Soft wrap lines at the preferred line length
|
||||
// "soft_wrap": "preferred_line_length",
|
||||
"soft_wrap": "none",
|
||||
"soft_wrap": "prefer_line",
|
||||
// The column at which to soft-wrap lines, for buffers where soft-wrap
|
||||
// is enabled.
|
||||
"preferred_line_length": 80,
|
||||
@@ -598,7 +602,12 @@
|
||||
"format_on_save": "off"
|
||||
},
|
||||
"Elixir": {
|
||||
"language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
|
||||
"language_servers": [
|
||||
"elixir-ls",
|
||||
"!next-ls",
|
||||
"!lexical",
|
||||
"..."
|
||||
]
|
||||
},
|
||||
"Gleam": {
|
||||
"tab_size": 2
|
||||
@@ -609,11 +618,19 @@
|
||||
}
|
||||
},
|
||||
"HEEX": {
|
||||
"language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
|
||||
"language_servers": [
|
||||
"elixir-ls",
|
||||
"!next-ls",
|
||||
"!lexical",
|
||||
"..."
|
||||
]
|
||||
},
|
||||
"Make": {
|
||||
"hard_tabs": true
|
||||
},
|
||||
"Markdown": {
|
||||
"format_on_save": "off"
|
||||
},
|
||||
"Prisma": {
|
||||
"tab_size": 2
|
||||
}
|
||||
|
||||
@@ -281,11 +281,14 @@ impl ActivityIndicator {
|
||||
message: "Installing Zed update…".to_string(),
|
||||
on_click: None,
|
||||
},
|
||||
AutoUpdateStatus::Updated => Content {
|
||||
AutoUpdateStatus::Updated { binary_path } => Content {
|
||||
icon: None,
|
||||
message: "Click to restart and update Zed".to_string(),
|
||||
on_click: Some(Arc::new(|_, cx| {
|
||||
workspace::restart(&Default::default(), cx)
|
||||
on_click: Some(Arc::new({
|
||||
let restart = workspace::Restart {
|
||||
binary_path: Some(binary_path.clone()),
|
||||
};
|
||||
move |_, cx| workspace::restart(&restart, cx)
|
||||
})),
|
||||
},
|
||||
AutoUpdateStatus::Errored => Content {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, StreamExt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::convert::TryFrom;
|
||||
use std::{convert::TryFrom, sync::Arc};
|
||||
use util::http::{AsyncBody, HttpClient, Method, Request as HttpRequest};
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||
@@ -141,7 +141,7 @@ pub enum TextDelta {
|
||||
}
|
||||
|
||||
pub async fn stream_completion(
|
||||
client: &dyn HttpClient,
|
||||
client: Arc<dyn HttpClient>,
|
||||
api_url: &str,
|
||||
api_key: &str,
|
||||
request: Request,
|
||||
|
||||
@@ -106,6 +106,11 @@ impl SavedConversationMetadata {
|
||||
.and_then(|name| name.to_str())
|
||||
.zip(metadata)
|
||||
{
|
||||
// This is used to filter out conversations saved by the new assistant.
|
||||
if !re.is_match(file_name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let title = re.replace(file_name, "");
|
||||
conversations.push(Self {
|
||||
title: title.into_owned(),
|
||||
|
||||
@@ -19,17 +19,20 @@ stories = ["dep:story"]
|
||||
anyhow.workspace = true
|
||||
assistant_tooling.workspace = true
|
||||
client.workspace = true
|
||||
chrono.workspace = true
|
||||
collections.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
nanoid.workspace = true
|
||||
open_ai.workspace = true
|
||||
picker.workspace = true
|
||||
project.workspace = true
|
||||
regex.workspace = true
|
||||
rich_text.workspace = true
|
||||
schemars.workspace = true
|
||||
semantic_index.workspace = true
|
||||
|
||||
1
crates/assistant2/evals/list-of-into-element.md
Normal file
@@ -0,0 +1 @@
|
||||
> Give me a comprehensive list of all the elements defined in my project using the following query: `impl Element for {}, impl<T: 'static> Element for {}, impl IntoElement for {})`
|
||||
1
crates/assistant2/evals/new-gpui-element.md
Normal file
@@ -0,0 +1 @@
|
||||
> What are all the places we define a new gpui element in my project? (impl Element for {})
|
||||
3
crates/assistant2/evals/settings-file.md
Normal file
@@ -0,0 +1,3 @@
|
||||
Use tools frequently, especially when referring to files and code. The Zed editor we're working in can show me files directly when you add annotations. Be concise in chat, bountiful in tool calling.
|
||||
|
||||
Teach me everything you can about how zed loads settings. Please annotate the code inline.
|
||||
1
crates/assistant2/evals/what-is-the-assistant2-crate.md
Normal file
@@ -0,0 +1 @@
|
||||
> Can you tell me what the assistant2 crate is for in my project? Tell me in 100 words or less.
|
||||
@@ -1,378 +0,0 @@
|
||||
//! This example creates a basic Chat UI with a function for rolling a die.
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use assets::Assets;
|
||||
use assistant2::AssistantPanel;
|
||||
use assistant_tooling::{LanguageModelTool, ToolRegistry};
|
||||
use client::{Client, UserStore};
|
||||
use fs::Fs;
|
||||
use futures::StreamExt as _;
|
||||
use gpui::{actions, AnyElement, App, AppContext, KeyBinding, Model, Task, View, WindowOptions};
|
||||
use language::LanguageRegistry;
|
||||
use project::Project;
|
||||
use rand::Rng;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{KeymapFile, DEFAULT_KEYMAP_PATH};
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use theme::LoadThemes;
|
||||
use ui::{div, prelude::*, Render};
|
||||
use util::ResultExt as _;
|
||||
|
||||
actions!(example, [Quit]);
|
||||
|
||||
struct RollDiceTool {}
|
||||
|
||||
impl RollDiceTool {
|
||||
fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, JsonSchema, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum Die {
|
||||
D6 = 6,
|
||||
D20 = 20,
|
||||
}
|
||||
|
||||
impl Die {
|
||||
fn into_str(&self) -> &'static str {
|
||||
match self {
|
||||
Die::D6 => "d6",
|
||||
Die::D20 => "d20",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, JsonSchema, Clone)]
|
||||
struct DiceParams {
|
||||
/// The number of dice to roll.
|
||||
num_dice: u8,
|
||||
/// Which die to roll. Defaults to a d6 if not provided.
|
||||
die_type: Option<Die>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct DieRoll {
|
||||
die: Die,
|
||||
roll: u8,
|
||||
}
|
||||
|
||||
impl DieRoll {
|
||||
fn render(&self) -> AnyElement {
|
||||
match self.die {
|
||||
Die::D6 => {
|
||||
let face = match self.roll {
|
||||
6 => div().child("⚅"),
|
||||
5 => div().child("⚄"),
|
||||
4 => div().child("⚃"),
|
||||
3 => div().child("⚂"),
|
||||
2 => div().child("⚁"),
|
||||
1 => div().child("⚀"),
|
||||
_ => div().child("😅"),
|
||||
};
|
||||
face.text_3xl().into_any_element()
|
||||
}
|
||||
_ => div()
|
||||
.child(format!("{}", self.roll))
|
||||
.text_3xl()
|
||||
.into_any_element(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct DiceRoll {
|
||||
rolls: Vec<DieRoll>,
|
||||
}
|
||||
|
||||
pub struct DiceView {
|
||||
result: Result<DiceRoll>,
|
||||
}
|
||||
|
||||
impl Render for DiceView {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let output = match &self.result {
|
||||
Ok(output) => output,
|
||||
Err(_) => return "Somehow dice failed 🎲".into_any_element(),
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.children(
|
||||
output
|
||||
.rolls
|
||||
.iter()
|
||||
.map(|roll| div().p_2().child(roll.render())),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageModelTool for RollDiceTool {
|
||||
type Input = DiceParams;
|
||||
type Output = DiceRoll;
|
||||
type View = DiceView;
|
||||
|
||||
fn name(&self) -> String {
|
||||
"roll_dice".to_string()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Rolls N many dice and returns the results.".to_string()
|
||||
}
|
||||
|
||||
fn execute(
|
||||
&self,
|
||||
input: &Self::Input,
|
||||
_cx: &mut WindowContext,
|
||||
) -> Task<gpui::Result<Self::Output>> {
|
||||
let rolls = (0..input.num_dice)
|
||||
.map(|_| {
|
||||
let die_type = input.die_type.as_ref().unwrap_or(&Die::D6).clone();
|
||||
|
||||
DieRoll {
|
||||
die: die_type.clone(),
|
||||
roll: rand::thread_rng().gen_range(1..=die_type as u8),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
return Task::ready(Ok(DiceRoll { rolls }));
|
||||
}
|
||||
|
||||
fn output_view(
|
||||
_tool_call_id: String,
|
||||
_input: Self::Input,
|
||||
result: Result<Self::Output>,
|
||||
cx: &mut WindowContext,
|
||||
) -> gpui::View<Self::View> {
|
||||
cx.new_view(|_cx| DiceView { result })
|
||||
}
|
||||
|
||||
fn format(_: &Self::Input, output: &Result<Self::Output>) -> String {
|
||||
let output = match output {
|
||||
Ok(output) => output,
|
||||
Err(_) => return "Somehow dice failed 🎲".to_string(),
|
||||
};
|
||||
|
||||
let mut result = String::new();
|
||||
for roll in &output.rolls {
|
||||
let die = &roll.die;
|
||||
result.push_str(&format!("{}: {}\n", die.into_str(), roll.roll));
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
struct FileBrowserTool {
|
||||
fs: Arc<dyn Fs>,
|
||||
root_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl FileBrowserTool {
|
||||
fn new(fs: Arc<dyn Fs>, root_dir: PathBuf) -> Self {
|
||||
Self { fs, root_dir }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
struct FileBrowserParams {
|
||||
command: FileBrowserCommand,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
enum FileBrowserCommand {
|
||||
Ls { path: PathBuf },
|
||||
Cat { path: PathBuf },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
enum FileBrowserOutput {
|
||||
Ls { entries: Vec<String> },
|
||||
Cat { content: String },
|
||||
}
|
||||
|
||||
pub struct FileBrowserView {
|
||||
result: Result<FileBrowserOutput>,
|
||||
}
|
||||
|
||||
impl Render for FileBrowserView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let Ok(output) = self.result.as_ref() else {
|
||||
return h_flex().child("Failed to perform operation");
|
||||
};
|
||||
|
||||
match output {
|
||||
FileBrowserOutput::Ls { entries } => v_flex().children(
|
||||
entries
|
||||
.into_iter()
|
||||
.map(|entry| h_flex().text_ui(cx).child(entry.clone())),
|
||||
),
|
||||
FileBrowserOutput::Cat { content } => h_flex().child(content.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageModelTool for FileBrowserTool {
|
||||
type Input = FileBrowserParams;
|
||||
type Output = FileBrowserOutput;
|
||||
type View = FileBrowserView;
|
||||
|
||||
fn name(&self) -> String {
|
||||
"file_browser".to_string()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"A tool for browsing the filesystem.".to_string()
|
||||
}
|
||||
|
||||
fn execute(
|
||||
&self,
|
||||
input: &Self::Input,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<gpui::Result<Self::Output>> {
|
||||
cx.spawn({
|
||||
let fs = self.fs.clone();
|
||||
let root_dir = self.root_dir.clone();
|
||||
let input = input.clone();
|
||||
|_cx| async move {
|
||||
match input.command {
|
||||
FileBrowserCommand::Ls { path } => {
|
||||
let path = root_dir.join(path);
|
||||
|
||||
let mut output = fs.read_dir(&path).await?;
|
||||
|
||||
let mut entries = Vec::new();
|
||||
while let Some(entry) = output.next().await {
|
||||
let entry = entry?;
|
||||
entries.push(entry.display().to_string());
|
||||
}
|
||||
|
||||
Ok(FileBrowserOutput::Ls { entries })
|
||||
}
|
||||
FileBrowserCommand::Cat { path } => {
|
||||
let path = root_dir.join(path);
|
||||
|
||||
let output = fs.load(&path).await?;
|
||||
|
||||
Ok(FileBrowserOutput::Cat { content: output })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn output_view(
|
||||
_tool_call_id: String,
|
||||
_input: Self::Input,
|
||||
result: Result<Self::Output>,
|
||||
cx: &mut WindowContext,
|
||||
) -> gpui::View<Self::View> {
|
||||
cx.new_view(|_cx| FileBrowserView { result })
|
||||
}
|
||||
|
||||
fn format(_input: &Self::Input, output: &Result<Self::Output>) -> String {
|
||||
let Ok(output) = output else {
|
||||
return "Failed to perform command: {input:?}".to_string();
|
||||
};
|
||||
|
||||
match output {
|
||||
FileBrowserOutput::Ls { entries } => entries.join("\n"),
|
||||
FileBrowserOutput::Cat { content } => content.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
App::new().with_assets(Assets).run(|cx| {
|
||||
cx.bind_keys(Some(KeyBinding::new("cmd-q", Quit, None)));
|
||||
cx.on_action(|_: &Quit, cx: &mut AppContext| {
|
||||
cx.quit();
|
||||
});
|
||||
|
||||
settings::init(cx);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
editor::init(cx);
|
||||
theme::init(LoadThemes::JustBase, cx);
|
||||
Assets.load_fonts(cx).unwrap();
|
||||
KeymapFile::load_asset(DEFAULT_KEYMAP_PATH, cx).unwrap();
|
||||
client::init_settings(cx);
|
||||
release_channel::init("0.130.0", cx);
|
||||
|
||||
let client = Client::production(cx);
|
||||
{
|
||||
let client = client.clone();
|
||||
cx.spawn(|cx| async move { client.authenticate_and_connect(false, &cx).await })
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
assistant2::init(client.clone(), cx);
|
||||
|
||||
let language_registry = Arc::new(LanguageRegistry::new(
|
||||
Task::ready(()),
|
||||
cx.background_executor().clone(),
|
||||
));
|
||||
|
||||
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
|
||||
let node_runtime = node_runtime::RealNodeRuntime::new(client.http_client());
|
||||
languages::init(language_registry.clone(), node_runtime, cx);
|
||||
|
||||
cx.spawn(|cx| async move {
|
||||
cx.update(|cx| {
|
||||
let fs = Arc::new(fs::RealFs::new(None));
|
||||
let cwd = std::env::current_dir().expect("Failed to get current working directory");
|
||||
|
||||
cx.open_window(WindowOptions::default(), |cx| {
|
||||
let mut tool_registry = ToolRegistry::new();
|
||||
tool_registry
|
||||
.register(RollDiceTool::new(), cx)
|
||||
.context("failed to register DummyTool")
|
||||
.log_err();
|
||||
|
||||
tool_registry
|
||||
.register(FileBrowserTool::new(fs, cwd), cx)
|
||||
.context("failed to register FileBrowserTool")
|
||||
.log_err();
|
||||
|
||||
let tool_registry = Arc::new(tool_registry);
|
||||
|
||||
println!("Tools registered");
|
||||
for definition in tool_registry.definitions() {
|
||||
println!("{}", definition);
|
||||
}
|
||||
|
||||
cx.new_view(|cx| Example::new(language_registry, tool_registry, user_store, cx))
|
||||
});
|
||||
cx.activate(true);
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
}
|
||||
|
||||
struct Example {
|
||||
assistant_panel: View<AssistantPanel>,
|
||||
}
|
||||
|
||||
impl Example {
|
||||
fn new(
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
tool_registry: Arc<ToolRegistry>,
|
||||
user_store: Model<UserStore>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
Self {
|
||||
assistant_panel: cx.new_view(|cx| {
|
||||
AssistantPanel::new(language_registry, tool_registry, user_store, None, cx)
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Example {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl ui::prelude::IntoElement {
|
||||
div().size_full().child(self.assistant_panel.clone())
|
||||
}
|
||||
}
|
||||
3
crates/assistant2/src/attachments.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod active_file;
|
||||
|
||||
pub use active_file::*;
|
||||
144
crates/assistant2/src/attachments/active_file.rs
Normal file
@@ -0,0 +1,144 @@
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_tooling::{AttachmentOutput, LanguageModelAttachment, ProjectContext};
|
||||
use editor::Editor;
|
||||
use gpui::{Render, Task, View, WeakModel, WeakView};
|
||||
use language::Buffer;
|
||||
use project::ProjectPath;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ui::{prelude::*, ButtonLike, Tooltip, WindowContext};
|
||||
use util::maybe;
|
||||
use workspace::Workspace;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ActiveEditorAttachment {
|
||||
#[serde(skip)]
|
||||
buffer: Option<WeakModel<Buffer>>,
|
||||
path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
pub struct FileAttachmentView {
|
||||
project_path: Option<ProjectPath>,
|
||||
buffer: Option<WeakModel<Buffer>>,
|
||||
error: Option<anyhow::Error>,
|
||||
}
|
||||
|
||||
impl Render for FileAttachmentView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
if let Some(error) = &self.error {
|
||||
return div().child(error.to_string()).into_any_element();
|
||||
}
|
||||
|
||||
let filename: SharedString = self
|
||||
.project_path
|
||||
.as_ref()
|
||||
.and_then(|p| p.path.file_name()?.to_str())
|
||||
.unwrap_or("Untitled")
|
||||
.to_string()
|
||||
.into();
|
||||
|
||||
ButtonLike::new("file-attachment")
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded_md()
|
||||
.child(ui::Icon::new(IconName::File))
|
||||
.child(filename.clone()),
|
||||
)
|
||||
.tooltip(move |cx| Tooltip::with_meta("File Attached", None, filename.clone(), cx))
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl AttachmentOutput for FileAttachmentView {
|
||||
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String {
|
||||
if let Some(path) = &self.project_path {
|
||||
project.add_file(path.clone());
|
||||
return format!("current file: {}", path.path.display());
|
||||
}
|
||||
|
||||
if let Some(buffer) = self.buffer.as_ref().and_then(|buffer| buffer.upgrade()) {
|
||||
return format!("current untitled buffer text:\n{}", buffer.read(cx).text());
|
||||
}
|
||||
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ActiveEditorAttachmentTool {
|
||||
workspace: WeakView<Workspace>,
|
||||
}
|
||||
|
||||
impl ActiveEditorAttachmentTool {
|
||||
pub fn new(workspace: WeakView<Workspace>, _cx: &mut WindowContext) -> Self {
|
||||
Self { workspace }
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageModelAttachment for ActiveEditorAttachmentTool {
|
||||
type Output = ActiveEditorAttachment;
|
||||
type View = FileAttachmentView;
|
||||
|
||||
fn name(&self) -> Arc<str> {
|
||||
"active-editor-attachment".into()
|
||||
}
|
||||
|
||||
fn run(&self, cx: &mut WindowContext) -> Task<Result<ActiveEditorAttachment>> {
|
||||
Task::ready(maybe!({
|
||||
let active_buffer = self
|
||||
.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| Some(item.act_as::<Editor>(cx)?.read(cx).buffer().clone()))
|
||||
})?
|
||||
.ok_or_else(|| anyhow!("no active buffer"))?;
|
||||
|
||||
let buffer = active_buffer.read(cx);
|
||||
|
||||
if let Some(buffer) = buffer.as_singleton() {
|
||||
let path = project::File::from_dyn(buffer.read(cx).file())
|
||||
.and_then(|file| file.worktree.read(cx).absolutize(&file.path).ok());
|
||||
return Ok(ActiveEditorAttachment {
|
||||
buffer: Some(buffer.downgrade()),
|
||||
path,
|
||||
});
|
||||
} else {
|
||||
Err(anyhow!("no active buffer"))
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn view(
|
||||
&self,
|
||||
output: Result<ActiveEditorAttachment>,
|
||||
cx: &mut WindowContext,
|
||||
) -> View<Self::View> {
|
||||
let error;
|
||||
let project_path;
|
||||
let buffer;
|
||||
match output {
|
||||
Ok(output) => {
|
||||
error = None;
|
||||
let workspace = self.workspace.upgrade().unwrap();
|
||||
let project = workspace.read(cx).project();
|
||||
project_path = output
|
||||
.path
|
||||
.and_then(|path| project.read(cx).project_path_for_absolute_path(&path, cx));
|
||||
buffer = output.buffer;
|
||||
}
|
||||
Err(err) => {
|
||||
error = Some(err);
|
||||
buffer = None;
|
||||
project_path = None;
|
||||
}
|
||||
}
|
||||
cx.new_view(|_cx| FileAttachmentView {
|
||||
project_path,
|
||||
buffer,
|
||||
error,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ impl CompletionProvider {
|
||||
messages: Vec<CompletionMessage>,
|
||||
stop: Vec<String>,
|
||||
temperature: f32,
|
||||
tools: &[ToolFunctionDefinition],
|
||||
tools: Vec<ToolFunctionDefinition>,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<proto::LanguageModelResponseMessage>>>>
|
||||
{
|
||||
self.0.complete(model, messages, stop, temperature, tools)
|
||||
@@ -51,7 +51,7 @@ pub trait CompletionProviderBackend: 'static {
|
||||
messages: Vec<CompletionMessage>,
|
||||
stop: Vec<String>,
|
||||
temperature: f32,
|
||||
tools: &[ToolFunctionDefinition],
|
||||
tools: Vec<ToolFunctionDefinition>,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<proto::LanguageModelResponseMessage>>>>;
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ impl CompletionProviderBackend for CloudCompletionProvider {
|
||||
messages: Vec<CompletionMessage>,
|
||||
stop: Vec<String>,
|
||||
temperature: f32,
|
||||
tools: &[ToolFunctionDefinition],
|
||||
tools: Vec<ToolFunctionDefinition>,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<proto::LanguageModelResponseMessage>>>>
|
||||
{
|
||||
let client = self.client.clone();
|
||||
|
||||
90
crates/assistant2/src/saved_conversation.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
use std::cmp::Reverse;
|
||||
use std::ffi::OsStr;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use assistant_tooling::{SavedToolFunctionCall, SavedUserAttachment};
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use gpui::SharedString;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use util::paths::CONVERSATIONS_DIR;
|
||||
|
||||
use crate::MessageId;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedConversation {
|
||||
/// The schema version of the conversation.
|
||||
pub version: String,
|
||||
/// The title of the conversation, generated by the Assistant.
|
||||
pub title: String,
|
||||
pub messages: Vec<SavedChatMessage>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum SavedChatMessage {
|
||||
User {
|
||||
id: MessageId,
|
||||
body: String,
|
||||
attachments: Vec<SavedUserAttachment>,
|
||||
},
|
||||
Assistant {
|
||||
id: MessageId,
|
||||
messages: Vec<SavedAssistantMessagePart>,
|
||||
error: Option<SharedString>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedAssistantMessagePart {
|
||||
pub body: SharedString,
|
||||
pub tool_calls: Vec<SavedToolFunctionCall>,
|
||||
}
|
||||
|
||||
pub struct SavedConversationMetadata {
|
||||
pub title: String,
|
||||
pub path: PathBuf,
|
||||
pub mtime: chrono::DateTime<chrono::Local>,
|
||||
}
|
||||
|
||||
impl SavedConversationMetadata {
|
||||
pub async fn list(fs: Arc<dyn Fs>) -> Result<Vec<Self>> {
|
||||
fs.create_dir(&CONVERSATIONS_DIR).await?;
|
||||
|
||||
let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?;
|
||||
let mut conversations = Vec::new();
|
||||
while let Some(path) = paths.next().await {
|
||||
let path = path?;
|
||||
if path.extension() != Some(OsStr::new("json")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let pattern = r" - \d+.zed.\d.\d.\d.json$";
|
||||
let re = Regex::new(pattern).unwrap();
|
||||
|
||||
let metadata = fs.metadata(&path).await?;
|
||||
if let Some((file_name, metadata)) = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.zip(metadata)
|
||||
{
|
||||
// This is used to filter out conversations saved by the old assistant.
|
||||
if !re.is_match(file_name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let title = re.replace(file_name, "");
|
||||
conversations.push(Self {
|
||||
title: title.into_owned(),
|
||||
path,
|
||||
mtime: metadata.mtime.into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime));
|
||||
|
||||
Ok(conversations)
|
||||
}
|
||||
}
|
||||
196
crates/assistant2/src/saved_conversations.rs
Normal file
@@ -0,0 +1,196 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
|
||||
use gpui::{AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, View, WeakView};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::saved_conversation::SavedConversationMetadata;
|
||||
|
||||
pub struct SavedConversations {
|
||||
focus_handle: FocusHandle,
|
||||
picker: Option<View<Picker<SavedConversationPickerDelegate>>>,
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for SavedConversations {}
|
||||
|
||||
impl FocusableView for SavedConversations {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
if let Some(picker) = self.picker.as_ref() {
|
||||
picker.focus_handle(cx)
|
||||
} else {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SavedConversations {
|
||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
picker: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(
|
||||
&mut self,
|
||||
saved_conversations: Vec<SavedConversationMetadata>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let delegate =
|
||||
SavedConversationPickerDelegate::new(cx.view().downgrade(), saved_conversations);
|
||||
self.picker = Some(cx.new_view(|cx| Picker::uniform_list(delegate, cx).modal(false)));
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for SavedConversations {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.w_full()
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.children(self.picker.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SavedConversationPickerDelegate {
|
||||
view: WeakView<SavedConversations>,
|
||||
saved_conversations: Vec<SavedConversationMetadata>,
|
||||
selected_index: usize,
|
||||
matches: Vec<StringMatch>,
|
||||
}
|
||||
|
||||
impl SavedConversationPickerDelegate {
|
||||
pub fn new(
|
||||
weak_view: WeakView<SavedConversations>,
|
||||
saved_conversations: Vec<SavedConversationMetadata>,
|
||||
) -> Self {
|
||||
let matches = saved_conversations
|
||||
.iter()
|
||||
.map(|conversation| StringMatch {
|
||||
candidate_id: 0,
|
||||
score: 0.0,
|
||||
positions: Default::default(),
|
||||
string: conversation.title.clone(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
view: weak_view,
|
||||
saved_conversations,
|
||||
selected_index: 0,
|
||||
matches,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for SavedConversationPickerDelegate {
|
||||
type ListItem = ui::ListItem;
|
||||
|
||||
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
|
||||
"Select saved conversation...".into()
|
||||
}
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> gpui::Task<()> {
|
||||
let background_executor = cx.background_executor().clone();
|
||||
let candidates = self
|
||||
.saved_conversations
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, conversation)| {
|
||||
let text = conversation.title.clone();
|
||||
|
||||
StringMatchCandidate {
|
||||
id,
|
||||
char_bag: text.as_str().into(),
|
||||
string: text,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
let matches = if query.is_empty() {
|
||||
candidates
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, candidate)| StringMatch {
|
||||
candidate_id: index,
|
||||
string: candidate.string,
|
||||
positions: Vec::new(),
|
||||
score: 0.0,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
false,
|
||||
100,
|
||||
&Default::default(),
|
||||
background_executor,
|
||||
)
|
||||
.await
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, _cx| {
|
||||
this.delegate.matches = matches;
|
||||
this.delegate.selected_index = this
|
||||
.delegate
|
||||
.selected_index
|
||||
.min(this.delegate.matches.len().saturating_sub(1));
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
if self.matches.is_empty() {
|
||||
self.dismissed(cx);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Implement selecting a saved conversation.
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ui::prelude::ViewContext<Picker<Self>>) {
|
||||
self.view
|
||||
.update(cx, |_, cx| cx.emit(DismissEvent))
|
||||
.log_err();
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let conversation_match = &self.matches[ix];
|
||||
let _conversation = &self.saved_conversations[conversation_match.candidate_id];
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.selected(selected)
|
||||
.child(HighlightedLabel::new(
|
||||
conversation_match.string.clone(),
|
||||
conversation_match.positions.clone(),
|
||||
)),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
mod annotate_code;
|
||||
mod create_buffer;
|
||||
mod project_index;
|
||||
|
||||
pub use annotate_code::*;
|
||||
pub use create_buffer::*;
|
||||
pub use project_index::*;
|
||||
|
||||
310
crates/assistant2/src/tools/annotate_code.rs
Normal file
@@ -0,0 +1,310 @@
|
||||
use anyhow::Result;
|
||||
use assistant_tooling::{LanguageModelTool, ProjectContext, ToolOutput};
|
||||
use editor::{
|
||||
display_map::{BlockContext, BlockDisposition, BlockProperties, BlockStyle},
|
||||
Editor, MultiBuffer,
|
||||
};
|
||||
use futures::{channel::mpsc::UnboundedSender, StreamExt as _};
|
||||
use gpui::{prelude::*, AnyElement, AsyncWindowContext, Model, Task, View, WeakView};
|
||||
use language::ToPoint;
|
||||
use project::{search::SearchQuery, Project, ProjectPath};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use std::path::Path;
|
||||
use ui::prelude::*;
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub struct AnnotationTool {
|
||||
workspace: WeakView<Workspace>,
|
||||
project: Model<Project>,
|
||||
}
|
||||
|
||||
impl AnnotationTool {
|
||||
pub fn new(workspace: WeakView<Workspace>, project: Model<Project>) -> Self {
|
||||
Self { workspace, project }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Deserialize, JsonSchema, Clone)]
|
||||
pub struct AnnotationInput {
|
||||
/// Name for this set of annotations
|
||||
#[serde(default = "default_title")]
|
||||
title: String,
|
||||
/// Excerpts from the file to show to the user.
|
||||
excerpts: Vec<Excerpt>,
|
||||
}
|
||||
|
||||
fn default_title() -> String {
|
||||
"Untitled".to_string()
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema, Clone)]
|
||||
struct Excerpt {
|
||||
/// Path to the file
|
||||
path: String,
|
||||
/// A short, distinctive string that appears in the file, used to define a location in the file.
|
||||
text_passage: String,
|
||||
/// Text to display above the code excerpt. All explanation of code should be included here.
|
||||
annotation: String,
|
||||
}
|
||||
|
||||
impl LanguageModelTool for AnnotationTool {
|
||||
type View = AnnotationResultView;
|
||||
|
||||
fn name(&self) -> String {
|
||||
"show_code_file_excerpts".to_string()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"
|
||||
Show and explain code from the current project
|
||||
Opens a buffer in a separate pane/tab, to the side of the conversation.
|
||||
The annotations are shown in the editor as block decorations.
|
||||
Many related excerpts can be shown at once.
|
||||
"
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn view(&self, cx: &mut WindowContext) -> View<Self::View> {
|
||||
cx.new_view(|cx| {
|
||||
let (tx, mut rx) = futures::channel::mpsc::unbounded();
|
||||
cx.spawn(|view, mut cx| async move {
|
||||
while let Some(excerpt) = rx.next().await {
|
||||
AnnotationResultView::add_excerpt(view.clone(), excerpt, &mut cx).await?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
AnnotationResultView {
|
||||
project: self.project.clone(),
|
||||
workspace: self.workspace.clone(),
|
||||
tx,
|
||||
pending_excerpt: None,
|
||||
added_editor_to_workspace: false,
|
||||
editor: None,
|
||||
error: None,
|
||||
rendered_excerpt_count: 0,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AnnotationResultView {
|
||||
workspace: WeakView<Workspace>,
|
||||
project: Model<Project>,
|
||||
pending_excerpt: Option<Excerpt>,
|
||||
added_editor_to_workspace: bool,
|
||||
editor: Option<View<Editor>>,
|
||||
tx: UnboundedSender<Excerpt>,
|
||||
error: Option<anyhow::Error>,
|
||||
rendered_excerpt_count: usize,
|
||||
}
|
||||
|
||||
impl AnnotationResultView {
|
||||
async fn add_excerpt(
|
||||
this: WeakView<Self>,
|
||||
excerpt: Excerpt,
|
||||
cx: &mut AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
let project = this.update(cx, |this, _cx| this.project.clone())?;
|
||||
|
||||
let worktree_id = project.update(cx, |project, cx| {
|
||||
let worktree = project.worktrees().next()?;
|
||||
let worktree_id = worktree.read(cx).id();
|
||||
Some(worktree_id)
|
||||
})?;
|
||||
|
||||
let worktree_id = if let Some(worktree_id) = worktree_id {
|
||||
worktree_id
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("No worktree found"));
|
||||
};
|
||||
|
||||
let buffer_task = project.update(cx, |project, cx| {
|
||||
project.open_buffer(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Path::new(&excerpt.path).into(),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
|
||||
let buffer = match buffer_task.await {
|
||||
Ok(buffer) => buffer,
|
||||
Err(error) => {
|
||||
return this.update(cx, |this, cx| {
|
||||
this.error = Some(error);
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
let snapshot = buffer.update(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
let query = SearchQuery::text(&excerpt.text_passage, false, false, false, vec![], vec![])?;
|
||||
let matches = query.search(&snapshot, None).await;
|
||||
let Some(first_match) = matches.first() else {
|
||||
log::warn!(
|
||||
"text {:?} does not appear in '{}'",
|
||||
excerpt.text_passage,
|
||||
excerpt.path
|
||||
);
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
let mut start = first_match.start.to_point(&snapshot);
|
||||
start.column = 0;
|
||||
|
||||
if let Some(editor) = &this.editor {
|
||||
editor.update(cx, |editor, cx| {
|
||||
let ranges = editor.buffer().update(cx, |multibuffer, cx| {
|
||||
multibuffer.push_excerpts_with_context_lines(
|
||||
buffer.clone(),
|
||||
vec![start..start],
|
||||
5,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let annotation = SharedString::from(excerpt.annotation);
|
||||
editor.insert_blocks(
|
||||
[BlockProperties {
|
||||
position: ranges[0].start,
|
||||
height: annotation.split('\n').count() as u8 + 1,
|
||||
style: BlockStyle::Fixed,
|
||||
render: Box::new(move |cx| Self::render_note_block(&annotation, cx)),
|
||||
disposition: BlockDisposition::Above,
|
||||
}],
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
if !this.added_editor_to_workspace {
|
||||
this.added_editor_to_workspace = true;
|
||||
this.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, cx);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_note_block(explanation: &SharedString, cx: &mut BlockContext) -> AnyElement {
|
||||
let anchor_x = cx.anchor_x;
|
||||
let gutter_width = cx.gutter_dimensions.width;
|
||||
|
||||
h_flex()
|
||||
.w_full()
|
||||
.py_2()
|
||||
.border_y_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_center()
|
||||
.w(gutter_width)
|
||||
.child(Icon::new(IconName::Ai).color(Color::Hint)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.ml(anchor_x - gutter_width)
|
||||
.child(explanation.clone()),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AnnotationResultView {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
if let Some(error) = &self.error {
|
||||
ui::Label::new(error.to_string()).into_any_element()
|
||||
} else {
|
||||
ui::Label::new(SharedString::from(format!(
|
||||
"Opened a buffer with {} excerpts",
|
||||
self.rendered_excerpt_count
|
||||
)))
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolOutput for AnnotationResultView {
|
||||
type Input = AnnotationInput;
|
||||
type SerializedState = Option<String>;
|
||||
|
||||
fn generate(&self, _: &mut ProjectContext, _: &mut ViewContext<Self>) -> String {
|
||||
if let Some(error) = &self.error {
|
||||
format!("Failed to create buffer: {error:?}")
|
||||
} else {
|
||||
format!(
|
||||
"opened {} excerpts in a buffer",
|
||||
self.rendered_excerpt_count
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn set_input(&mut self, mut input: Self::Input, cx: &mut ViewContext<Self>) {
|
||||
let editor = if let Some(editor) = &self.editor {
|
||||
editor.clone()
|
||||
} else {
|
||||
let multibuffer = cx.new_model(|_cx| {
|
||||
MultiBuffer::new(0, language::Capability::ReadWrite).with_title(String::new())
|
||||
});
|
||||
let editor = cx.new_view(|cx| {
|
||||
Editor::for_multibuffer(multibuffer.clone(), Some(self.project.clone()), cx)
|
||||
});
|
||||
|
||||
self.editor = Some(editor.clone());
|
||||
editor
|
||||
};
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.buffer().update(cx, |multibuffer, cx| {
|
||||
if multibuffer.title(cx) != input.title {
|
||||
multibuffer.set_title(input.title.clone(), cx);
|
||||
}
|
||||
});
|
||||
|
||||
self.pending_excerpt = input.excerpts.pop();
|
||||
for excerpt in input.excerpts.iter().skip(self.rendered_excerpt_count) {
|
||||
self.tx.unbounded_send(excerpt.clone()).ok();
|
||||
}
|
||||
self.rendered_excerpt_count = input.excerpts.len();
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn execute(&mut self, _cx: &mut ViewContext<Self>) -> Task<Result<()>> {
|
||||
if let Some(excerpt) = self.pending_excerpt.take() {
|
||||
self.rendered_excerpt_count += 1;
|
||||
self.tx.unbounded_send(excerpt.clone()).ok();
|
||||
}
|
||||
|
||||
self.tx.close_channel();
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn serialize(&self, _cx: &mut ViewContext<Self>) -> Self::SerializedState {
|
||||
self.error.as_ref().map(|error| error.to_string())
|
||||
}
|
||||
|
||||
fn deserialize(
|
||||
&mut self,
|
||||
output: Self::SerializedState,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> Result<()> {
|
||||
if let Some(error_message) = output {
|
||||
self.error = Some(anyhow::anyhow!("{}", error_message));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use assistant_tooling::LanguageModelTool;
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_tooling::{LanguageModelTool, ProjectContext, ToolOutput};
|
||||
use editor::Editor;
|
||||
use gpui::{prelude::*, Model, Task, View, WeakView};
|
||||
use project::Project;
|
||||
@@ -20,7 +20,7 @@ impl CreateBufferTool {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
#[derive(Debug, Clone, Deserialize, JsonSchema)]
|
||||
pub struct CreateBufferInput {
|
||||
/// The contents of the buffer.
|
||||
text: String,
|
||||
@@ -31,28 +31,70 @@ pub struct CreateBufferInput {
|
||||
language: String,
|
||||
}
|
||||
|
||||
pub struct CreateBufferOutput {}
|
||||
|
||||
impl LanguageModelTool for CreateBufferTool {
|
||||
type Input = CreateBufferInput;
|
||||
type Output = CreateBufferOutput;
|
||||
type View = CreateBufferView;
|
||||
|
||||
fn name(&self) -> String {
|
||||
"create_buffer".to_string()
|
||||
"create_new_source_file".to_string()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Create a new buffer in the current codebase".to_string()
|
||||
"Create a new file in the current codebase. Only use this when generating new code, NOT when showing existing code from the project.".to_string()
|
||||
}
|
||||
|
||||
fn execute(&self, input: &Self::Input, cx: &mut WindowContext) -> Task<Result<Self::Output>> {
|
||||
fn view(&self, cx: &mut WindowContext) -> View<Self::View> {
|
||||
cx.new_view(|_cx| CreateBufferView {
|
||||
workspace: self.workspace.clone(),
|
||||
project: self.project.clone(),
|
||||
input: None,
|
||||
error: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CreateBufferView {
|
||||
workspace: WeakView<Workspace>,
|
||||
project: Model<Project>,
|
||||
input: Option<CreateBufferInput>,
|
||||
error: Option<anyhow::Error>,
|
||||
}
|
||||
|
||||
impl Render for CreateBufferView {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div().child("Opening a buffer")
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolOutput for CreateBufferView {
|
||||
type Input = CreateBufferInput;
|
||||
|
||||
type SerializedState = ();
|
||||
|
||||
fn generate(&self, _project: &mut ProjectContext, _cx: &mut ViewContext<Self>) -> String {
|
||||
let Some(input) = self.input.as_ref() else {
|
||||
return "No input".to_string();
|
||||
};
|
||||
|
||||
match &self.error {
|
||||
None => format!("Created a new {} buffer", input.language),
|
||||
Some(err) => format!("Failed to create buffer: {err:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_input(&mut self, input: Self::Input, _cx: &mut ViewContext<Self>) {
|
||||
self.input = Some(input);
|
||||
}
|
||||
|
||||
fn execute(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
|
||||
cx.spawn({
|
||||
let workspace = self.workspace.clone();
|
||||
let project = self.project.clone();
|
||||
let text = input.text.clone();
|
||||
let language_name = input.language.clone();
|
||||
|mut cx| async move {
|
||||
let input = self.input.clone();
|
||||
|_this, mut cx| async move {
|
||||
let input = input.ok_or_else(|| anyhow!("no input"))?;
|
||||
|
||||
let text = input.text.clone();
|
||||
let language_name = input.language.clone();
|
||||
let language = cx
|
||||
.update(|cx| {
|
||||
project
|
||||
@@ -83,32 +125,20 @@ impl LanguageModelTool for CreateBufferTool {
|
||||
})
|
||||
.log_err();
|
||||
|
||||
Ok(CreateBufferOutput {})
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn format(input: &Self::Input, output: &Result<Self::Output>) -> String {
|
||||
match output {
|
||||
Ok(_) => format!("Created a new {} buffer", input.language),
|
||||
Err(err) => format!("Failed to create buffer: {err:?}"),
|
||||
}
|
||||
fn serialize(&self, _cx: &mut ViewContext<Self>) -> Self::SerializedState {
|
||||
()
|
||||
}
|
||||
|
||||
fn output_view(
|
||||
_tool_call_id: String,
|
||||
_input: Self::Input,
|
||||
_output: Result<Self::Output>,
|
||||
cx: &mut WindowContext,
|
||||
) -> View<Self::View> {
|
||||
cx.new_view(|_cx| CreateBufferView {})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CreateBufferView {}
|
||||
|
||||
impl Render for CreateBufferView {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div().child("Opening a buffer")
|
||||
fn deserialize(
|
||||
&mut self,
|
||||
_output: Self::SerializedState,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,40 @@
|
||||
use anyhow::Result;
|
||||
use assistant_tooling::LanguageModelTool;
|
||||
use gpui::{percentage, prelude::*, Animation, AnimationExt, AnyView, Model, Task, Transformation};
|
||||
use project::Fs;
|
||||
use assistant_tooling::{LanguageModelTool, ToolOutput};
|
||||
use collections::BTreeMap;
|
||||
use gpui::{prelude::*, Model, Task};
|
||||
use project::ProjectPath;
|
||||
use schemars::JsonSchema;
|
||||
use semantic_index::{ProjectIndex, Status};
|
||||
use serde::Deserialize;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use ui::{
|
||||
div, prelude::*, ButtonLike, CollapsibleContainer, Color, Icon, IconName, Indicator, Label,
|
||||
SharedString, Tooltip, WindowContext,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fmt::Write as _, ops::Range, path::Path, sync::Arc};
|
||||
use ui::{prelude::*, CollapsibleContainer, Color, Icon, IconName, Label, WindowContext};
|
||||
|
||||
const DEFAULT_SEARCH_LIMIT: usize = 20;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CodebaseExcerpt {
|
||||
path: SharedString,
|
||||
text: SharedString,
|
||||
score: f32,
|
||||
element_id: ElementId,
|
||||
expanded: bool,
|
||||
pub struct ProjectIndexTool {
|
||||
project_index: Model<ProjectIndex>,
|
||||
}
|
||||
|
||||
// Note: Comments on a `LanguageModelTool::Input` become descriptions on the generated JSON schema as shown to the language model.
|
||||
// Any changes or deletions to the `CodebaseQuery` comments will change model behavior.
|
||||
#[derive(Default)]
|
||||
enum ProjectIndexToolState {
|
||||
#[default]
|
||||
CollectingQuery,
|
||||
Searching,
|
||||
Error(anyhow::Error),
|
||||
Finished {
|
||||
excerpts: BTreeMap<ProjectPath, Vec<Range<usize>>>,
|
||||
index_status: Status,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
pub struct ProjectIndexView {
|
||||
project_index: Model<ProjectIndex>,
|
||||
input: CodebaseQuery,
|
||||
expanded_header: bool,
|
||||
state: ProjectIndexToolState,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, JsonSchema)]
|
||||
pub struct CodebaseQuery {
|
||||
/// Semantic search query
|
||||
query: String,
|
||||
@@ -34,23 +42,22 @@ pub struct CodebaseQuery {
|
||||
limit: Option<usize>,
|
||||
}
|
||||
|
||||
pub struct ProjectIndexView {
|
||||
input: CodebaseQuery,
|
||||
output: Result<ProjectIndexOutput>,
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SerializedState {
|
||||
index_status: Status,
|
||||
error_message: Option<String>,
|
||||
worktrees: BTreeMap<Arc<Path>, WorktreeIndexOutput>,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
struct WorktreeIndexOutput {
|
||||
excerpts: BTreeMap<Arc<Path>, Vec<Range<usize>>>,
|
||||
}
|
||||
|
||||
impl ProjectIndexView {
|
||||
fn toggle_expanded(&mut self, element_id: ElementId, cx: &mut ViewContext<Self>) {
|
||||
if let Ok(output) = &mut self.output {
|
||||
if let Some(excerpt) = output
|
||||
.excerpts
|
||||
.iter_mut()
|
||||
.find(|excerpt| excerpt.element_id == element_id)
|
||||
{
|
||||
excerpt.expanded = !excerpt.expanded;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
fn toggle_header(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.expanded_header = !self.expanded_header;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,76 +65,215 @@ impl Render for ProjectIndexView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let query = self.input.query.clone();
|
||||
|
||||
let result = &self.output;
|
||||
|
||||
let output = match result {
|
||||
Err(err) => {
|
||||
return div().child(Label::new(format!("Error: {}", err)).color(Color::Error));
|
||||
let (header_text, content) = match &self.state {
|
||||
ProjectIndexToolState::Error(error) => {
|
||||
return format!("failed to search: {error:?}").into_any_element()
|
||||
}
|
||||
ProjectIndexToolState::CollectingQuery | ProjectIndexToolState::Searching => {
|
||||
("Searching...".to_string(), div())
|
||||
}
|
||||
ProjectIndexToolState::Finished { excerpts, .. } => {
|
||||
let file_count = excerpts.len();
|
||||
|
||||
let header_text = format!(
|
||||
"Read {} {}",
|
||||
file_count,
|
||||
if file_count == 1 { "file" } else { "files" }
|
||||
);
|
||||
|
||||
let el = v_flex().gap_2().children(excerpts.keys().map(|path| {
|
||||
h_flex().gap_2().child(Icon::new(IconName::File)).child(
|
||||
Label::new(path.path.to_string_lossy().to_string()).color(Color::Muted),
|
||||
)
|
||||
}));
|
||||
|
||||
(header_text, el)
|
||||
}
|
||||
Ok(output) => output,
|
||||
};
|
||||
|
||||
div()
|
||||
.v_flex()
|
||||
let header = h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.p_2()
|
||||
.rounded_md()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(
|
||||
h_flex()
|
||||
.child(Label::new("Query: ").color(Color::Modified))
|
||||
.child(Label::new(query).color(Color::Muted)),
|
||||
),
|
||||
)
|
||||
.children(output.excerpts.iter().map(|excerpt| {
|
||||
let element_id = excerpt.element_id.clone();
|
||||
let expanded = excerpt.expanded;
|
||||
.child(Icon::new(IconName::File))
|
||||
.child(header_text);
|
||||
|
||||
CollapsibleContainer::new(element_id.clone(), expanded)
|
||||
.start_slot(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Icon::new(IconName::File).color(Color::Muted))
|
||||
.child(Label::new(excerpt.path.clone()).color(Color::Muted)),
|
||||
)
|
||||
v_flex()
|
||||
.gap_3()
|
||||
.child(
|
||||
CollapsibleContainer::new("collapsible-container", self.expanded_header)
|
||||
.start_slot(header)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.toggle_expanded(element_id.clone(), cx);
|
||||
this.toggle_header(cx);
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.p_2()
|
||||
.rounded_md()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(excerpt.text.clone()),
|
||||
)
|
||||
}))
|
||||
v_flex()
|
||||
.gap_3()
|
||||
.p_3()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Icon::new(IconName::MagnifyingGlass))
|
||||
.child(Label::new(format!("`{}`", query)).color(Color::Muted)),
|
||||
)
|
||||
.child(content),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ProjectIndexTool {
|
||||
project_index: Model<ProjectIndex>,
|
||||
fs: Arc<dyn Fs>,
|
||||
}
|
||||
impl ToolOutput for ProjectIndexView {
|
||||
type Input = CodebaseQuery;
|
||||
type SerializedState = SerializedState;
|
||||
|
||||
pub struct ProjectIndexOutput {
|
||||
excerpts: Vec<CodebaseExcerpt>,
|
||||
status: Status,
|
||||
fn generate(
|
||||
&self,
|
||||
context: &mut assistant_tooling::ProjectContext,
|
||||
_: &mut ViewContext<Self>,
|
||||
) -> String {
|
||||
match &self.state {
|
||||
ProjectIndexToolState::CollectingQuery => String::new(),
|
||||
ProjectIndexToolState::Searching => String::new(),
|
||||
ProjectIndexToolState::Error(error) => format!("failed to search: {error:?}"),
|
||||
ProjectIndexToolState::Finished {
|
||||
excerpts,
|
||||
index_status,
|
||||
} => {
|
||||
let mut body = "found results in the following paths:\n".to_string();
|
||||
|
||||
for (project_path, ranges) in excerpts {
|
||||
context.add_excerpts(project_path.clone(), ranges);
|
||||
writeln!(&mut body, "* {}", &project_path.path.display()).unwrap();
|
||||
}
|
||||
|
||||
if *index_status != Status::Idle {
|
||||
body.push_str("Still indexing. Results may be incomplete.\n");
|
||||
}
|
||||
|
||||
body
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_input(&mut self, input: Self::Input, cx: &mut ViewContext<Self>) {
|
||||
self.input = input;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn execute(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
|
||||
self.state = ProjectIndexToolState::Searching;
|
||||
cx.notify();
|
||||
|
||||
let project_index = self.project_index.read(cx);
|
||||
let index_status = project_index.status();
|
||||
let search = project_index.search(
|
||||
self.input.query.clone(),
|
||||
self.input.limit.unwrap_or(DEFAULT_SEARCH_LIMIT),
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let search_result = search.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
match search_result {
|
||||
Ok(search_results) => {
|
||||
let mut excerpts = BTreeMap::<ProjectPath, Vec<Range<usize>>>::new();
|
||||
for search_result in search_results {
|
||||
let project_path = ProjectPath {
|
||||
worktree_id: search_result.worktree.read(cx).id(),
|
||||
path: search_result.path,
|
||||
};
|
||||
excerpts
|
||||
.entry(project_path)
|
||||
.or_default()
|
||||
.push(search_result.range);
|
||||
}
|
||||
this.state = ProjectIndexToolState::Finished {
|
||||
excerpts,
|
||||
index_status,
|
||||
};
|
||||
}
|
||||
Err(error) => {
|
||||
this.state = ProjectIndexToolState::Error(error);
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn serialize(&self, cx: &mut ViewContext<Self>) -> Self::SerializedState {
|
||||
let mut serialized = SerializedState {
|
||||
error_message: None,
|
||||
index_status: Status::Idle,
|
||||
worktrees: Default::default(),
|
||||
};
|
||||
match &self.state {
|
||||
ProjectIndexToolState::Error(err) => serialized.error_message = Some(err.to_string()),
|
||||
ProjectIndexToolState::Finished {
|
||||
excerpts,
|
||||
index_status,
|
||||
} => {
|
||||
serialized.index_status = *index_status;
|
||||
if let Some(project) = self.project_index.read(cx).project().upgrade() {
|
||||
let project = project.read(cx);
|
||||
for (project_path, excerpts) in excerpts {
|
||||
if let Some(worktree) =
|
||||
project.worktree_for_id(project_path.worktree_id, cx)
|
||||
{
|
||||
let worktree_path = worktree.read(cx).abs_path();
|
||||
serialized
|
||||
.worktrees
|
||||
.entry(worktree_path)
|
||||
.or_default()
|
||||
.excerpts
|
||||
.insert(project_path.path.clone(), excerpts.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
serialized
|
||||
}
|
||||
|
||||
fn deserialize(
|
||||
&mut self,
|
||||
serialized: Self::SerializedState,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Result<()> {
|
||||
if !serialized.worktrees.is_empty() {
|
||||
let mut excerpts = BTreeMap::<ProjectPath, Vec<Range<usize>>>::new();
|
||||
if let Some(project) = self.project_index.read(cx).project().upgrade() {
|
||||
let project = project.read(cx);
|
||||
for (worktree_path, worktree_state) in serialized.worktrees {
|
||||
if let Some(worktree) = project
|
||||
.worktrees()
|
||||
.find(|worktree| worktree.read(cx).abs_path() == worktree_path)
|
||||
{
|
||||
let worktree_id = worktree.read(cx).id();
|
||||
for (path, serialized_excerpts) in worktree_state.excerpts {
|
||||
excerpts.insert(ProjectPath { worktree_id, path }, serialized_excerpts);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.state = ProjectIndexToolState::Finished {
|
||||
excerpts,
|
||||
index_status: serialized.index_status,
|
||||
};
|
||||
}
|
||||
cx.notify();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ProjectIndexTool {
|
||||
pub fn new(project_index: Model<ProjectIndex>, fs: Arc<dyn Fs>) -> Self {
|
||||
// Listen for project index status and update the ProjectIndexTool directly
|
||||
|
||||
// TODO: setup a better description based on the user's current codebase.
|
||||
Self { project_index, fs }
|
||||
pub fn new(project_index: Model<ProjectIndex>) -> Self {
|
||||
Self { project_index }
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageModelTool for ProjectIndexTool {
|
||||
type Input = CodebaseQuery;
|
||||
type Output = ProjectIndexOutput;
|
||||
type View = ProjectIndexView;
|
||||
|
||||
fn name(&self) -> String {
|
||||
@@ -135,183 +281,15 @@ impl LanguageModelTool for ProjectIndexTool {
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Semantic search against the user's current codebase, returning excerpts related to the query by computing a dot product against embeddings of chunks and an embedding of the query".to_string()
|
||||
"Semantic search against the user's current codebase, returning excerpts related to the query by computing a dot product against embeddings of code chunks in the code base and an embedding of the query.".to_string()
|
||||
}
|
||||
|
||||
fn execute(&self, query: &Self::Input, cx: &mut WindowContext) -> Task<Result<Self::Output>> {
|
||||
let project_index = self.project_index.read(cx);
|
||||
let status = project_index.status();
|
||||
let results = project_index.search(
|
||||
query.query.clone(),
|
||||
query.limit.unwrap_or(DEFAULT_SEARCH_LIMIT),
|
||||
cx,
|
||||
);
|
||||
|
||||
let fs = self.fs.clone();
|
||||
|
||||
cx.spawn(|cx| async move {
|
||||
let results = results.await?;
|
||||
|
||||
let excerpts = results.into_iter().map(|result| {
|
||||
let abs_path = result
|
||||
.worktree
|
||||
.read_with(&cx, |worktree, _| worktree.abs_path().join(&result.path));
|
||||
let fs = fs.clone();
|
||||
|
||||
async move {
|
||||
let path = result.path.clone();
|
||||
let text = fs.load(&abs_path?).await?;
|
||||
|
||||
let mut start = result.range.start;
|
||||
let mut end = result.range.end.min(text.len());
|
||||
while !text.is_char_boundary(start) {
|
||||
start += 1;
|
||||
}
|
||||
while !text.is_char_boundary(end) {
|
||||
end -= 1;
|
||||
}
|
||||
|
||||
anyhow::Ok(CodebaseExcerpt {
|
||||
element_id: ElementId::Name(nanoid::nanoid!().into()),
|
||||
expanded: false,
|
||||
path: path.to_string_lossy().to_string().into(),
|
||||
text: SharedString::from(text[start..end].to_string()),
|
||||
score: result.score,
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
let excerpts = futures::future::join_all(excerpts)
|
||||
.await
|
||||
.into_iter()
|
||||
.filter_map(|result| result.log_err())
|
||||
.collect();
|
||||
anyhow::Ok(ProjectIndexOutput { excerpts, status })
|
||||
fn view(&self, cx: &mut WindowContext) -> gpui::View<Self::View> {
|
||||
cx.new_view(|_| ProjectIndexView {
|
||||
state: ProjectIndexToolState::CollectingQuery,
|
||||
input: Default::default(),
|
||||
expanded_header: false,
|
||||
project_index: self.project_index.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
fn output_view(
|
||||
_tool_call_id: String,
|
||||
input: Self::Input,
|
||||
output: Result<Self::Output>,
|
||||
cx: &mut WindowContext,
|
||||
) -> gpui::View<Self::View> {
|
||||
cx.new_view(|_cx| ProjectIndexView { input, output })
|
||||
}
|
||||
|
||||
fn status_view(&self, cx: &mut WindowContext) -> Option<AnyView> {
|
||||
Some(
|
||||
cx.new_view(|cx| ProjectIndexStatusView::new(self.project_index.clone(), cx))
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
|
||||
fn format(_input: &Self::Input, output: &Result<Self::Output>) -> String {
|
||||
match &output {
|
||||
Ok(output) => {
|
||||
let mut body = "Semantic search results:\n".to_string();
|
||||
|
||||
if output.status != Status::Idle {
|
||||
body.push_str("Still indexing. Results may be incomplete.\n");
|
||||
}
|
||||
|
||||
if output.excerpts.is_empty() {
|
||||
body.push_str("No results found");
|
||||
return body;
|
||||
}
|
||||
|
||||
for excerpt in &output.excerpts {
|
||||
body.push_str("Excerpt from ");
|
||||
body.push_str(excerpt.path.as_ref());
|
||||
body.push_str(", score ");
|
||||
body.push_str(&excerpt.score.to_string());
|
||||
body.push_str(":\n");
|
||||
body.push_str("~~~\n");
|
||||
body.push_str(excerpt.text.as_ref());
|
||||
body.push_str("~~~\n");
|
||||
}
|
||||
body
|
||||
}
|
||||
Err(err) => format!("Error: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ProjectIndexStatusView {
|
||||
project_index: Model<ProjectIndex>,
|
||||
}
|
||||
|
||||
impl ProjectIndexStatusView {
|
||||
pub fn new(project_index: Model<ProjectIndex>, cx: &mut ViewContext<Self>) -> Self {
|
||||
cx.subscribe(&project_index, |_this, _, _status: &Status, cx| {
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
Self { project_index }
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ProjectIndexStatusView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let status = self.project_index.read(cx).status();
|
||||
|
||||
let is_enabled = match status {
|
||||
Status::Idle => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
let icon = match status {
|
||||
Status::Idle => Icon::new(IconName::Code)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Default),
|
||||
Status::Loading => Icon::new(IconName::Code)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
Status::Scanning { .. } => Icon::new(IconName::Code)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
};
|
||||
|
||||
let indicator = match status {
|
||||
Status::Idle => Some(Indicator::dot().color(Color::Success)),
|
||||
Status::Scanning { .. } => Some(Indicator::dot().color(Color::Warning)),
|
||||
Status::Loading => Some(Indicator::icon(
|
||||
Icon::new(IconName::Spinner)
|
||||
.color(Color::Accent)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
),
|
||||
)),
|
||||
};
|
||||
|
||||
ButtonLike::new("project-index")
|
||||
.disabled(!is_enabled)
|
||||
.child(
|
||||
ui::IconWithIndicator::new(icon, indicator)
|
||||
.indicator_border_color(Some(gpui::transparent_black())),
|
||||
)
|
||||
.tooltip({
|
||||
move |cx| {
|
||||
let (tooltip, meta) = match status {
|
||||
Status::Idle => (
|
||||
"Project index ready".to_string(),
|
||||
Some("Click to disable".to_string()),
|
||||
),
|
||||
Status::Loading => ("Project index loading...".to_string(), None),
|
||||
Status::Scanning { remaining_count } => (
|
||||
"Project index scanning...".to_string(),
|
||||
Some(format!("{} remaining...", remaining_count)),
|
||||
),
|
||||
};
|
||||
|
||||
if let Some(meta) = meta {
|
||||
Tooltip::with_meta(tooltip, None, meta, cx)
|
||||
} else {
|
||||
Tooltip::text(tooltip, cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
mod active_file_button;
|
||||
mod chat_message;
|
||||
mod chat_notice;
|
||||
mod composer;
|
||||
mod project_index_button;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories;
|
||||
|
||||
pub use active_file_button::*;
|
||||
pub use chat_message::*;
|
||||
pub use chat_notice::*;
|
||||
pub use composer::*;
|
||||
pub use project_index_button::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
134
crates/assistant2/src/ui/active_file_button.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
use crate::attachments::ActiveEditorAttachmentTool;
|
||||
use assistant_tooling::AttachmentRegistry;
|
||||
use editor::Editor;
|
||||
use gpui::{prelude::*, Subscription, View};
|
||||
use std::sync::Arc;
|
||||
use ui::{prelude::*, ButtonLike, Color, Icon, IconName, Tooltip};
|
||||
use workspace::Workspace;
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Status {
|
||||
ActiveFile(String),
|
||||
#[allow(dead_code)]
|
||||
NoFile,
|
||||
}
|
||||
|
||||
pub struct ActiveFileButton {
|
||||
attachment_registry: Arc<AttachmentRegistry>,
|
||||
status: Status,
|
||||
#[allow(dead_code)]
|
||||
workspace_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl ActiveFileButton {
|
||||
pub fn new(
|
||||
attachment_registry: Arc<AttachmentRegistry>,
|
||||
workspace: View<Workspace>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let workspace_subscription = cx.subscribe(&workspace, Self::handle_workspace_event);
|
||||
|
||||
cx.defer(move |this, cx| this.update_active_buffer(workspace.clone(), cx));
|
||||
|
||||
Self {
|
||||
attachment_registry,
|
||||
status: Status::NoFile,
|
||||
workspace_subscription,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_enabled(&mut self, enabled: bool) {
|
||||
self.attachment_registry
|
||||
.set_attachment_tool_enabled::<ActiveEditorAttachmentTool>(enabled);
|
||||
}
|
||||
|
||||
pub fn update_active_buffer(&mut self, workspace: View<Workspace>, cx: &mut ViewContext<Self>) {
|
||||
let active_buffer = workspace
|
||||
.read(cx)
|
||||
.active_item(cx)
|
||||
.and_then(|item| Some(item.act_as::<Editor>(cx)?.read(cx).buffer().clone()));
|
||||
|
||||
if let Some(buffer) = active_buffer {
|
||||
let buffer = buffer.read(cx);
|
||||
|
||||
if let Some(singleton) = buffer.as_singleton() {
|
||||
let singleton = singleton.read(cx);
|
||||
|
||||
let filename: String = singleton
|
||||
.file()
|
||||
.map(|file| file.path().to_string_lossy())
|
||||
.unwrap_or("Untitled".into())
|
||||
.into();
|
||||
|
||||
self.status = Status::ActiveFile(filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_workspace_event(
|
||||
&mut self,
|
||||
workspace: View<Workspace>,
|
||||
event: &workspace::Event,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let workspace::Event::ActiveItemChanged = event {
|
||||
self.update_active_buffer(workspace, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ActiveFileButton {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let is_enabled = self
|
||||
.attachment_registry
|
||||
.is_attachment_tool_enabled::<ActiveEditorAttachmentTool>();
|
||||
|
||||
let icon = if is_enabled {
|
||||
Icon::new(IconName::File)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Default)
|
||||
} else {
|
||||
Icon::new(IconName::File)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Disabled)
|
||||
};
|
||||
|
||||
let indicator = None;
|
||||
|
||||
let status = self.status.clone();
|
||||
|
||||
ButtonLike::new("active-file-button")
|
||||
.child(
|
||||
ui::IconWithIndicator::new(icon, indicator)
|
||||
.indicator_border_color(Some(gpui::transparent_black())),
|
||||
)
|
||||
.tooltip({
|
||||
move |cx| {
|
||||
let status = status.clone();
|
||||
let (tooltip, meta) = match (is_enabled, status) {
|
||||
(false, _) => (
|
||||
"Active file disabled".to_string(),
|
||||
Some("Click to enable".to_string()),
|
||||
),
|
||||
(true, Status::ActiveFile(filename)) => (
|
||||
format!("Active file {filename} enabled"),
|
||||
Some("Click to disable".to_string()),
|
||||
),
|
||||
(true, Status::NoFile) => {
|
||||
("No file active for conversation".to_string(), None)
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(meta) = meta {
|
||||
Tooltip::with_meta(tooltip, None, meta, cx)
|
||||
} else {
|
||||
Tooltip::text(tooltip, cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.set_enabled(!is_enabled);
|
||||
cx.notify();
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use client::User;
|
||||
use gpui::{AnyElement, ClickEvent};
|
||||
use ui::{prelude::*, Avatar};
|
||||
use gpui::{hsla, AnyElement, ClickEvent};
|
||||
use ui::{prelude::*, Avatar, Tooltip};
|
||||
|
||||
use crate::MessageId;
|
||||
|
||||
@@ -15,7 +15,8 @@ pub enum UserOrAssistant {
|
||||
pub struct ChatMessage {
|
||||
id: MessageId,
|
||||
player: UserOrAssistant,
|
||||
message: Option<AnyElement>,
|
||||
messages: Vec<AnyElement>,
|
||||
selected: bool,
|
||||
collapsed: bool,
|
||||
on_collapse_handle_click: Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>,
|
||||
}
|
||||
@@ -24,83 +25,44 @@ impl ChatMessage {
|
||||
pub fn new(
|
||||
id: MessageId,
|
||||
player: UserOrAssistant,
|
||||
message: Option<AnyElement>,
|
||||
messages: Vec<AnyElement>,
|
||||
collapsed: bool,
|
||||
on_collapse_handle_click: Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
player,
|
||||
message,
|
||||
messages,
|
||||
selected: false,
|
||||
collapsed,
|
||||
on_collapse_handle_click,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Selectable for ChatMessage {
|
||||
fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ChatMessage {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let collapse_handle_id = SharedString::from(format!("{}_collapse_handle", self.id.0));
|
||||
let collapse_handle = h_flex()
|
||||
.id(collapse_handle_id.clone())
|
||||
.group(collapse_handle_id.clone())
|
||||
.flex_none()
|
||||
.justify_center()
|
||||
.w_1()
|
||||
.mx_2()
|
||||
.h_full()
|
||||
.on_click(self.on_collapse_handle_click)
|
||||
.child(
|
||||
div()
|
||||
.w_px()
|
||||
.h_full()
|
||||
.rounded_lg()
|
||||
.overflow_hidden()
|
||||
.bg(cx.theme().colors().element_background)
|
||||
.group_hover(collapse_handle_id, |this| {
|
||||
this.bg(cx.theme().colors().element_hover)
|
||||
}),
|
||||
);
|
||||
let message_group = SharedString::from(format!("{}_group", self.id.0));
|
||||
|
||||
let content_padding = rems(1.);
|
||||
let collapse_handle_id = SharedString::from(format!("{}_collapse_handle", self.id.0));
|
||||
|
||||
let content_padding = Spacing::Small.rems(cx);
|
||||
// Clamp the message height to exactly 1.5 lines when collapsed.
|
||||
let collapsed_height = content_padding.to_pixels(cx.rem_size()) + cx.line_height() * 1.5;
|
||||
|
||||
let content = self.message.map(|message| {
|
||||
div()
|
||||
.overflow_hidden()
|
||||
.w_full()
|
||||
.p(content_padding)
|
||||
.rounded_lg()
|
||||
.when(self.collapsed, |this| this.h(collapsed_height))
|
||||
.bg(cx.theme().colors().surface_background)
|
||||
.child(message)
|
||||
});
|
||||
let background_color = if let UserOrAssistant::User(_) = &self.player {
|
||||
Some(cx.theme().colors().surface_background)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(ChatMessageHeader::new(self.player))
|
||||
.child(h_flex().gap_3().child(collapse_handle).children(content))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct ChatMessageHeader {
|
||||
player: UserOrAssistant,
|
||||
contexts: Vec<()>,
|
||||
}
|
||||
|
||||
impl ChatMessageHeader {
|
||||
fn new(player: UserOrAssistant) -> Self {
|
||||
Self {
|
||||
player,
|
||||
contexts: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ChatMessageHeader {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
let (username, avatar_uri) = match self.player {
|
||||
UserOrAssistant::Assistant => (
|
||||
"Assistant".into(),
|
||||
@@ -112,23 +74,67 @@ impl RenderOnce for ChatMessageHeader {
|
||||
UserOrAssistant::User(None) => ("You".into(), None),
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.justify_between()
|
||||
v_flex()
|
||||
.group(message_group.clone())
|
||||
.gap(Spacing::XSmall.rems(cx))
|
||||
.p(Spacing::XSmall.rems(cx))
|
||||
.when(self.selected, |element| {
|
||||
element.bg(hsla(0.6, 0.67, 0.46, 0.12))
|
||||
})
|
||||
.rounded_lg()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_3()
|
||||
.map(|this| {
|
||||
let avatar_size = rems_from_px(20.);
|
||||
if let Some(avatar_uri) = avatar_uri {
|
||||
this.child(Avatar::new(avatar_uri).size(avatar_size))
|
||||
} else {
|
||||
this.child(div().size(avatar_size))
|
||||
}
|
||||
})
|
||||
.child(Label::new(username).color(Color::Default)),
|
||||
.justify_between()
|
||||
.px(content_padding)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.map(|this| {
|
||||
let avatar_size = rems_from_px(20.);
|
||||
if let Some(avatar_uri) = avatar_uri {
|
||||
this.child(Avatar::new(avatar_uri).size(avatar_size))
|
||||
} else {
|
||||
this.child(div().size(avatar_size))
|
||||
}
|
||||
})
|
||||
.child(Label::new(username).color(Color::Muted)),
|
||||
)
|
||||
.child(
|
||||
h_flex().visible_on_hover(message_group).child(
|
||||
// temp icons
|
||||
IconButton::new(
|
||||
collapse_handle_id.clone(),
|
||||
if self.collapsed {
|
||||
IconName::ArrowUp
|
||||
} else {
|
||||
IconName::ArrowDown
|
||||
},
|
||||
)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(self.on_collapse_handle_click)
|
||||
.tooltip(|cx| Tooltip::text("Collapse Message", cx)),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(div().when(!self.contexts.is_empty(), |this| {
|
||||
this.child(Label::new(self.contexts.len().to_string()).color(Color::Muted))
|
||||
}))
|
||||
.when(self.messages.len() > 0, |el| {
|
||||
el.child(
|
||||
h_flex().child(
|
||||
v_flex()
|
||||
.relative()
|
||||
.overflow_hidden()
|
||||
.w_full()
|
||||
.p(content_padding)
|
||||
.gap_3()
|
||||
.text_ui(cx)
|
||||
.rounded_lg()
|
||||
.when_some(background_color, |this, background_color| {
|
||||
this.bg(background_color)
|
||||
})
|
||||
.when(self.collapsed, |this| this.h(collapsed_height))
|
||||
.children(self.messages),
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,107 +1,120 @@
|
||||
use assistant_tooling::ToolRegistry;
|
||||
use client::User;
|
||||
use crate::{
|
||||
ui::{ActiveFileButton, ProjectIndexButton},
|
||||
AssistantChat, CompletionProvider,
|
||||
};
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use gpui::{AnyElement, FontStyle, FontWeight, TextStyle, View, WeakView, WhiteSpace};
|
||||
use settings::Settings;
|
||||
use std::sync::Arc;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{popover_menu, prelude::*, Avatar, ButtonLike, ContextMenu, Tooltip};
|
||||
|
||||
use crate::{AssistantChat, CompletionProvider};
|
||||
use ui::{popover_menu, prelude::*, ButtonLike, ContextMenu, Divider, TextSize, Tooltip};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Composer {
|
||||
editor: View<Editor>,
|
||||
player: Option<Arc<User>>,
|
||||
tool_registry: Arc<ToolRegistry>,
|
||||
project_index_button: View<ProjectIndexButton>,
|
||||
active_file_button: Option<View<ActiveFileButton>>,
|
||||
model_selector: AnyElement,
|
||||
}
|
||||
|
||||
impl Composer {
|
||||
pub fn new(
|
||||
editor: View<Editor>,
|
||||
player: Option<Arc<User>>,
|
||||
tool_registry: Arc<ToolRegistry>,
|
||||
project_index_button: View<ProjectIndexButton>,
|
||||
active_file_button: Option<View<ActiveFileButton>>,
|
||||
model_selector: AnyElement,
|
||||
) -> Self {
|
||||
Self {
|
||||
editor,
|
||||
player,
|
||||
tool_registry,
|
||||
project_index_button,
|
||||
active_file_button,
|
||||
model_selector,
|
||||
}
|
||||
}
|
||||
|
||||
fn render_tools(&mut self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
h_flex().child(self.project_index_button.clone())
|
||||
}
|
||||
|
||||
fn render_attachment_tools(&mut self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
h_flex().children(
|
||||
self.active_file_button
|
||||
.clone()
|
||||
.map(|view| view.into_any_element()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Composer {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let mut player_avatar = div().size(rems_from_px(20.)).into_any_element();
|
||||
if let Some(player) = self.player.clone() {
|
||||
player_avatar = Avatar::new(player.avatar_uri.clone())
|
||||
.size(rems_from_px(20.))
|
||||
.into_any_element();
|
||||
}
|
||||
|
||||
let font_size = rems(0.875);
|
||||
fn render(mut self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let font_size = TextSize::Default.rems(cx);
|
||||
let line_height = font_size.to_pixels(cx.rem_size()) * 1.3;
|
||||
let mut editor_border = cx.theme().colors().text;
|
||||
editor_border.fade_out(0.90);
|
||||
|
||||
// Remove the extra 1px added by the border
|
||||
let padding = Spacing::XLarge.rems(cx) - rems_from_px(1.);
|
||||
|
||||
h_flex()
|
||||
.p(Spacing::Small.rems(cx))
|
||||
.w_full()
|
||||
.items_start()
|
||||
.mt_4()
|
||||
.gap_3()
|
||||
.child(player_avatar)
|
||||
.child(
|
||||
v_flex().size_full().gap_1().child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.p_4()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded_lg()
|
||||
.child(
|
||||
v_flex()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.child({
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().editor_foreground,
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_size: font_size.into(),
|
||||
font_weight: FontWeight::NORMAL,
|
||||
font_style: FontStyle::Normal,
|
||||
line_height: line_height.into(),
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
white_space: WhiteSpace::Normal,
|
||||
};
|
||||
v_flex()
|
||||
.w_full()
|
||||
.rounded_lg()
|
||||
.p(padding)
|
||||
.border_1()
|
||||
.border_color(editor_border)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(
|
||||
v_flex()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.child({
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().editor_foreground,
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_size: font_size.into(),
|
||||
font_weight: FontWeight::NORMAL,
|
||||
font_style: FontStyle::Normal,
|
||||
line_height: line_height.into(),
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
white_space: WhiteSpace::Normal,
|
||||
};
|
||||
|
||||
EditorElement::new(
|
||||
&self.editor,
|
||||
EditorStyle {
|
||||
background: cx.theme().colors().editor_background,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
},
|
||||
EditorElement::new(
|
||||
&self.editor,
|
||||
EditorStyle {
|
||||
background: cx.theme().colors().editor_background,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_none()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.child(
|
||||
h_flex().gap_1().child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(self.render_tools(cx))
|
||||
.child(Divider::vertical())
|
||||
.child(self.render_attachment_tools(cx)),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_none()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.child(h_flex().gap_1().children(
|
||||
self.tool_registry.status_views().iter().cloned(),
|
||||
))
|
||||
.child(h_flex().gap_1().child(self.model_selector)),
|
||||
),
|
||||
),
|
||||
),
|
||||
.child(h_flex().gap_1().child(self.model_selector)),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -136,7 +149,7 @@ impl RenderOnce for ModelSelector {
|
||||
let assistant_chat = self.assistant_chat.clone();
|
||||
move |cx| {
|
||||
_ = assistant_chat.update(cx, |assistant_chat, cx| {
|
||||
assistant_chat.model = model.clone();
|
||||
assistant_chat.model.clone_from(&model);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
|
||||
112
crates/assistant2/src/ui/project_index_button.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
use assistant_tooling::ToolRegistry;
|
||||
use gpui::{percentage, prelude::*, Animation, AnimationExt, Model, Transformation};
|
||||
use semantic_index::{ProjectIndex, Status};
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use ui::{prelude::*, ButtonLike, Color, Icon, IconName, Indicator, Tooltip};
|
||||
|
||||
use crate::tools::ProjectIndexTool;
|
||||
|
||||
pub struct ProjectIndexButton {
|
||||
project_index: Model<ProjectIndex>,
|
||||
tool_registry: Arc<ToolRegistry>,
|
||||
}
|
||||
|
||||
impl ProjectIndexButton {
|
||||
pub fn new(
|
||||
project_index: Model<ProjectIndex>,
|
||||
tool_registry: Arc<ToolRegistry>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
cx.subscribe(&project_index, |_this, _, _status: &Status, cx| {
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
Self {
|
||||
project_index,
|
||||
tool_registry,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_enabled(&mut self, enabled: bool) {
|
||||
self.tool_registry
|
||||
.set_tool_enabled::<ProjectIndexTool>(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ProjectIndexButton {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let status = self.project_index.read(cx).status();
|
||||
let is_enabled = self.tool_registry.is_tool_enabled::<ProjectIndexTool>();
|
||||
|
||||
let icon = if is_enabled {
|
||||
match status {
|
||||
Status::Idle => Icon::new(IconName::Code)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Default),
|
||||
Status::Loading => Icon::new(IconName::Code)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
Status::Scanning { .. } => Icon::new(IconName::Code)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
}
|
||||
} else {
|
||||
Icon::new(IconName::Code)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Disabled)
|
||||
};
|
||||
|
||||
let indicator = if is_enabled {
|
||||
match status {
|
||||
Status::Idle => Some(Indicator::dot().color(Color::Success)),
|
||||
Status::Scanning { .. } => Some(Indicator::dot().color(Color::Warning)),
|
||||
Status::Loading => Some(Indicator::icon(
|
||||
Icon::new(IconName::Spinner)
|
||||
.color(Color::Accent)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
),
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
ButtonLike::new("project-index")
|
||||
.child(
|
||||
ui::IconWithIndicator::new(icon, indicator)
|
||||
.indicator_border_color(Some(gpui::transparent_black())),
|
||||
)
|
||||
.tooltip({
|
||||
move |cx| {
|
||||
let (tooltip, meta) = match (is_enabled, status) {
|
||||
(false, _) => (
|
||||
"Project index disabled".to_string(),
|
||||
Some("Click to enable".to_string()),
|
||||
),
|
||||
(_, Status::Idle) => (
|
||||
"Project index ready".to_string(),
|
||||
Some("Click to disable".to_string()),
|
||||
),
|
||||
(_, Status::Loading) => ("Project index loading...".to_string(), None),
|
||||
(_, Status::Scanning { remaining_count }) => (
|
||||
"Project index scanning...".to_string(),
|
||||
Some(format!("{} remaining...", remaining_count)),
|
||||
),
|
||||
};
|
||||
|
||||
if let Some(meta) = meta {
|
||||
Tooltip::with_meta(tooltip, None, meta, cx)
|
||||
} else {
|
||||
Tooltip::text(tooltip, cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.set_enabled(!is_enabled);
|
||||
cx.notify();
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ impl Render for ChatMessageStory {
|
||||
ChatMessage::new(
|
||||
MessageId(0),
|
||||
UserOrAssistant::User(Some(user_1.clone())),
|
||||
Some(div().child("What can I do here?").into_any_element()),
|
||||
vec![div().child("What can I do here?").into_any_element()],
|
||||
false,
|
||||
Box::new(|_, _| {}),
|
||||
),
|
||||
@@ -38,7 +38,7 @@ impl Render for ChatMessageStory {
|
||||
ChatMessage::new(
|
||||
MessageId(0),
|
||||
UserOrAssistant::User(Some(user_1.clone())),
|
||||
Some(div().child("What can I do here?").into_any_element()),
|
||||
vec![div().child("What can I do here?").into_any_element()],
|
||||
true,
|
||||
Box::new(|_, _| {}),
|
||||
),
|
||||
@@ -51,7 +51,7 @@ impl Render for ChatMessageStory {
|
||||
ChatMessage::new(
|
||||
MessageId(0),
|
||||
UserOrAssistant::Assistant,
|
||||
Some(div().child("You can talk to me!").into_any_element()),
|
||||
vec![div().child("You can talk to me!").into_any_element()],
|
||||
false,
|
||||
Box::new(|_, _| {}),
|
||||
),
|
||||
@@ -61,7 +61,7 @@ impl Render for ChatMessageStory {
|
||||
ChatMessage::new(
|
||||
MessageId(0),
|
||||
UserOrAssistant::Assistant,
|
||||
Some(div().child(MULTI_LINE_MESSAGE).into_any_element()),
|
||||
vec![div().child(MULTI_LINE_MESSAGE).into_any_element()],
|
||||
true,
|
||||
Box::new(|_, _| {}),
|
||||
),
|
||||
@@ -75,21 +75,21 @@ impl Render for ChatMessageStory {
|
||||
.child(ChatMessage::new(
|
||||
MessageId(0),
|
||||
UserOrAssistant::User(Some(user_1.clone())),
|
||||
Some(div().child("What is Rust??").into_any_element()),
|
||||
vec![div().child("What is Rust??").into_any_element()],
|
||||
false,
|
||||
Box::new(|_, _| {}),
|
||||
))
|
||||
.child(ChatMessage::new(
|
||||
MessageId(0),
|
||||
UserOrAssistant::Assistant,
|
||||
Some(div().child("Rust is a multi-paradigm programming language focused on performance and safety").into_any_element()),
|
||||
vec![div().child("Rust is a multi-paradigm programming language focused on performance and safety").into_any_element()],
|
||||
false,
|
||||
Box::new(|_, _| {}),
|
||||
))
|
||||
.child(ChatMessage::new(
|
||||
MessageId(0),
|
||||
UserOrAssistant::User(Some(user_1)),
|
||||
Some(div().child("Sounds pretty cool!").into_any_element()),
|
||||
vec![div().child("Sounds pretty cool!").into_any_element()],
|
||||
false,
|
||||
Box::new(|_, _| {}),
|
||||
)),
|
||||
|
||||
@@ -13,10 +13,21 @@ path = "src/assistant_tooling.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
log.workspace = true
|
||||
project.workspace = true
|
||||
repair_json.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sum_tree.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
unindent.workspace = true
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
pub mod registry;
|
||||
pub mod tool;
|
||||
mod attachment_registry;
|
||||
mod project_context;
|
||||
mod tool_registry;
|
||||
|
||||
pub use crate::registry::ToolRegistry;
|
||||
pub use crate::tool::{LanguageModelTool, ToolFunctionCall, ToolFunctionDefinition};
|
||||
pub use attachment_registry::{
|
||||
AttachmentOutput, AttachmentRegistry, LanguageModelAttachment, SavedUserAttachment,
|
||||
UserAttachment,
|
||||
};
|
||||
pub use project_context::ProjectContext;
|
||||
pub use tool_registry::{
|
||||
LanguageModelTool, SavedToolFunctionCall, ToolFunctionCall, ToolFunctionDefinition, ToolOutput,
|
||||
ToolRegistry,
|
||||
};
|
||||
|
||||
234
crates/assistant_tooling/src/attachment_registry.rs
Normal file
@@ -0,0 +1,234 @@
|
||||
use crate::ProjectContext;
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::HashMap;
|
||||
use futures::future::join_all;
|
||||
use gpui::{AnyView, Render, Task, View, WindowContext};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use serde_json::value::RawValue;
|
||||
use std::{
|
||||
any::TypeId,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering::SeqCst},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
|
||||
pub struct AttachmentRegistry {
|
||||
registered_attachments: HashMap<TypeId, RegisteredAttachment>,
|
||||
}
|
||||
|
||||
pub trait AttachmentOutput {
|
||||
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String;
|
||||
}
|
||||
|
||||
pub trait LanguageModelAttachment {
|
||||
type Output: DeserializeOwned + Serialize + 'static;
|
||||
type View: Render + AttachmentOutput;
|
||||
|
||||
fn name(&self) -> Arc<str>;
|
||||
fn run(&self, cx: &mut WindowContext) -> Task<Result<Self::Output>>;
|
||||
fn view(&self, output: Result<Self::Output>, cx: &mut WindowContext) -> View<Self::View>;
|
||||
}
|
||||
|
||||
/// A collected attachment from running an attachment tool
|
||||
pub struct UserAttachment {
|
||||
pub view: AnyView,
|
||||
name: Arc<str>,
|
||||
serialized_output: Result<Box<RawValue>, String>,
|
||||
generate_fn: fn(AnyView, &mut ProjectContext, cx: &mut WindowContext) -> String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedUserAttachment {
|
||||
name: Arc<str>,
|
||||
serialized_output: Result<Box<RawValue>, String>,
|
||||
}
|
||||
|
||||
/// Internal representation of an attachment tool to allow us to treat them dynamically
|
||||
struct RegisteredAttachment {
|
||||
name: Arc<str>,
|
||||
enabled: AtomicBool,
|
||||
call: Box<dyn Fn(&mut WindowContext) -> Task<Result<UserAttachment>>>,
|
||||
deserialize: Box<dyn Fn(&SavedUserAttachment, &mut WindowContext) -> Result<UserAttachment>>,
|
||||
}
|
||||
|
||||
impl AttachmentRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
registered_attachments: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register<A: LanguageModelAttachment + 'static>(&mut self, attachment: A) {
|
||||
let attachment = Arc::new(attachment);
|
||||
|
||||
let call = Box::new({
|
||||
let attachment = attachment.clone();
|
||||
move |cx: &mut WindowContext| {
|
||||
let result = attachment.run(cx);
|
||||
let attachment = attachment.clone();
|
||||
cx.spawn(move |mut cx| async move {
|
||||
let result: Result<A::Output> = result.await;
|
||||
let serialized_output =
|
||||
result
|
||||
.as_ref()
|
||||
.map_err(ToString::to_string)
|
||||
.and_then(|output| {
|
||||
Ok(RawValue::from_string(
|
||||
serde_json::to_string(output).map_err(|e| e.to_string())?,
|
||||
)
|
||||
.unwrap())
|
||||
});
|
||||
|
||||
let view = cx.update(|cx| attachment.view(result, cx))?;
|
||||
|
||||
Ok(UserAttachment {
|
||||
name: attachment.name(),
|
||||
view: view.into(),
|
||||
generate_fn: generate::<A>,
|
||||
serialized_output,
|
||||
})
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
let deserialize = Box::new({
|
||||
let attachment = attachment.clone();
|
||||
move |saved_attachment: &SavedUserAttachment, cx: &mut WindowContext| {
|
||||
let serialized_output = saved_attachment.serialized_output.clone();
|
||||
let output = match &serialized_output {
|
||||
Ok(serialized_output) => {
|
||||
Ok(serde_json::from_str::<A::Output>(serialized_output.get())?)
|
||||
}
|
||||
Err(error) => Err(anyhow!("{error}")),
|
||||
};
|
||||
let view = attachment.view(output, cx).into();
|
||||
|
||||
Ok(UserAttachment {
|
||||
name: saved_attachment.name.clone(),
|
||||
view,
|
||||
serialized_output,
|
||||
generate_fn: generate::<A>,
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
self.registered_attachments.insert(
|
||||
TypeId::of::<A>(),
|
||||
RegisteredAttachment {
|
||||
name: attachment.name(),
|
||||
call,
|
||||
deserialize,
|
||||
enabled: AtomicBool::new(true),
|
||||
},
|
||||
);
|
||||
return;
|
||||
|
||||
fn generate<T: LanguageModelAttachment>(
|
||||
view: AnyView,
|
||||
project: &mut ProjectContext,
|
||||
cx: &mut WindowContext,
|
||||
) -> String {
|
||||
view.downcast::<T::View>()
|
||||
.unwrap()
|
||||
.update(cx, |view, cx| T::View::generate(view, project, cx))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_attachment_tool_enabled<A: LanguageModelAttachment + 'static>(
|
||||
&self,
|
||||
is_enabled: bool,
|
||||
) {
|
||||
if let Some(attachment) = self.registered_attachments.get(&TypeId::of::<A>()) {
|
||||
attachment.enabled.store(is_enabled, SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_attachment_tool_enabled<A: LanguageModelAttachment + 'static>(&self) -> bool {
|
||||
if let Some(attachment) = self.registered_attachments.get(&TypeId::of::<A>()) {
|
||||
attachment.enabled.load(SeqCst)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn call<A: LanguageModelAttachment + 'static>(
|
||||
&self,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<UserAttachment>> {
|
||||
let Some(attachment) = self.registered_attachments.get(&TypeId::of::<A>()) else {
|
||||
return Task::ready(Err(anyhow!("no attachment tool")));
|
||||
};
|
||||
|
||||
(attachment.call)(cx)
|
||||
}
|
||||
|
||||
pub fn call_all_attachment_tools(
|
||||
self: Arc<Self>,
|
||||
cx: &mut WindowContext<'_>,
|
||||
) -> Task<Result<Vec<UserAttachment>>> {
|
||||
let this = self.clone();
|
||||
cx.spawn(|mut cx| async move {
|
||||
let attachment_tasks = cx.update(|cx| {
|
||||
let mut tasks = Vec::new();
|
||||
for attachment in this
|
||||
.registered_attachments
|
||||
.values()
|
||||
.filter(|attachment| attachment.enabled.load(SeqCst))
|
||||
{
|
||||
tasks.push((attachment.call)(cx))
|
||||
}
|
||||
|
||||
tasks
|
||||
})?;
|
||||
|
||||
let attachments = join_all(attachment_tasks.into_iter()).await;
|
||||
|
||||
Ok(attachments
|
||||
.into_iter()
|
||||
.filter_map(|attachment| attachment.log_err())
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn serialize_user_attachment(
|
||||
&self,
|
||||
user_attachment: &UserAttachment,
|
||||
) -> SavedUserAttachment {
|
||||
SavedUserAttachment {
|
||||
name: user_attachment.name.clone(),
|
||||
serialized_output: user_attachment.serialized_output.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize_user_attachment(
|
||||
&self,
|
||||
saved_user_attachment: SavedUserAttachment,
|
||||
cx: &mut WindowContext,
|
||||
) -> Result<UserAttachment> {
|
||||
if let Some(registered_attachment) = self
|
||||
.registered_attachments
|
||||
.values()
|
||||
.find(|attachment| attachment.name == saved_user_attachment.name)
|
||||
{
|
||||
(registered_attachment.deserialize)(&saved_user_attachment, cx)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"no attachment tool for name {}",
|
||||
saved_user_attachment.name
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UserAttachment {
|
||||
pub fn generate(&self, output: &mut ProjectContext, cx: &mut WindowContext) -> Option<String> {
|
||||
let result = (self.generate_fn)(self.view.clone(), output, cx);
|
||||
if result.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
296
crates/assistant_tooling/src/project_context.rs
Normal file
@@ -0,0 +1,296 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use gpui::{AppContext, Model, Task, WeakModel};
|
||||
use project::{Fs, Project, ProjectPath, Worktree};
|
||||
use std::{cmp::Ordering, fmt::Write as _, ops::Range, sync::Arc};
|
||||
use sum_tree::TreeMap;
|
||||
|
||||
pub struct ProjectContext {
|
||||
files: TreeMap<ProjectPath, PathState>,
|
||||
project: WeakModel<Project>,
|
||||
fs: Arc<dyn Fs>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum PathState {
|
||||
PathOnly,
|
||||
EntireFile,
|
||||
Excerpts { ranges: Vec<Range<usize>> },
|
||||
}
|
||||
|
||||
impl ProjectContext {
|
||||
pub fn new(project: WeakModel<Project>, fs: Arc<dyn Fs>) -> Self {
|
||||
Self {
|
||||
files: TreeMap::default(),
|
||||
fs,
|
||||
project,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_path(&mut self, project_path: ProjectPath) {
|
||||
if self.files.get(&project_path).is_none() {
|
||||
self.files.insert(project_path, PathState::PathOnly);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_excerpts(&mut self, project_path: ProjectPath, new_ranges: &[Range<usize>]) {
|
||||
let previous_state = self
|
||||
.files
|
||||
.get(&project_path)
|
||||
.unwrap_or(&PathState::PathOnly);
|
||||
|
||||
let mut ranges = match previous_state {
|
||||
PathState::EntireFile => return,
|
||||
PathState::PathOnly => Vec::new(),
|
||||
PathState::Excerpts { ranges } => ranges.to_vec(),
|
||||
};
|
||||
|
||||
for new_range in new_ranges {
|
||||
let ix = ranges.binary_search_by(|probe| {
|
||||
if probe.end < new_range.start {
|
||||
Ordering::Less
|
||||
} else if probe.start > new_range.end {
|
||||
Ordering::Greater
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
});
|
||||
|
||||
match ix {
|
||||
Ok(mut ix) => {
|
||||
let existing = &mut ranges[ix];
|
||||
existing.start = existing.start.min(new_range.start);
|
||||
existing.end = existing.end.max(new_range.end);
|
||||
while ix + 1 < ranges.len() && ranges[ix + 1].start <= ranges[ix].end {
|
||||
ranges[ix].end = ranges[ix].end.max(ranges[ix + 1].end);
|
||||
ranges.remove(ix + 1);
|
||||
}
|
||||
while ix > 0 && ranges[ix - 1].end >= ranges[ix].start {
|
||||
ranges[ix].start = ranges[ix].start.min(ranges[ix - 1].start);
|
||||
ranges.remove(ix - 1);
|
||||
ix -= 1;
|
||||
}
|
||||
}
|
||||
Err(ix) => {
|
||||
ranges.insert(ix, new_range.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.files
|
||||
.insert(project_path, PathState::Excerpts { ranges });
|
||||
}
|
||||
|
||||
pub fn add_file(&mut self, project_path: ProjectPath) {
|
||||
self.files.insert(project_path, PathState::EntireFile);
|
||||
}
|
||||
|
||||
pub fn generate_system_message(&self, cx: &mut AppContext) -> Task<Result<String>> {
|
||||
let project = self
|
||||
.project
|
||||
.upgrade()
|
||||
.ok_or_else(|| anyhow!("project dropped"));
|
||||
let files = self.files.clone();
|
||||
let fs = self.fs.clone();
|
||||
cx.spawn(|cx| async move {
|
||||
let project = project?;
|
||||
let mut result = "project structure:\n".to_string();
|
||||
|
||||
let mut last_worktree: Option<Model<Worktree>> = None;
|
||||
for (project_path, path_state) in files.iter() {
|
||||
if let Some(worktree) = &last_worktree {
|
||||
if worktree.read_with(&cx, |tree, _| tree.id())? != project_path.worktree_id {
|
||||
last_worktree = None;
|
||||
}
|
||||
}
|
||||
|
||||
let worktree;
|
||||
if let Some(last_worktree) = &last_worktree {
|
||||
worktree = last_worktree.clone();
|
||||
} else if let Some(tree) = project.read_with(&cx, |project, cx| {
|
||||
project.worktree_for_id(project_path.worktree_id, cx)
|
||||
})? {
|
||||
worktree = tree;
|
||||
last_worktree = Some(worktree.clone());
|
||||
let worktree_name =
|
||||
worktree.read_with(&cx, |tree, _cx| tree.root_name().to_string())?;
|
||||
writeln!(&mut result, "# {}", worktree_name).unwrap();
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
let worktree_abs_path = worktree.read_with(&cx, |tree, _cx| tree.abs_path())?;
|
||||
let path = &project_path.path;
|
||||
writeln!(&mut result, "## {}", path.display()).unwrap();
|
||||
|
||||
match path_state {
|
||||
PathState::PathOnly => {}
|
||||
PathState::EntireFile => {
|
||||
let text = fs.load(&worktree_abs_path.join(&path)).await?;
|
||||
writeln!(&mut result, "~~~\n{text}\n~~~").unwrap();
|
||||
}
|
||||
PathState::Excerpts { ranges } => {
|
||||
let text = fs.load(&worktree_abs_path.join(&path)).await?;
|
||||
|
||||
writeln!(&mut result, "~~~").unwrap();
|
||||
|
||||
// Assumption: ranges are in order, not overlapping
|
||||
let mut prev_range_end = 0;
|
||||
for range in ranges {
|
||||
if range.start > prev_range_end {
|
||||
writeln!(&mut result, "...").unwrap();
|
||||
prev_range_end = range.end;
|
||||
}
|
||||
|
||||
let mut start = range.start;
|
||||
let mut end = range.end.min(text.len());
|
||||
while !text.is_char_boundary(start) {
|
||||
start += 1;
|
||||
}
|
||||
while !text.is_char_boundary(end) {
|
||||
end -= 1;
|
||||
}
|
||||
result.push_str(&text[start..end]);
|
||||
if !result.ends_with('\n') {
|
||||
result.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
if prev_range_end < text.len() {
|
||||
writeln!(&mut result, "...").unwrap();
|
||||
}
|
||||
|
||||
writeln!(&mut result, "~~~").unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use project::FakeFs;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
|
||||
use unindent::Unindent as _;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_system_message_generation(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let file_3_contents = r#"
|
||||
fn test1() {}
|
||||
fn test2() {}
|
||||
fn test3() {}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/code",
|
||||
json!({
|
||||
"root1": {
|
||||
"lib": {
|
||||
"file1.rs": "mod example;",
|
||||
"file2.rs": "",
|
||||
},
|
||||
"test": {
|
||||
"file3.rs": file_3_contents,
|
||||
}
|
||||
},
|
||||
"root2": {
|
||||
"src": {
|
||||
"main.rs": ""
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(
|
||||
fs.clone(),
|
||||
["/code/root1".as_ref(), "/code/root2".as_ref()],
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
let worktree_ids = project.read_with(cx, |project, cx| {
|
||||
project
|
||||
.worktrees()
|
||||
.map(|worktree| worktree.read(cx).id())
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
let mut ax = ProjectContext::new(project.downgrade(), fs);
|
||||
|
||||
ax.add_file(ProjectPath {
|
||||
worktree_id: worktree_ids[0],
|
||||
path: Path::new("lib/file1.rs").into(),
|
||||
});
|
||||
|
||||
let message = cx
|
||||
.update(|cx| ax.generate_system_message(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
r#"
|
||||
project structure:
|
||||
# root1
|
||||
## lib/file1.rs
|
||||
~~~
|
||||
mod example;
|
||||
~~~
|
||||
"#
|
||||
.unindent(),
|
||||
message
|
||||
);
|
||||
|
||||
ax.add_excerpts(
|
||||
ProjectPath {
|
||||
worktree_id: worktree_ids[0],
|
||||
path: Path::new("test/file3.rs").into(),
|
||||
},
|
||||
&[
|
||||
file_3_contents.find("fn test2").unwrap()
|
||||
..file_3_contents.find("fn test3").unwrap(),
|
||||
],
|
||||
);
|
||||
|
||||
let message = cx
|
||||
.update(|cx| ax.generate_system_message(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
r#"
|
||||
project structure:
|
||||
# root1
|
||||
## lib/file1.rs
|
||||
~~~
|
||||
mod example;
|
||||
~~~
|
||||
## test/file3.rs
|
||||
~~~
|
||||
...
|
||||
fn test2() {}
|
||||
...
|
||||
~~~
|
||||
"#
|
||||
.unindent(),
|
||||
message
|
||||
);
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
Project::init_settings(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,259 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use gpui::{AnyView, Task, WindowContext};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::tool::{
|
||||
LanguageModelTool, ToolFunctionCall, ToolFunctionCallResult, ToolFunctionDefinition,
|
||||
};
|
||||
|
||||
pub struct ToolRegistry {
|
||||
tools: HashMap<
|
||||
String,
|
||||
Box<dyn Fn(&ToolFunctionCall, &mut WindowContext) -> Task<Result<ToolFunctionCall>>>,
|
||||
>,
|
||||
definitions: Vec<ToolFunctionDefinition>,
|
||||
status_views: Vec<AnyView>,
|
||||
}
|
||||
|
||||
impl ToolRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
tools: HashMap::new(),
|
||||
definitions: Vec::new(),
|
||||
status_views: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn definitions(&self) -> &[ToolFunctionDefinition] {
|
||||
&self.definitions
|
||||
}
|
||||
|
||||
pub fn register<T: 'static + LanguageModelTool>(
|
||||
&mut self,
|
||||
tool: T,
|
||||
cx: &mut WindowContext,
|
||||
) -> Result<()> {
|
||||
self.definitions.push(tool.definition());
|
||||
|
||||
if let Some(tool_view) = tool.status_view(cx) {
|
||||
self.status_views.push(tool_view);
|
||||
}
|
||||
|
||||
let name = tool.name();
|
||||
let previous = self.tools.insert(
|
||||
name.clone(),
|
||||
// registry.call(tool_call, cx)
|
||||
Box::new(
|
||||
move |tool_call: &ToolFunctionCall, cx: &mut WindowContext| {
|
||||
let name = tool_call.name.clone();
|
||||
let arguments = tool_call.arguments.clone();
|
||||
let id = tool_call.id.clone();
|
||||
|
||||
let Ok(input) = serde_json::from_str::<T::Input>(arguments.as_str()) else {
|
||||
return Task::ready(Ok(ToolFunctionCall {
|
||||
id,
|
||||
name: name.clone(),
|
||||
arguments,
|
||||
result: Some(ToolFunctionCallResult::ParsingFailed),
|
||||
}));
|
||||
};
|
||||
|
||||
let result = tool.execute(&input, cx);
|
||||
|
||||
cx.spawn(move |mut cx| async move {
|
||||
let result: Result<T::Output> = result.await;
|
||||
let for_model = T::format(&input, &result);
|
||||
let view = cx.update(|cx| T::output_view(id.clone(), input, result, cx))?;
|
||||
|
||||
Ok(ToolFunctionCall {
|
||||
id,
|
||||
name: name.clone(),
|
||||
arguments,
|
||||
result: Some(ToolFunctionCallResult::Finished {
|
||||
view: view.into(),
|
||||
for_model,
|
||||
}),
|
||||
})
|
||||
})
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if previous.is_some() {
|
||||
return Err(anyhow!("already registered a tool with name {}", name));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Task yields an error if the window for the given WindowContext is closed before the task completes.
|
||||
pub fn call(
|
||||
&self,
|
||||
tool_call: &ToolFunctionCall,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<ToolFunctionCall>> {
|
||||
let name = tool_call.name.clone();
|
||||
let arguments = tool_call.arguments.clone();
|
||||
let id = tool_call.id.clone();
|
||||
|
||||
let tool = match self.tools.get(&name) {
|
||||
Some(tool) => tool,
|
||||
None => {
|
||||
let name = name.clone();
|
||||
return Task::ready(Ok(ToolFunctionCall {
|
||||
id,
|
||||
name: name.clone(),
|
||||
arguments,
|
||||
result: Some(ToolFunctionCallResult::NoSuchTool),
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
tool(tool_call, cx)
|
||||
}
|
||||
|
||||
pub fn status_views(&self) -> &[AnyView] {
|
||||
&self.status_views
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use gpui::{div, prelude::*, Render, TestAppContext};
|
||||
use gpui::{EmptyView, View};
|
||||
use schemars::schema_for;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Deserialize, Serialize, JsonSchema)]
|
||||
struct WeatherQuery {
|
||||
location: String,
|
||||
unit: String,
|
||||
}
|
||||
|
||||
struct WeatherTool {
|
||||
current_weather: WeatherResult,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
|
||||
struct WeatherResult {
|
||||
location: String,
|
||||
temperature: f64,
|
||||
unit: String,
|
||||
}
|
||||
|
||||
struct WeatherView {
|
||||
result: WeatherResult,
|
||||
}
|
||||
|
||||
impl Render for WeatherView {
|
||||
fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
|
||||
div().child(format!("temperature: {}", self.result.temperature))
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageModelTool for WeatherTool {
|
||||
type Input = WeatherQuery;
|
||||
type Output = WeatherResult;
|
||||
type View = WeatherView;
|
||||
|
||||
fn name(&self) -> String {
|
||||
"get_current_weather".to_string()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Fetches the current weather for a given location.".to_string()
|
||||
}
|
||||
|
||||
fn execute(
|
||||
&self,
|
||||
input: &Self::Input,
|
||||
_cx: &mut WindowContext,
|
||||
) -> Task<Result<Self::Output>> {
|
||||
let _location = input.location.clone();
|
||||
let _unit = input.unit.clone();
|
||||
|
||||
let weather = self.current_weather.clone();
|
||||
|
||||
Task::ready(Ok(weather))
|
||||
}
|
||||
|
||||
fn output_view(
|
||||
_tool_call_id: String,
|
||||
_input: Self::Input,
|
||||
result: Result<Self::Output>,
|
||||
cx: &mut WindowContext,
|
||||
) -> View<Self::View> {
|
||||
cx.new_view(|_cx| {
|
||||
let result = result.unwrap();
|
||||
WeatherView { result }
|
||||
})
|
||||
}
|
||||
|
||||
fn format(_: &Self::Input, output: &Result<Self::Output>) -> String {
|
||||
serde_json::to_string(&output.as_ref().unwrap()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_openai_weather_example(cx: &mut TestAppContext) {
|
||||
cx.background_executor.run_until_parked();
|
||||
let (_, cx) = cx.add_window_view(|_cx| EmptyView);
|
||||
|
||||
let tool = WeatherTool {
|
||||
current_weather: WeatherResult {
|
||||
location: "San Francisco".to_string(),
|
||||
temperature: 21.0,
|
||||
unit: "Celsius".to_string(),
|
||||
},
|
||||
};
|
||||
|
||||
let tools = vec![tool.definition()];
|
||||
assert_eq!(tools.len(), 1);
|
||||
|
||||
let expected = ToolFunctionDefinition {
|
||||
name: "get_current_weather".to_string(),
|
||||
description: "Fetches the current weather for a given location.".to_string(),
|
||||
parameters: schema_for!(WeatherQuery),
|
||||
};
|
||||
|
||||
assert_eq!(tools[0].name, expected.name);
|
||||
assert_eq!(tools[0].description, expected.description);
|
||||
|
||||
let expected_schema = serde_json::to_value(&tools[0].parameters).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
expected_schema,
|
||||
json!({
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "WeatherQuery",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {
|
||||
"type": "string"
|
||||
},
|
||||
"unit": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["location", "unit"]
|
||||
})
|
||||
);
|
||||
|
||||
let args = json!({
|
||||
"location": "San Francisco",
|
||||
"unit": "Celsius"
|
||||
});
|
||||
|
||||
let query: WeatherQuery = serde_json::from_value(args).unwrap();
|
||||
|
||||
let result = cx.update(|cx| tool.execute(&query, cx)).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let result = result.unwrap();
|
||||
|
||||
assert_eq!(result, tool.current_weather);
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use gpui::{AnyElement, AnyView, IntoElement as _, Render, Task, View, WindowContext};
|
||||
use schemars::{schema::RootSchema, schema_for, JsonSchema};
|
||||
use serde::Deserialize;
|
||||
use std::fmt::Display;
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
pub struct ToolFunctionCall {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub arguments: String,
|
||||
#[serde(skip)]
|
||||
pub result: Option<ToolFunctionCallResult>,
|
||||
}
|
||||
|
||||
pub enum ToolFunctionCallResult {
|
||||
NoSuchTool,
|
||||
ParsingFailed,
|
||||
Finished { for_model: String, view: AnyView },
|
||||
}
|
||||
|
||||
impl ToolFunctionCallResult {
|
||||
pub fn format(&self, name: &String) -> String {
|
||||
match self {
|
||||
ToolFunctionCallResult::NoSuchTool => format!("No tool for {name}"),
|
||||
ToolFunctionCallResult::ParsingFailed => {
|
||||
format!("Unable to parse arguments for {name}")
|
||||
}
|
||||
ToolFunctionCallResult::Finished { for_model, .. } => for_model.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_any_element(&self, name: &String) -> AnyElement {
|
||||
match self {
|
||||
ToolFunctionCallResult::NoSuchTool => {
|
||||
format!("Language Model attempted to call {name}").into_any_element()
|
||||
}
|
||||
ToolFunctionCallResult::ParsingFailed => {
|
||||
format!("Language Model called {name} with bad arguments").into_any_element()
|
||||
}
|
||||
ToolFunctionCallResult::Finished { view, .. } => view.clone().into_any_element(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ToolFunctionDefinition {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub parameters: RootSchema,
|
||||
}
|
||||
|
||||
impl Display for ToolFunctionDefinition {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let schema = serde_json::to_string(&self.parameters).ok();
|
||||
let schema = schema.unwrap_or("None".to_string());
|
||||
write!(f, "Name: {}:\n", self.name)?;
|
||||
write!(f, "Description: {}\n", self.description)?;
|
||||
write!(f, "Parameters: {}", schema)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait LanguageModelTool {
|
||||
/// The input type that will be passed in to `execute` when the tool is called
|
||||
/// by the language model.
|
||||
type Input: for<'de> Deserialize<'de> + JsonSchema;
|
||||
|
||||
/// The output returned by executing the tool.
|
||||
type Output: 'static;
|
||||
|
||||
type View: Render;
|
||||
|
||||
/// Returns the name of the tool.
|
||||
///
|
||||
/// This name is exposed to the language model to allow the model to pick
|
||||
/// which tools to use. As this name is used to identify the tool within a
|
||||
/// tool registry, it should be unique.
|
||||
fn name(&self) -> String;
|
||||
|
||||
/// Returns the description of the tool.
|
||||
///
|
||||
/// This can be used to _prompt_ the model as to what the tool does.
|
||||
fn description(&self) -> String;
|
||||
|
||||
/// Returns the OpenAI Function definition for the tool, for direct use with OpenAI's API.
|
||||
fn definition(&self) -> ToolFunctionDefinition {
|
||||
let root_schema = schema_for!(Self::Input);
|
||||
|
||||
ToolFunctionDefinition {
|
||||
name: self.name(),
|
||||
description: self.description(),
|
||||
parameters: root_schema,
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes the tool with the given input.
|
||||
fn execute(&self, input: &Self::Input, cx: &mut WindowContext) -> Task<Result<Self::Output>>;
|
||||
|
||||
fn format(input: &Self::Input, output: &Result<Self::Output>) -> String;
|
||||
|
||||
fn output_view(
|
||||
tool_call_id: String,
|
||||
input: Self::Input,
|
||||
output: Result<Self::Output>,
|
||||
cx: &mut WindowContext,
|
||||
) -> View<Self::View>;
|
||||
|
||||
fn status_view(&self, _cx: &mut WindowContext) -> Option<AnyView> {
|
||||
None
|
||||
}
|
||||
}
|
||||
526
crates/assistant_tooling/src/tool_registry.rs
Normal file
@@ -0,0 +1,526 @@
|
||||
use crate::ProjectContext;
|
||||
use anyhow::{anyhow, Result};
|
||||
use gpui::{AnyElement, AnyView, IntoElement, Render, Task, View, WindowContext};
|
||||
use repair_json::repair;
|
||||
use schemars::{schema::RootSchema, schema_for, JsonSchema};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use serde_json::value::RawValue;
|
||||
use std::{
|
||||
any::TypeId,
|
||||
collections::HashMap,
|
||||
fmt::Display,
|
||||
mem,
|
||||
sync::atomic::{AtomicBool, Ordering::SeqCst},
|
||||
};
|
||||
use ui::ViewContext;
|
||||
|
||||
pub struct ToolRegistry {
|
||||
registered_tools: HashMap<String, RegisteredTool>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ToolFunctionCall {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub arguments: String,
|
||||
state: ToolFunctionCallState,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
enum ToolFunctionCallState {
|
||||
#[default]
|
||||
Initializing,
|
||||
NoSuchTool,
|
||||
KnownTool(Box<dyn ToolView>),
|
||||
ExecutedTool(Box<dyn ToolView>),
|
||||
}
|
||||
|
||||
trait ToolView {
|
||||
fn view(&self) -> AnyView;
|
||||
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String;
|
||||
fn try_set_input(&self, input: &str, cx: &mut WindowContext);
|
||||
fn execute(&self, cx: &mut WindowContext) -> Task<Result<()>>;
|
||||
fn serialize_output(&self, cx: &mut WindowContext) -> Result<Box<RawValue>>;
|
||||
fn deserialize_output(&self, raw_value: &RawValue, cx: &mut WindowContext) -> Result<()>;
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
pub struct SavedToolFunctionCall {
|
||||
id: String,
|
||||
name: String,
|
||||
arguments: String,
|
||||
state: SavedToolFunctionCallState,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
enum SavedToolFunctionCallState {
|
||||
#[default]
|
||||
Initializing,
|
||||
NoSuchTool,
|
||||
KnownTool,
|
||||
ExecutedTool(Box<RawValue>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ToolFunctionDefinition {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub parameters: RootSchema,
|
||||
}
|
||||
|
||||
pub trait LanguageModelTool {
|
||||
type View: ToolOutput;
|
||||
|
||||
/// Returns the name of the tool.
|
||||
///
|
||||
/// This name is exposed to the language model to allow the model to pick
|
||||
/// which tools to use. As this name is used to identify the tool within a
|
||||
/// tool registry, it should be unique.
|
||||
fn name(&self) -> String;
|
||||
|
||||
/// Returns the description of the tool.
|
||||
///
|
||||
/// This can be used to _prompt_ the model as to what the tool does.
|
||||
fn description(&self) -> String;
|
||||
|
||||
/// Returns the OpenAI Function definition for the tool, for direct use with OpenAI's API.
|
||||
fn definition(&self) -> ToolFunctionDefinition {
|
||||
let root_schema = schema_for!(<Self::View as ToolOutput>::Input);
|
||||
|
||||
ToolFunctionDefinition {
|
||||
name: self.name(),
|
||||
description: self.description(),
|
||||
parameters: root_schema,
|
||||
}
|
||||
}
|
||||
|
||||
/// A view of the output of running the tool, for displaying to the user.
|
||||
fn view(&self, cx: &mut WindowContext) -> View<Self::View>;
|
||||
}
|
||||
|
||||
pub trait ToolOutput: Render {
|
||||
/// The input type that will be passed in to `execute` when the tool is called
|
||||
/// by the language model.
|
||||
type Input: DeserializeOwned + JsonSchema;
|
||||
|
||||
/// The output returned by executing the tool.
|
||||
type SerializedState: DeserializeOwned + Serialize;
|
||||
|
||||
fn generate(&self, project: &mut ProjectContext, cx: &mut ViewContext<Self>) -> String;
|
||||
fn set_input(&mut self, input: Self::Input, cx: &mut ViewContext<Self>);
|
||||
fn execute(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>>;
|
||||
|
||||
fn serialize(&self, cx: &mut ViewContext<Self>) -> Self::SerializedState;
|
||||
fn deserialize(
|
||||
&mut self,
|
||||
output: Self::SerializedState,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Result<()>;
|
||||
}
|
||||
|
||||
struct RegisteredTool {
|
||||
enabled: AtomicBool,
|
||||
type_id: TypeId,
|
||||
build_view: Box<dyn Fn(&mut WindowContext) -> Box<dyn ToolView>>,
|
||||
definition: ToolFunctionDefinition,
|
||||
}
|
||||
|
||||
impl ToolRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
registered_tools: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_tool_enabled<T: 'static + LanguageModelTool>(&self, is_enabled: bool) {
|
||||
for tool in self.registered_tools.values() {
|
||||
if tool.type_id == TypeId::of::<T>() {
|
||||
tool.enabled.store(is_enabled, SeqCst);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_tool_enabled<T: 'static + LanguageModelTool>(&self) -> bool {
|
||||
for tool in self.registered_tools.values() {
|
||||
if tool.type_id == TypeId::of::<T>() {
|
||||
return tool.enabled.load(SeqCst);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn definitions(&self) -> Vec<ToolFunctionDefinition> {
|
||||
self.registered_tools
|
||||
.values()
|
||||
.filter(|tool| tool.enabled.load(SeqCst))
|
||||
.map(|tool| tool.definition.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn update_tool_call(
|
||||
&self,
|
||||
call: &mut ToolFunctionCall,
|
||||
name: Option<&str>,
|
||||
arguments: Option<&str>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
if let Some(name) = name {
|
||||
call.name.push_str(name);
|
||||
}
|
||||
if let Some(arguments) = arguments {
|
||||
if call.arguments.is_empty() {
|
||||
if let Some(tool) = self.registered_tools.get(&call.name) {
|
||||
let view = (tool.build_view)(cx);
|
||||
call.state = ToolFunctionCallState::KnownTool(view);
|
||||
} else {
|
||||
call.state = ToolFunctionCallState::NoSuchTool;
|
||||
}
|
||||
}
|
||||
call.arguments.push_str(arguments);
|
||||
|
||||
if let ToolFunctionCallState::KnownTool(view) = &call.state {
|
||||
if let Ok(repaired_arguments) = repair(call.arguments.clone()) {
|
||||
view.try_set_input(&repaired_arguments, cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn execute_tool_call(
|
||||
&self,
|
||||
tool_call: &mut ToolFunctionCall,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
if let ToolFunctionCallState::KnownTool(view) = mem::take(&mut tool_call.state) {
|
||||
let task = view.execute(cx);
|
||||
tool_call.state = ToolFunctionCallState::ExecutedTool(view);
|
||||
Some(task)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_tool_call(
|
||||
&self,
|
||||
tool_call: &ToolFunctionCall,
|
||||
_cx: &mut WindowContext,
|
||||
) -> Option<AnyElement> {
|
||||
match &tool_call.state {
|
||||
ToolFunctionCallState::NoSuchTool => {
|
||||
Some(ui::Label::new("No such tool").into_any_element())
|
||||
}
|
||||
ToolFunctionCallState::Initializing => None,
|
||||
ToolFunctionCallState::KnownTool(view) | ToolFunctionCallState::ExecutedTool(view) => {
|
||||
Some(view.view().into_any_element())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn content_for_tool_call(
|
||||
&self,
|
||||
tool_call: &ToolFunctionCall,
|
||||
project_context: &mut ProjectContext,
|
||||
cx: &mut WindowContext,
|
||||
) -> String {
|
||||
match &tool_call.state {
|
||||
ToolFunctionCallState::Initializing => String::new(),
|
||||
ToolFunctionCallState::NoSuchTool => {
|
||||
format!("No such tool: {}", tool_call.name)
|
||||
}
|
||||
ToolFunctionCallState::KnownTool(view) | ToolFunctionCallState::ExecutedTool(view) => {
|
||||
view.generate(project_context, cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize_tool_call(
|
||||
&self,
|
||||
call: &ToolFunctionCall,
|
||||
cx: &mut WindowContext,
|
||||
) -> Result<SavedToolFunctionCall> {
|
||||
Ok(SavedToolFunctionCall {
|
||||
id: call.id.clone(),
|
||||
name: call.name.clone(),
|
||||
arguments: call.arguments.clone(),
|
||||
state: match &call.state {
|
||||
ToolFunctionCallState::Initializing => SavedToolFunctionCallState::Initializing,
|
||||
ToolFunctionCallState::NoSuchTool => SavedToolFunctionCallState::NoSuchTool,
|
||||
ToolFunctionCallState::KnownTool(_) => SavedToolFunctionCallState::KnownTool,
|
||||
ToolFunctionCallState::ExecutedTool(view) => {
|
||||
SavedToolFunctionCallState::ExecutedTool(view.serialize_output(cx)?)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub fn deserialize_tool_call(
|
||||
&self,
|
||||
call: &SavedToolFunctionCall,
|
||||
cx: &mut WindowContext,
|
||||
) -> Result<ToolFunctionCall> {
|
||||
let Some(tool) = self.registered_tools.get(&call.name) else {
|
||||
return Err(anyhow!("no such tool {}", call.name));
|
||||
};
|
||||
|
||||
Ok(ToolFunctionCall {
|
||||
id: call.id.clone(),
|
||||
name: call.name.clone(),
|
||||
arguments: call.arguments.clone(),
|
||||
state: match &call.state {
|
||||
SavedToolFunctionCallState::Initializing => ToolFunctionCallState::Initializing,
|
||||
SavedToolFunctionCallState::NoSuchTool => ToolFunctionCallState::NoSuchTool,
|
||||
SavedToolFunctionCallState::KnownTool => {
|
||||
log::error!("Deserialized tool that had not executed");
|
||||
let view = (tool.build_view)(cx);
|
||||
view.try_set_input(&call.arguments, cx);
|
||||
ToolFunctionCallState::KnownTool(view)
|
||||
}
|
||||
SavedToolFunctionCallState::ExecutedTool(output) => {
|
||||
let view = (tool.build_view)(cx);
|
||||
view.try_set_input(&call.arguments, cx);
|
||||
view.deserialize_output(output, cx)?;
|
||||
ToolFunctionCallState::ExecutedTool(view)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub fn register<T: 'static + LanguageModelTool>(&mut self, tool: T) -> Result<()> {
|
||||
let name = tool.name();
|
||||
let registered_tool = RegisteredTool {
|
||||
type_id: TypeId::of::<T>(),
|
||||
definition: tool.definition(),
|
||||
enabled: AtomicBool::new(true),
|
||||
build_view: Box::new(move |cx: &mut WindowContext| Box::new(tool.view(cx))),
|
||||
};
|
||||
|
||||
let previous = self.registered_tools.insert(name.clone(), registered_tool);
|
||||
if previous.is_some() {
|
||||
return Err(anyhow!("already registered a tool with name {}", name));
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ToolOutput> ToolView for View<T> {
|
||||
fn view(&self) -> AnyView {
|
||||
self.clone().into()
|
||||
}
|
||||
|
||||
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String {
|
||||
self.update(cx, |view, cx| view.generate(project, cx))
|
||||
}
|
||||
|
||||
fn try_set_input(&self, input: &str, cx: &mut WindowContext) {
|
||||
if let Ok(input) = serde_json::from_str::<T::Input>(input) {
|
||||
self.update(cx, |view, cx| {
|
||||
view.set_input(input, cx);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn execute(&self, cx: &mut WindowContext) -> Task<Result<()>> {
|
||||
self.update(cx, |view, cx| view.execute(cx))
|
||||
}
|
||||
|
||||
fn serialize_output(&self, cx: &mut WindowContext) -> Result<Box<RawValue>> {
|
||||
let output = self.update(cx, |view, cx| view.serialize(cx));
|
||||
Ok(RawValue::from_string(serde_json::to_string(&output)?)?)
|
||||
}
|
||||
|
||||
fn deserialize_output(&self, output: &RawValue, cx: &mut WindowContext) -> Result<()> {
|
||||
let state = serde_json::from_str::<T::SerializedState>(output.get())?;
|
||||
self.update(cx, |view, cx| view.deserialize(state, cx))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ToolFunctionDefinition {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let schema = serde_json::to_string(&self.parameters).ok();
|
||||
let schema = schema.unwrap_or("None".to_string());
|
||||
write!(f, "Name: {}:\n", self.name)?;
|
||||
write!(f, "Description: {}\n", self.description)?;
|
||||
write!(f, "Parameters: {}", schema)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use gpui::{div, prelude::*, Render, TestAppContext};
|
||||
use gpui::{EmptyView, View};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Deserialize, Serialize, JsonSchema)]
|
||||
struct WeatherQuery {
|
||||
location: String,
|
||||
unit: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
|
||||
struct WeatherResult {
|
||||
location: String,
|
||||
temperature: f64,
|
||||
unit: String,
|
||||
}
|
||||
|
||||
struct WeatherView {
|
||||
input: Option<WeatherQuery>,
|
||||
result: Option<WeatherResult>,
|
||||
|
||||
// Fake API call
|
||||
current_weather: WeatherResult,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
struct WeatherTool {
|
||||
current_weather: WeatherResult,
|
||||
}
|
||||
|
||||
impl WeatherView {
|
||||
fn new(current_weather: WeatherResult) -> Self {
|
||||
Self {
|
||||
input: None,
|
||||
result: None,
|
||||
current_weather,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for WeatherView {
|
||||
fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
|
||||
match self.result {
|
||||
Some(ref result) => div()
|
||||
.child(format!("temperature: {}", result.temperature))
|
||||
.into_any_element(),
|
||||
None => div().child("Calculating weather...").into_any_element(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolOutput for WeatherView {
|
||||
type Input = WeatherQuery;
|
||||
|
||||
type SerializedState = WeatherResult;
|
||||
|
||||
fn generate(&self, _output: &mut ProjectContext, _cx: &mut ViewContext<Self>) -> String {
|
||||
serde_json::to_string(&self.result).unwrap()
|
||||
}
|
||||
|
||||
fn set_input(&mut self, input: Self::Input, cx: &mut ViewContext<Self>) {
|
||||
self.input = Some(input);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn execute(&mut self, _cx: &mut ViewContext<Self>) -> Task<Result<()>> {
|
||||
let input = self.input.as_ref().unwrap();
|
||||
|
||||
let _location = input.location.clone();
|
||||
let _unit = input.unit.clone();
|
||||
|
||||
let weather = self.current_weather.clone();
|
||||
|
||||
self.result = Some(weather);
|
||||
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn serialize(&self, _cx: &mut ViewContext<Self>) -> Self::SerializedState {
|
||||
self.current_weather.clone()
|
||||
}
|
||||
|
||||
fn deserialize(
|
||||
&mut self,
|
||||
output: Self::SerializedState,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> Result<()> {
|
||||
self.current_weather = output;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageModelTool for WeatherTool {
|
||||
type View = WeatherView;
|
||||
|
||||
fn name(&self) -> String {
|
||||
"get_current_weather".to_string()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Fetches the current weather for a given location.".to_string()
|
||||
}
|
||||
|
||||
fn view(&self, cx: &mut WindowContext) -> View<Self::View> {
|
||||
cx.new_view(|_cx| WeatherView::new(self.current_weather.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_openai_weather_example(cx: &mut TestAppContext) {
|
||||
let (_, cx) = cx.add_window_view(|_cx| EmptyView);
|
||||
|
||||
let mut registry = ToolRegistry::new();
|
||||
registry
|
||||
.register(WeatherTool {
|
||||
current_weather: WeatherResult {
|
||||
location: "San Francisco".to_string(),
|
||||
temperature: 21.0,
|
||||
unit: "Celsius".to_string(),
|
||||
},
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let definitions = registry.definitions();
|
||||
assert_eq!(
|
||||
definitions,
|
||||
[ToolFunctionDefinition {
|
||||
name: "get_current_weather".to_string(),
|
||||
description: "Fetches the current weather for a given location.".to_string(),
|
||||
parameters: serde_json::from_value(json!({
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "WeatherQuery",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {
|
||||
"type": "string"
|
||||
},
|
||||
"unit": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["location", "unit"]
|
||||
}))
|
||||
.unwrap(),
|
||||
}]
|
||||
);
|
||||
|
||||
let mut call = ToolFunctionCall {
|
||||
id: "the-id".to_string(),
|
||||
name: "get_cur".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let task = cx.update(|cx| {
|
||||
registry.update_tool_call(
|
||||
&mut call,
|
||||
Some("rent_weather"),
|
||||
Some(r#"{"location": "San Francisco","#),
|
||||
cx,
|
||||
);
|
||||
registry.update_tool_call(&mut call, None, Some(r#" "unit": "Celsius"}"#), cx);
|
||||
registry.execute_tool_call(&mut call, cx).unwrap()
|
||||
});
|
||||
task.await.unwrap();
|
||||
|
||||
match &call.state {
|
||||
ToolFunctionCallState::ExecutedTool(_view) => {}
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPrevi
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde_derive::Serialize;
|
||||
use smol::io::AsyncReadExt;
|
||||
use smol::{fs, io::AsyncReadExt};
|
||||
|
||||
use settings::{Settings, SettingsSources, SettingsStore};
|
||||
use smol::{fs::File, process::Command};
|
||||
@@ -24,6 +24,7 @@ use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
|
||||
use std::{
|
||||
env::consts::{ARCH, OS},
|
||||
ffi::OsString,
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
@@ -55,16 +56,22 @@ struct UpdateRequestBody {
|
||||
telemetry: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub enum AutoUpdateStatus {
|
||||
Idle,
|
||||
Checking,
|
||||
Downloading,
|
||||
Installing,
|
||||
Updated,
|
||||
Updated { binary_path: PathBuf },
|
||||
Errored,
|
||||
}
|
||||
|
||||
impl AutoUpdateStatus {
|
||||
pub fn is_updated(&self) -> bool {
|
||||
matches!(self, Self::Updated { .. })
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AutoUpdater {
|
||||
status: AutoUpdateStatus,
|
||||
current_version: SemanticVersion,
|
||||
@@ -305,7 +312,7 @@ impl AutoUpdater {
|
||||
}
|
||||
|
||||
pub fn poll(&mut self, cx: &mut ModelContext<Self>) {
|
||||
if self.pending_poll.is_some() || self.status == AutoUpdateStatus::Updated {
|
||||
if self.pending_poll.is_some() || self.status.is_updated() {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -327,7 +334,7 @@ impl AutoUpdater {
|
||||
}
|
||||
|
||||
pub fn status(&self) -> AutoUpdateStatus {
|
||||
self.status
|
||||
self.status.clone()
|
||||
}
|
||||
|
||||
pub fn dismiss_error(&mut self, cx: &mut ModelContext<Self>) {
|
||||
@@ -340,9 +347,15 @@ impl AutoUpdater {
|
||||
(this.http_client.clone(), this.current_version)
|
||||
})?;
|
||||
|
||||
let asset = match OS {
|
||||
"linux" => format!("zed-linux-{}.tar.gz", ARCH),
|
||||
"macos" => "Zed.dmg".into(),
|
||||
_ => return Err(anyhow!("auto-update not supported for OS {:?}", OS)),
|
||||
};
|
||||
|
||||
let mut url_string = client.build_url(&format!(
|
||||
"/api/releases/latest?asset=Zed.dmg&os={}&arch={}",
|
||||
OS, ARCH
|
||||
"/api/releases/latest?asset={}&os={}&arch={}",
|
||||
asset, OS, ARCH
|
||||
));
|
||||
cx.update(|cx| {
|
||||
if let Some(param) = ReleaseChannel::try_global(cx)
|
||||
@@ -361,6 +374,7 @@ impl AutoUpdater {
|
||||
.read_to_end(&mut body)
|
||||
.await
|
||||
.context("error reading release")?;
|
||||
|
||||
let release: JsonRelease =
|
||||
serde_json::from_slice(body.as_slice()).context("error deserializing release")?;
|
||||
|
||||
@@ -389,88 +403,31 @@ impl AutoUpdater {
|
||||
let temp_dir = tempfile::Builder::new()
|
||||
.prefix("zed-auto-update")
|
||||
.tempdir()?;
|
||||
let dmg_path = temp_dir.path().join("Zed.dmg");
|
||||
let mount_path = temp_dir.path().join("Zed");
|
||||
let running_app_path = ZED_APP_PATH
|
||||
.clone()
|
||||
.map_or_else(|| cx.update(|cx| cx.app_path())?, Ok)?;
|
||||
let running_app_filename = running_app_path
|
||||
.file_name()
|
||||
.ok_or_else(|| anyhow!("invalid running app path"))?;
|
||||
let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into();
|
||||
mounted_app_path.push("/");
|
||||
|
||||
let mut dmg_file = File::create(&dmg_path).await?;
|
||||
|
||||
let (installation_id, release_channel, telemetry) = cx.update(|cx| {
|
||||
let installation_id = Client::global(cx).telemetry().installation_id();
|
||||
let release_channel = ReleaseChannel::try_global(cx)
|
||||
.map(|release_channel| release_channel.display_name());
|
||||
let telemetry = TelemetrySettings::get_global(cx).metrics;
|
||||
|
||||
(installation_id, release_channel, telemetry)
|
||||
})?;
|
||||
|
||||
let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody {
|
||||
installation_id,
|
||||
release_channel,
|
||||
telemetry,
|
||||
})?);
|
||||
|
||||
let mut response = client.get(&release.url, request_body, true).await?;
|
||||
smol::io::copy(response.body_mut(), &mut dmg_file).await?;
|
||||
log::info!("downloaded update. path:{:?}", dmg_path);
|
||||
let downloaded_asset = download_release(&temp_dir, release, &asset, client, &cx).await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.status = AutoUpdateStatus::Installing;
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
let output = Command::new("hdiutil")
|
||||
.args(&["attach", "-nobrowse"])
|
||||
.arg(&dmg_path)
|
||||
.arg("-mountroot")
|
||||
.arg(&temp_dir.path())
|
||||
.output()
|
||||
.await?;
|
||||
if !output.status.success() {
|
||||
Err(anyhow!(
|
||||
"failed to mount: {:?}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
))?;
|
||||
}
|
||||
// We store the path of our current binary, before we install, since installation might
|
||||
// delete it. Once deleted, it's hard to get the path to our binary on Linux.
|
||||
// So we cache it here, which allows us to then restart later on.
|
||||
let binary_path = cx.update(|cx| cx.app_path())??;
|
||||
|
||||
let output = Command::new("rsync")
|
||||
.args(&["-av", "--delete"])
|
||||
.arg(&mounted_app_path)
|
||||
.arg(&running_app_path)
|
||||
.output()
|
||||
.await?;
|
||||
if !output.status.success() {
|
||||
Err(anyhow!(
|
||||
"failed to copy app: {:?}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
))?;
|
||||
}
|
||||
|
||||
let output = Command::new("hdiutil")
|
||||
.args(&["detach"])
|
||||
.arg(&mount_path)
|
||||
.output()
|
||||
.await?;
|
||||
if !output.status.success() {
|
||||
Err(anyhow!(
|
||||
"failed to unmount: {:?}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
))?;
|
||||
}
|
||||
match OS {
|
||||
"macos" => install_release_macos(&temp_dir, downloaded_asset, &cx).await,
|
||||
"linux" => install_release_linux(&temp_dir, downloaded_asset, &cx).await,
|
||||
_ => Err(anyhow!("not supported: {:?}", OS)),
|
||||
}?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.set_should_show_update_notification(true, cx)
|
||||
.detach_and_log_err(cx);
|
||||
this.status = AutoUpdateStatus::Updated;
|
||||
this.status = AutoUpdateStatus::Updated { binary_path };
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -504,3 +461,150 @@ impl AutoUpdater {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn download_release(
|
||||
temp_dir: &tempfile::TempDir,
|
||||
release: JsonRelease,
|
||||
target_filename: &str,
|
||||
client: Arc<HttpClientWithUrl>,
|
||||
cx: &AsyncAppContext,
|
||||
) -> Result<PathBuf> {
|
||||
let target_path = temp_dir.path().join(target_filename);
|
||||
let mut target_file = File::create(&target_path).await?;
|
||||
|
||||
let (installation_id, release_channel, telemetry) = cx.update(|cx| {
|
||||
let installation_id = Client::global(cx).telemetry().installation_id();
|
||||
let release_channel =
|
||||
ReleaseChannel::try_global(cx).map(|release_channel| release_channel.display_name());
|
||||
let telemetry = TelemetrySettings::get_global(cx).metrics;
|
||||
|
||||
(installation_id, release_channel, telemetry)
|
||||
})?;
|
||||
|
||||
let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody {
|
||||
installation_id,
|
||||
release_channel,
|
||||
telemetry,
|
||||
})?);
|
||||
|
||||
let mut response = client.get(&release.url, request_body, true).await?;
|
||||
smol::io::copy(response.body_mut(), &mut target_file).await?;
|
||||
log::info!("downloaded update. path:{:?}", target_path);
|
||||
|
||||
Ok(target_path)
|
||||
}
|
||||
|
||||
async fn install_release_linux(
|
||||
temp_dir: &tempfile::TempDir,
|
||||
downloaded_tar_gz: PathBuf,
|
||||
cx: &AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let channel = cx.update(|cx| ReleaseChannel::global(cx).dev_name())?;
|
||||
let home_dir = PathBuf::from(std::env::var("HOME").context("no HOME env var set")?);
|
||||
|
||||
let extracted = temp_dir.path().join("zed");
|
||||
fs::create_dir_all(&extracted)
|
||||
.await
|
||||
.context("failed to create directory into which to extract update")?;
|
||||
|
||||
let output = Command::new("tar")
|
||||
.arg("-xzf")
|
||||
.arg(&downloaded_tar_gz)
|
||||
.arg("-C")
|
||||
.arg(&extracted)
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
anyhow::ensure!(
|
||||
output.status.success(),
|
||||
"failed to extract {:?} to {:?}: {:?}",
|
||||
downloaded_tar_gz,
|
||||
extracted,
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let suffix = if channel != "stable" {
|
||||
format!("-{}", channel)
|
||||
} else {
|
||||
String::default()
|
||||
};
|
||||
let app_folder_name = format!("zed{}.app", suffix);
|
||||
|
||||
let from = extracted.join(&app_folder_name);
|
||||
let to = home_dir.join(".local");
|
||||
|
||||
let output = Command::new("rsync")
|
||||
.args(&["-av", "--delete"])
|
||||
.arg(&from)
|
||||
.arg(&to)
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
anyhow::ensure!(
|
||||
output.status.success(),
|
||||
"failed to copy Zed update from {:?} to {:?}: {:?}",
|
||||
from,
|
||||
to,
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn install_release_macos(
|
||||
temp_dir: &tempfile::TempDir,
|
||||
downloaded_dmg: PathBuf,
|
||||
cx: &AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let running_app_path = ZED_APP_PATH
|
||||
.clone()
|
||||
.map_or_else(|| cx.update(|cx| cx.app_path())?, Ok)?;
|
||||
let running_app_filename = running_app_path
|
||||
.file_name()
|
||||
.ok_or_else(|| anyhow!("invalid running app path"))?;
|
||||
|
||||
let mount_path = temp_dir.path().join("Zed");
|
||||
let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into();
|
||||
|
||||
mounted_app_path.push("/");
|
||||
let output = Command::new("hdiutil")
|
||||
.args(&["attach", "-nobrowse"])
|
||||
.arg(&downloaded_dmg)
|
||||
.arg("-mountroot")
|
||||
.arg(&temp_dir.path())
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
anyhow::ensure!(
|
||||
output.status.success(),
|
||||
"failed to mount: {:?}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let output = Command::new("rsync")
|
||||
.args(&["-av", "--delete"])
|
||||
.arg(&mounted_app_path)
|
||||
.arg(&running_app_path)
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
anyhow::ensure!(
|
||||
output.status.success(),
|
||||
"failed to copy app: {:?}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let output = Command::new("hdiutil")
|
||||
.args(&["detach"])
|
||||
.arg(&mount_path)
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
anyhow::ensure!(
|
||||
output.status.success(),
|
||||
"failed to unount: {:?}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -19,11 +19,17 @@ path = "src/main.rs"
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
clap.workspace = true
|
||||
libc.workspace = true
|
||||
ipc-channel = "0.18"
|
||||
once_cell.workspace = true
|
||||
release_channel.workspace = true
|
||||
serde.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
exec.workspace = true
|
||||
fork.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
core-foundation.workspace = true
|
||||
core-services = "0.2"
|
||||
|
||||
@@ -13,6 +13,7 @@ pub enum CliRequest {
|
||||
paths: Vec<String>,
|
||||
wait: bool,
|
||||
open_new_workspace: Option<bool>,
|
||||
dev_server_token: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
#![cfg_attr(any(target_os = "linux", target_os = "windows"), allow(dead_code))]
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use anyhow::{Context, Result};
|
||||
use clap::Parser;
|
||||
use cli::{CliRequest, CliResponse};
|
||||
use serde::Deserialize;
|
||||
use cli::{ipc::IpcOneShotServer, CliRequest, CliResponse, IpcHandshake};
|
||||
use std::{
|
||||
env,
|
||||
ffi::OsStr,
|
||||
fs,
|
||||
env, fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use util::paths::PathLikeWithPosition;
|
||||
|
||||
struct Detect;
|
||||
|
||||
trait InstalledApp {
|
||||
fn zed_version_string(&self) -> String;
|
||||
fn launch(&self, ipc_url: String) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "zed", disable_version_flag = true)]
|
||||
struct Args {
|
||||
@@ -33,9 +37,9 @@ struct Args {
|
||||
/// Print Zed's version and the app path.
|
||||
#[arg(short, long)]
|
||||
version: bool,
|
||||
/// Custom Zed.app path
|
||||
#[arg(short, long)]
|
||||
bundle_path: Option<PathBuf>,
|
||||
/// Custom path to Zed.app or the zed binary
|
||||
#[arg(long)]
|
||||
zed: Option<PathBuf>,
|
||||
/// Run zed in dev-server mode
|
||||
#[arg(long)]
|
||||
dev_server_token: Option<String>,
|
||||
@@ -49,12 +53,6 @@ fn parse_path_with_position(
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct InfoPlist {
|
||||
#[serde(rename = "CFBundleShortVersionString")]
|
||||
bundle_short_version_string: String,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
// Intercept version designators
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -68,14 +66,10 @@ fn main() -> Result<()> {
|
||||
}
|
||||
let args = Args::parse();
|
||||
|
||||
let bundle = Bundle::detect(args.bundle_path.as_deref()).context("Bundle detection")?;
|
||||
|
||||
if let Some(dev_server_token) = args.dev_server_token {
|
||||
return bundle.spawn(vec!["--dev-server-token".into(), dev_server_token]);
|
||||
}
|
||||
let app = Detect::detect(args.zed.as_deref()).context("Bundle detection")?;
|
||||
|
||||
if args.version {
|
||||
println!("{}", bundle.zed_version_string());
|
||||
println!("{}", app.zed_version_string());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -101,7 +95,14 @@ fn main() -> Result<()> {
|
||||
paths.push(canonicalized.to_string(|path| path.display().to_string()))
|
||||
}
|
||||
|
||||
let (tx, rx) = bundle.launch()?;
|
||||
let (server, server_name) =
|
||||
IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
|
||||
let url = format!("zed-cli://{server_name}");
|
||||
|
||||
app.launch(url)?;
|
||||
let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
|
||||
let (tx, rx) = (handshake.requests, handshake.responses);
|
||||
|
||||
let open_new_workspace = if args.new {
|
||||
Some(true)
|
||||
} else if args.add {
|
||||
@@ -114,6 +115,7 @@ fn main() -> Result<()> {
|
||||
paths,
|
||||
wait: args.wait,
|
||||
open_new_workspace,
|
||||
dev_server_token: args.dev_server_token,
|
||||
})?;
|
||||
|
||||
while let Ok(response) = rx.recv() {
|
||||
@@ -128,60 +130,125 @@ fn main() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
enum Bundle {
|
||||
App {
|
||||
app_bundle: PathBuf,
|
||||
plist: InfoPlist,
|
||||
},
|
||||
LocalPath {
|
||||
executable: PathBuf,
|
||||
plist: InfoPlist,
|
||||
},
|
||||
}
|
||||
|
||||
fn locate_bundle() -> Result<PathBuf> {
|
||||
let cli_path = std::env::current_exe()?.canonicalize()?;
|
||||
let mut app_path = cli_path.clone();
|
||||
while app_path.extension() != Some(OsStr::new("app")) {
|
||||
if !app_path.pop() {
|
||||
return Err(anyhow!("cannot find app bundle containing {:?}", cli_path));
|
||||
}
|
||||
}
|
||||
Ok(app_path)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux {
|
||||
use std::path::Path;
|
||||
use std::{
|
||||
env,
|
||||
ffi::OsString,
|
||||
io,
|
||||
os::{
|
||||
linux::net::SocketAddrExt,
|
||||
unix::net::{SocketAddr, UnixDatagram},
|
||||
},
|
||||
path::{Path, PathBuf},
|
||||
process, thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use cli::{CliRequest, CliResponse};
|
||||
use ipc_channel::ipc::{IpcReceiver, IpcSender};
|
||||
use anyhow::anyhow;
|
||||
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
|
||||
use fork::Fork;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::{Bundle, InfoPlist};
|
||||
use crate::{Detect, InstalledApp};
|
||||
|
||||
impl Bundle {
|
||||
pub fn detect(_args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
|
||||
unimplemented!()
|
||||
static RELEASE_CHANNEL: Lazy<String> =
|
||||
Lazy::new(|| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string());
|
||||
|
||||
struct App(PathBuf);
|
||||
|
||||
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 {
|
||||
let cli = env::current_exe()?;
|
||||
let dir = cli
|
||||
.parent()
|
||||
.ok_or_else(|| anyhow!("no parent path for cli"))?;
|
||||
|
||||
match dir.join("zed").canonicalize() {
|
||||
Ok(path) => Ok(path),
|
||||
// development builds have Zed capitalized
|
||||
Err(e) => match dir.join("Zed").canonicalize() {
|
||||
Ok(path) => Ok(path),
|
||||
Err(_) => Err(e),
|
||||
},
|
||||
}
|
||||
}?;
|
||||
|
||||
Ok(App(path))
|
||||
}
|
||||
}
|
||||
|
||||
impl InstalledApp for App {
|
||||
fn zed_version_string(&self) -> String {
|
||||
format!(
|
||||
"Zed {}{} – {}",
|
||||
if *RELEASE_CHANNEL == "stable" {
|
||||
"".to_string()
|
||||
} else {
|
||||
format!(" {} ", *RELEASE_CHANNEL)
|
||||
},
|
||||
option_env!("RELEASE_VERSION").unwrap_or_default(),
|
||||
self.0.display(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn plist(&self) -> &InfoPlist {
|
||||
unimplemented!()
|
||||
fn launch(&self, ipc_url: String) -> anyhow::Result<()> {
|
||||
let uid: u32 = unsafe { libc::getuid() };
|
||||
let sock_addr =
|
||||
SocketAddr::from_abstract_name(format!("zed-{}-{}", *RELEASE_CHANNEL, uid))?;
|
||||
|
||||
let sock = UnixDatagram::unbound()?;
|
||||
if sock.connect_addr(&sock_addr).is_err() {
|
||||
self.boot_background(ipc_url)?;
|
||||
} else {
|
||||
sock.send(ipc_url.as_bytes())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn boot_background(&self, ipc_url: String) -> anyhow::Result<()> {
|
||||
let path = &self.0;
|
||||
|
||||
match fork::fork() {
|
||||
Ok(Fork::Parent(_)) => Ok(()),
|
||||
Ok(Fork::Child) => {
|
||||
std::env::set_var(FORCE_CLI_MODE_ENV_VAR_NAME, "");
|
||||
if let Err(_) = fork::setsid() {
|
||||
eprintln!("failed to setsid: {}", std::io::Error::last_os_error());
|
||||
process::exit(1);
|
||||
}
|
||||
if std::env::var("ZED_KEEP_FD").is_err() {
|
||||
if let Err(_) = fork::close_fd() {
|
||||
eprintln!("failed to close_fd: {}", std::io::Error::last_os_error());
|
||||
}
|
||||
}
|
||||
let error =
|
||||
exec::execvp(path.clone(), &[path.as_os_str(), &OsString::from(ipc_url)]);
|
||||
// if exec succeeded, we never get here.
|
||||
eprintln!("failed to exec {:?}: {}", path, error);
|
||||
process::exit(1)
|
||||
}
|
||||
Err(_) => Err(anyhow!(io::Error::last_os_error())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &Path {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub fn spawn(&self, _args: Vec<String>) -> anyhow::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub fn zed_version_string(&self) -> String {
|
||||
unimplemented!()
|
||||
fn wait_for_socket(
|
||||
&self,
|
||||
sock_addr: &SocketAddr,
|
||||
sock: &mut UnixDatagram,
|
||||
) -> Result<(), std::io::Error> {
|
||||
for _ in 0..100 {
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
if sock.connect_addr(&sock_addr).is_ok() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
sock.connect_addr(&sock_addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,59 +256,79 @@ mod linux {
|
||||
// todo("windows")
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows {
|
||||
use crate::{Detect, InstalledApp};
|
||||
use std::path::Path;
|
||||
|
||||
use cli::{CliRequest, CliResponse};
|
||||
use ipc_channel::ipc::{IpcReceiver, IpcSender};
|
||||
|
||||
use crate::{Bundle, InfoPlist};
|
||||
|
||||
impl Bundle {
|
||||
pub fn detect(_args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
|
||||
struct App;
|
||||
impl InstalledApp for App {
|
||||
fn zed_version_string(&self) -> String {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub fn plist(&self) -> &InfoPlist {
|
||||
fn launch(&self, _ipc_url: String) -> anyhow::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &Path {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub fn spawn(&self, _args: Vec<String>) -> anyhow::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub fn zed_version_string(&self) -> String {
|
||||
unimplemented!()
|
||||
impl Detect {
|
||||
pub fn detect(_path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
|
||||
Ok(App)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod mac_os {
|
||||
use anyhow::{Context, Result};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use core_foundation::{
|
||||
array::{CFArray, CFIndex},
|
||||
string::kCFStringEncodingUTF8,
|
||||
url::{CFURLCreateWithBytes, CFURL},
|
||||
};
|
||||
use core_services::{kLSLaunchDefaults, LSLaunchURLSpec, LSOpenFromURLSpec, TCFType};
|
||||
use std::{fs, path::Path, process::Command, ptr};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
ptr,
|
||||
};
|
||||
|
||||
use cli::{CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME};
|
||||
use ipc_channel::ipc::{IpcOneShotServer, IpcReceiver, IpcSender};
|
||||
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
|
||||
|
||||
use crate::{locate_bundle, Bundle, InfoPlist};
|
||||
use crate::{Detect, InstalledApp};
|
||||
|
||||
impl Bundle {
|
||||
pub fn detect(args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
|
||||
let bundle_path = if let Some(bundle_path) = args_bundle_path {
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct InfoPlist {
|
||||
#[serde(rename = "CFBundleShortVersionString")]
|
||||
bundle_short_version_string: String,
|
||||
}
|
||||
|
||||
enum Bundle {
|
||||
App {
|
||||
app_bundle: PathBuf,
|
||||
plist: InfoPlist,
|
||||
},
|
||||
LocalPath {
|
||||
executable: PathBuf,
|
||||
plist: InfoPlist,
|
||||
},
|
||||
}
|
||||
|
||||
fn locate_bundle() -> Result<PathBuf> {
|
||||
let cli_path = std::env::current_exe()?.canonicalize()?;
|
||||
let mut app_path = cli_path.clone();
|
||||
while app_path.extension() != Some(OsStr::new("app")) {
|
||||
if !app_path.pop() {
|
||||
return Err(anyhow!("cannot find app bundle containing {:?}", cli_path));
|
||||
}
|
||||
}
|
||||
Ok(app_path)
|
||||
}
|
||||
|
||||
impl Detect {
|
||||
pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
|
||||
let bundle_path = if let Some(bundle_path) = path {
|
||||
bundle_path
|
||||
.canonicalize()
|
||||
.with_context(|| format!("Args bundle path {bundle_path:?} canonicalization"))?
|
||||
@@ -256,7 +343,7 @@ mod mac_os {
|
||||
plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
|
||||
format!("Reading *.app bundle plist file at {plist_path:?}")
|
||||
})?;
|
||||
Ok(Self::App {
|
||||
Ok(Bundle::App {
|
||||
app_bundle: bundle_path,
|
||||
plist,
|
||||
})
|
||||
@@ -271,42 +358,27 @@ mod mac_os {
|
||||
plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
|
||||
format!("Reading dev bundle plist file at {plist_path:?}")
|
||||
})?;
|
||||
Ok(Self::LocalPath {
|
||||
Ok(Bundle::LocalPath {
|
||||
executable: bundle_path,
|
||||
plist,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn plist(&self) -> &InfoPlist {
|
||||
match self {
|
||||
Self::App { plist, .. } => plist,
|
||||
Self::LocalPath { plist, .. } => plist,
|
||||
}
|
||||
impl InstalledApp for Bundle {
|
||||
fn zed_version_string(&self) -> String {
|
||||
let is_dev = matches!(self, Self::LocalPath { .. });
|
||||
format!(
|
||||
"Zed {}{} – {}",
|
||||
self.plist().bundle_short_version_string,
|
||||
if is_dev { " (dev)" } else { "" },
|
||||
self.path().display(),
|
||||
)
|
||||
}
|
||||
|
||||
fn path(&self) -> &Path {
|
||||
match self {
|
||||
Self::App { app_bundle, .. } => app_bundle,
|
||||
Self::LocalPath { executable, .. } => executable,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn(&self, args: Vec<String>) -> Result<()> {
|
||||
let path = match self {
|
||||
Self::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/zed"),
|
||||
Self::LocalPath { executable, .. } => executable.clone(),
|
||||
};
|
||||
Command::new(path).args(args).status()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
|
||||
let (server, server_name) =
|
||||
IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
|
||||
let url = format!("zed-cli://{server_name}");
|
||||
|
||||
fn launch(&self, url: String) -> anyhow::Result<()> {
|
||||
match self {
|
||||
Self::App { app_bundle, .. } => {
|
||||
let app_path = app_bundle;
|
||||
@@ -368,18 +440,23 @@ mod mac_os {
|
||||
}
|
||||
}
|
||||
|
||||
let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
|
||||
Ok((handshake.requests, handshake.responses))
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Bundle {
|
||||
fn plist(&self) -> &InfoPlist {
|
||||
match self {
|
||||
Self::App { plist, .. } => plist,
|
||||
Self::LocalPath { plist, .. } => plist,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn zed_version_string(&self) -> String {
|
||||
let is_dev = matches!(self, Self::LocalPath { .. });
|
||||
format!(
|
||||
"Zed {}{} – {}",
|
||||
self.plist().bundle_short_version_string,
|
||||
if is_dev { " (dev)" } else { "" },
|
||||
self.path().display(),
|
||||
)
|
||||
fn path(&self) -> &Path {
|
||||
match self {
|
||||
Self::App { app_bundle, .. } => app_bundle,
|
||||
Self::LocalPath { executable, .. } => executable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,39 +16,38 @@ doctest = false
|
||||
test-support = ["clock/test-support", "collections/test-support", "gpui/test-support", "rpc/test-support"]
|
||||
|
||||
[dependencies]
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
gpui.workspace = true
|
||||
util.workspace = true
|
||||
release_channel.workspace = true
|
||||
rpc.workspace = true
|
||||
text.workspace = true
|
||||
settings.workspace = true
|
||||
feature_flags.workspace = true
|
||||
|
||||
anyhow.workspace = true
|
||||
async-recursion = "0.3"
|
||||
async-tungstenite = { version = "0.16", features = ["async-std", "async-native-tls"] }
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
feature_flags.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
lazy_static.workspace = true
|
||||
log.workspace = true
|
||||
once_cell = "1.19.0"
|
||||
once_cell.workspace = true
|
||||
parking_lot.workspace = true
|
||||
postage.workspace = true
|
||||
rand.workspace = true
|
||||
release_channel.workspace = true
|
||||
rpc.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
sha2.workspace = true
|
||||
smol.workspace = true
|
||||
sysinfo.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
tempfile.workspace = true
|
||||
text.workspace = true
|
||||
thiserror.workspace = true
|
||||
time.workspace = true
|
||||
tiny_http = "0.8"
|
||||
url.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
clock = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -30,6 +30,7 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources, SettingsStore};
|
||||
use std::fmt;
|
||||
use std::pin::Pin;
|
||||
use std::{
|
||||
any::TypeId,
|
||||
convert::TryFrom,
|
||||
@@ -65,6 +66,13 @@ impl fmt::Display for DevServerToken {
|
||||
lazy_static! {
|
||||
static ref ZED_SERVER_URL: Option<String> = std::env::var("ZED_SERVER_URL").ok();
|
||||
static ref ZED_RPC_URL: Option<String> = std::env::var("ZED_RPC_URL").ok();
|
||||
/// An environment variable whose presence indicates that the development auth
|
||||
/// provider should be used.
|
||||
///
|
||||
/// Only works in development. Setting this environment variable in other release
|
||||
/// channels is a no-op.
|
||||
pub static ref ZED_DEVELOPMENT_AUTH: bool =
|
||||
std::env::var("ZED_DEVELOPMENT_AUTH").map_or(false, |value| !value.is_empty());
|
||||
pub static ref IMPERSONATE_LOGIN: Option<String> = std::env::var("ZED_IMPERSONATE")
|
||||
.ok()
|
||||
.and_then(|s| if s.is_empty() { None } else { Some(s) });
|
||||
@@ -100,7 +108,7 @@ impl Settings for ClientSettings {
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
|
||||
let mut result = sources.json_merge::<Self>()?;
|
||||
if let Some(server_url) = &*ZED_SERVER_URL {
|
||||
result.server_url = server_url.clone()
|
||||
result.server_url.clone_from(&server_url)
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
@@ -161,6 +169,7 @@ pub struct Client {
|
||||
peer: Arc<Peer>,
|
||||
http: Arc<HttpClientWithUrl>,
|
||||
telemetry: Arc<Telemetry>,
|
||||
credentials_provider: Arc<dyn CredentialsProvider + Send + Sync + 'static>,
|
||||
state: RwLock<ClientState>,
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
@@ -298,6 +307,32 @@ impl Credentials {
|
||||
}
|
||||
}
|
||||
|
||||
/// A provider for [`Credentials`].
|
||||
///
|
||||
/// Used to abstract over reading and writing credentials to some form of
|
||||
/// persistence (like the system keychain).
|
||||
trait CredentialsProvider {
|
||||
/// Reads the credentials from the provider.
|
||||
fn read_credentials<'a>(
|
||||
&'a self,
|
||||
cx: &'a AsyncAppContext,
|
||||
) -> Pin<Box<dyn Future<Output = Option<Credentials>> + 'a>>;
|
||||
|
||||
/// Writes the credentials to the provider.
|
||||
fn write_credentials<'a>(
|
||||
&'a self,
|
||||
user_id: u64,
|
||||
access_token: String,
|
||||
cx: &'a AsyncAppContext,
|
||||
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>>;
|
||||
|
||||
/// Deletes the credentials from the provider.
|
||||
fn delete_credentials<'a>(
|
||||
&'a self,
|
||||
cx: &'a AsyncAppContext,
|
||||
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>>;
|
||||
}
|
||||
|
||||
impl Default for ClientState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@@ -443,11 +478,27 @@ impl Client {
|
||||
http: Arc<HttpClientWithUrl>,
|
||||
cx: &mut AppContext,
|
||||
) -> Arc<Self> {
|
||||
let use_zed_development_auth = match ReleaseChannel::try_global(cx) {
|
||||
Some(ReleaseChannel::Dev) => *ZED_DEVELOPMENT_AUTH,
|
||||
Some(ReleaseChannel::Nightly | ReleaseChannel::Preview | ReleaseChannel::Stable)
|
||||
| None => false,
|
||||
};
|
||||
|
||||
let credentials_provider: Arc<dyn CredentialsProvider + Send + Sync + 'static> =
|
||||
if use_zed_development_auth {
|
||||
Arc::new(DevelopmentCredentialsProvider {
|
||||
path: util::paths::CONFIG_DIR.join("development_auth"),
|
||||
})
|
||||
} else {
|
||||
Arc::new(KeychainCredentialsProvider)
|
||||
};
|
||||
|
||||
Arc::new(Self {
|
||||
id: AtomicU64::new(0),
|
||||
peer: Peer::new(0),
|
||||
telemetry: Telemetry::new(clock, http.clone(), cx),
|
||||
http,
|
||||
credentials_provider,
|
||||
state: Default::default(),
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
@@ -763,8 +814,11 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool {
|
||||
read_credentials_from_keychain(cx).await.is_some()
|
||||
pub async fn has_credentials(&self, cx: &AsyncAppContext) -> bool {
|
||||
self.credentials_provider
|
||||
.read_credentials(cx)
|
||||
.await
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub fn set_dev_server_token(&self, token: DevServerToken) -> &Self {
|
||||
@@ -775,7 +829,7 @@ impl Client {
|
||||
#[async_recursion(?Send)]
|
||||
pub async fn authenticate_and_connect(
|
||||
self: &Arc<Self>,
|
||||
try_keychain: bool,
|
||||
try_provider: bool,
|
||||
cx: &AsyncAppContext,
|
||||
) -> anyhow::Result<()> {
|
||||
let was_disconnected = match *self.status().borrow() {
|
||||
@@ -796,12 +850,13 @@ impl Client {
|
||||
self.set_status(Status::Reauthenticating, cx)
|
||||
}
|
||||
|
||||
let mut read_from_keychain = false;
|
||||
let mut read_from_provider = false;
|
||||
let mut credentials = self.state.read().credentials.clone();
|
||||
if credentials.is_none() && try_keychain {
|
||||
credentials = read_credentials_from_keychain(cx).await;
|
||||
read_from_keychain = credentials.is_some();
|
||||
if credentials.is_none() && try_provider {
|
||||
credentials = self.credentials_provider.read_credentials(cx).await;
|
||||
read_from_provider = credentials.is_some();
|
||||
}
|
||||
|
||||
if credentials.is_none() {
|
||||
let mut status_rx = self.status();
|
||||
let _ = status_rx.next().await;
|
||||
@@ -838,9 +893,9 @@ impl Client {
|
||||
match connection {
|
||||
Ok(conn) => {
|
||||
self.state.write().credentials = Some(credentials.clone());
|
||||
if !read_from_keychain && IMPERSONATE_LOGIN.is_none() {
|
||||
if !read_from_provider && IMPERSONATE_LOGIN.is_none() {
|
||||
if let Credentials::User{user_id, access_token} = credentials {
|
||||
write_credentials_to_keychain(user_id, access_token, cx).await.log_err();
|
||||
self.credentials_provider.write_credentials(user_id, access_token, cx).await.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -854,8 +909,8 @@ impl Client {
|
||||
}
|
||||
Err(EstablishConnectionError::Unauthorized) => {
|
||||
self.state.write().credentials.take();
|
||||
if read_from_keychain {
|
||||
delete_credentials_from_keychain(cx).await.log_err();
|
||||
if read_from_provider {
|
||||
self.credentials_provider.delete_credentials(cx).await.log_err();
|
||||
self.set_status(Status::SignedOut, cx);
|
||||
self.authenticate_and_connect(false, cx).await
|
||||
} else {
|
||||
@@ -1264,8 +1319,11 @@ impl Client {
|
||||
self.state.write().credentials = None;
|
||||
self.disconnect(&cx);
|
||||
|
||||
if self.has_keychain_credentials(cx).await {
|
||||
delete_credentials_from_keychain(cx).await.log_err();
|
||||
if self.has_credentials(cx).await {
|
||||
self.credentials_provider
|
||||
.delete_credentials(cx)
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1465,41 +1523,128 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
|
||||
if IMPERSONATE_LOGIN.is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (user_id, access_token) = cx
|
||||
.update(|cx| cx.read_credentials(&ClientSettings::get_global(cx).server_url))
|
||||
.log_err()?
|
||||
.await
|
||||
.log_err()??;
|
||||
|
||||
Some(Credentials::User {
|
||||
user_id: user_id.parse().ok()?,
|
||||
access_token: String::from_utf8(access_token).ok()?,
|
||||
})
|
||||
}
|
||||
|
||||
async fn write_credentials_to_keychain(
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct DevelopmentCredentials {
|
||||
user_id: u64,
|
||||
access_token: String,
|
||||
cx: &AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
cx.update(move |cx| {
|
||||
cx.write_credentials(
|
||||
&ClientSettings::get_global(cx).server_url,
|
||||
&user_id.to_string(),
|
||||
access_token.as_bytes(),
|
||||
)
|
||||
})?
|
||||
.await
|
||||
}
|
||||
|
||||
async fn delete_credentials_from_keychain(cx: &AsyncAppContext) -> Result<()> {
|
||||
cx.update(move |cx| cx.delete_credentials(&ClientSettings::get_global(cx).server_url))?
|
||||
.await
|
||||
/// A credentials provider that stores credentials in a local file.
|
||||
///
|
||||
/// This MUST only be used in development, as this is not a secure way of storing
|
||||
/// credentials on user machines.
|
||||
///
|
||||
/// Its existence is purely to work around the annoyance of having to constantly
|
||||
/// re-allow access to the system keychain when developing Zed.
|
||||
struct DevelopmentCredentialsProvider {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl CredentialsProvider for DevelopmentCredentialsProvider {
|
||||
fn read_credentials<'a>(
|
||||
&'a self,
|
||||
_cx: &'a AsyncAppContext,
|
||||
) -> Pin<Box<dyn Future<Output = Option<Credentials>> + 'a>> {
|
||||
async move {
|
||||
if IMPERSONATE_LOGIN.is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let json = std::fs::read(&self.path).log_err()?;
|
||||
|
||||
let credentials: DevelopmentCredentials = serde_json::from_slice(&json).log_err()?;
|
||||
|
||||
Some(Credentials::User {
|
||||
user_id: credentials.user_id,
|
||||
access_token: credentials.access_token,
|
||||
})
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
|
||||
fn write_credentials<'a>(
|
||||
&'a self,
|
||||
user_id: u64,
|
||||
access_token: String,
|
||||
_cx: &'a AsyncAppContext,
|
||||
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
|
||||
async move {
|
||||
let json = serde_json::to_string(&DevelopmentCredentials {
|
||||
user_id,
|
||||
access_token,
|
||||
})?;
|
||||
|
||||
std::fs::write(&self.path, json)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
|
||||
fn delete_credentials<'a>(
|
||||
&'a self,
|
||||
_cx: &'a AsyncAppContext,
|
||||
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
|
||||
async move { Ok(std::fs::remove_file(&self.path)?) }.boxed_local()
|
||||
}
|
||||
}
|
||||
|
||||
/// A credentials provider that stores credentials in the system keychain.
|
||||
struct KeychainCredentialsProvider;
|
||||
|
||||
impl CredentialsProvider for KeychainCredentialsProvider {
|
||||
fn read_credentials<'a>(
|
||||
&'a self,
|
||||
cx: &'a AsyncAppContext,
|
||||
) -> Pin<Box<dyn Future<Output = Option<Credentials>> + 'a>> {
|
||||
async move {
|
||||
if IMPERSONATE_LOGIN.is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (user_id, access_token) = cx
|
||||
.update(|cx| cx.read_credentials(&ClientSettings::get_global(cx).server_url))
|
||||
.log_err()?
|
||||
.await
|
||||
.log_err()??;
|
||||
|
||||
Some(Credentials::User {
|
||||
user_id: user_id.parse().ok()?,
|
||||
access_token: String::from_utf8(access_token).ok()?,
|
||||
})
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
|
||||
fn write_credentials<'a>(
|
||||
&'a self,
|
||||
user_id: u64,
|
||||
access_token: String,
|
||||
cx: &'a AsyncAppContext,
|
||||
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
|
||||
async move {
|
||||
cx.update(move |cx| {
|
||||
cx.write_credentials(
|
||||
&ClientSettings::get_global(cx).server_url,
|
||||
&user_id.to_string(),
|
||||
access_token.as_bytes(),
|
||||
)
|
||||
})?
|
||||
.await
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
|
||||
fn delete_credentials<'a>(
|
||||
&'a self,
|
||||
cx: &'a AsyncAppContext,
|
||||
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
|
||||
async move {
|
||||
cx.update(move |cx| cx.delete_credentials(&ClientSettings::get_global(cx).server_url))?
|
||||
.await
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
}
|
||||
|
||||
/// prefix for the zed:// url scheme
|
||||
|
||||
@@ -217,7 +217,7 @@ impl Telemetry {
|
||||
}
|
||||
|
||||
let metrics_id: Option<Arc<str>> = metrics_id.map(|id| id.into());
|
||||
state.metrics_id = metrics_id.clone();
|
||||
state.metrics_id.clone_from(&metrics_id);
|
||||
state.is_staff = Some(is_staff);
|
||||
drop(state);
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ live_kit_server.workspace = true
|
||||
log.workspace = true
|
||||
nanoid.workspace = true
|
||||
open_ai.workspace = true
|
||||
supermaven_api.workspace = true
|
||||
parking_lot.workspace = true
|
||||
prometheus = "0.13"
|
||||
prost.workspace = true
|
||||
@@ -82,6 +83,7 @@ env_logger.workspace = true
|
||||
file_finder.workspace = true
|
||||
fs = { workspace = true, features = ["test-support"] }
|
||||
git = { workspace = true, features = ["test-support"] }
|
||||
git_hosting_providers.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
indoc.workspace = true
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -172,6 +172,11 @@ spec:
|
||||
secretKeyRef:
|
||||
name: slack
|
||||
key: panics_webhook
|
||||
- name: SUPERMAVEN_ADMIN_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: supermaven
|
||||
key: api_key
|
||||
- name: INVITE_LINK_PREFIX
|
||||
value: ${INVITE_LINK_PREFIX}
|
||||
- name: RUST_BACKTRACE
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE projects DROP COLUMN remote_project_id;
|
||||
DROP TABLE remote_projects;
|
||||
@@ -116,13 +116,6 @@ struct CreateUserParams {
|
||||
invite_count: i32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
struct CreateUserResponse {
|
||||
user: User,
|
||||
signup_device_id: Option<String>,
|
||||
metrics_id: String,
|
||||
}
|
||||
|
||||
async fn get_rpc_server_snapshot(
|
||||
Extension(rpc_server): Extension<Option<Arc<rpc::Server>>>,
|
||||
) -> Result<ErasedJson> {
|
||||
|
||||
2
crates/collab/src/completion.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use rpc::proto;
|
||||
@@ -415,7 +415,7 @@ impl Database {
|
||||
if is_serialization_error(error) && prev_attempt_count < SLEEPS.len() {
|
||||
let base_delay = SLEEPS[prev_attempt_count];
|
||||
let randomized_delay = base_delay * self.rng.lock().await.gen_range(0.5..=2.0);
|
||||
log::info!(
|
||||
log::warn!(
|
||||
"retrying transaction after serialization error. delay: {} ms.",
|
||||
randomized_delay
|
||||
);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use anyhow::anyhow;
|
||||
use rpc::{proto, ConnectionId};
|
||||
use rpc::{
|
||||
proto::{self},
|
||||
ConnectionId,
|
||||
};
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ActiveValue, ColumnTrait, Condition, DatabaseTransaction, EntityTrait,
|
||||
ModelTrait, QueryFilter,
|
||||
@@ -35,24 +38,33 @@ impl Database {
|
||||
dev_server_id: DevServerId,
|
||||
) -> crate::Result<Vec<proto::DevServerProject>> {
|
||||
self.transaction(|tx| async move {
|
||||
let servers = dev_server_project::Entity::find()
|
||||
.filter(dev_server_project::Column::DevServerId.eq(dev_server_id))
|
||||
.find_also_related(project::Entity)
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
Ok(servers
|
||||
.into_iter()
|
||||
.map(|(dev_server_project, project)| proto::DevServerProject {
|
||||
id: dev_server_project.id.to_proto(),
|
||||
project_id: project.map(|p| p.id.to_proto()),
|
||||
dev_server_id: dev_server_project.dev_server_id.to_proto(),
|
||||
path: dev_server_project.path,
|
||||
})
|
||||
.collect())
|
||||
self.get_projects_for_dev_server_internal(dev_server_id, &tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_projects_for_dev_server_internal(
|
||||
&self,
|
||||
dev_server_id: DevServerId,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> crate::Result<Vec<proto::DevServerProject>> {
|
||||
let servers = dev_server_project::Entity::find()
|
||||
.filter(dev_server_project::Column::DevServerId.eq(dev_server_id))
|
||||
.find_also_related(project::Entity)
|
||||
.all(tx)
|
||||
.await?;
|
||||
Ok(servers
|
||||
.into_iter()
|
||||
.map(|(dev_server_project, project)| proto::DevServerProject {
|
||||
id: dev_server_project.id.to_proto(),
|
||||
project_id: project.map(|p| p.id.to_proto()),
|
||||
dev_server_id: dev_server_project.dev_server_id.to_proto(),
|
||||
path: dev_server_project.path,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn dev_server_project_ids_for_user(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
@@ -136,6 +148,39 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_dev_server_project(
|
||||
&self,
|
||||
dev_server_project_id: DevServerProjectId,
|
||||
dev_server_id: DevServerId,
|
||||
user_id: UserId,
|
||||
) -> crate::Result<(Vec<proto::DevServerProject>, proto::DevServerProjectsUpdate)> {
|
||||
self.transaction(|tx| async move {
|
||||
project::Entity::delete_many()
|
||||
.filter(project::Column::DevServerProjectId.eq(dev_server_project_id))
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
let result = dev_server_project::Entity::delete_by_id(dev_server_project_id)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
if result.rows_affected != 1 {
|
||||
return Err(anyhow!(
|
||||
"no dev server project with id {}",
|
||||
dev_server_project_id
|
||||
))?;
|
||||
}
|
||||
|
||||
let status = self
|
||||
.dev_server_projects_update_internal(user_id, &tx)
|
||||
.await?;
|
||||
|
||||
let projects = self
|
||||
.get_projects_for_dev_server_internal(dev_server_id, &tx)
|
||||
.await?;
|
||||
Ok((projects, status))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn share_dev_server_project(
|
||||
&self,
|
||||
dev_server_project_id: DevServerProjectId,
|
||||
|
||||
@@ -77,10 +77,14 @@ impl Database {
|
||||
user_id: UserId,
|
||||
) -> crate::Result<(dev_server::Model, proto::DevServerProjectsUpdate)> {
|
||||
self.transaction(|tx| async move {
|
||||
if name.trim().is_empty() {
|
||||
return Err(anyhow::anyhow!(proto::ErrorCode::Forbidden))?;
|
||||
}
|
||||
|
||||
let dev_server = dev_server::Entity::insert(dev_server::ActiveModel {
|
||||
id: ActiveValue::NotSet,
|
||||
hashed_token: ActiveValue::Set(hashed_access_token.to_string()),
|
||||
name: ActiveValue::Set(name.to_string()),
|
||||
name: ActiveValue::Set(name.trim().to_string()),
|
||||
user_id: ActiveValue::Set(user_id),
|
||||
})
|
||||
.exec_with_returning(&*tx)
|
||||
@@ -95,6 +99,66 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn update_dev_server_token(
|
||||
&self,
|
||||
id: DevServerId,
|
||||
hashed_token: &str,
|
||||
user_id: UserId,
|
||||
) -> crate::Result<proto::DevServerProjectsUpdate> {
|
||||
self.transaction(|tx| async move {
|
||||
let Some(dev_server) = dev_server::Entity::find_by_id(id).one(&*tx).await? else {
|
||||
return Err(anyhow::anyhow!("no dev server with id {}", id))?;
|
||||
};
|
||||
if dev_server.user_id != user_id {
|
||||
return Err(anyhow::anyhow!(proto::ErrorCode::Forbidden))?;
|
||||
}
|
||||
|
||||
dev_server::Entity::update(dev_server::ActiveModel {
|
||||
hashed_token: ActiveValue::Set(hashed_token.to_string()),
|
||||
..dev_server.clone().into_active_model()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
let dev_server_projects = self
|
||||
.dev_server_projects_update_internal(user_id, &tx)
|
||||
.await?;
|
||||
|
||||
Ok(dev_server_projects)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn rename_dev_server(
|
||||
&self,
|
||||
id: DevServerId,
|
||||
name: &str,
|
||||
user_id: UserId,
|
||||
) -> crate::Result<proto::DevServerProjectsUpdate> {
|
||||
self.transaction(|tx| async move {
|
||||
let Some(dev_server) = dev_server::Entity::find_by_id(id).one(&*tx).await? else {
|
||||
return Err(anyhow::anyhow!("no dev server with id {}", id))?;
|
||||
};
|
||||
if dev_server.user_id != user_id || name.trim().is_empty() {
|
||||
return Err(anyhow::anyhow!(proto::ErrorCode::Forbidden))?;
|
||||
}
|
||||
|
||||
dev_server::Entity::update(dev_server::ActiveModel {
|
||||
name: ActiveValue::Set(name.trim().to_string()),
|
||||
..dev_server.clone().into_active_model()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
let dev_server_projects = self
|
||||
.dev_server_projects_update_internal(user_id, &tx)
|
||||
.await?;
|
||||
|
||||
Ok(dev_server_projects)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_dev_server(
|
||||
&self,
|
||||
id: DevServerId,
|
||||
|
||||
@@ -78,7 +78,6 @@ impl Database {
|
||||
.await?;
|
||||
|
||||
// todo! check user is a project-collaborator
|
||||
|
||||
let room = self.get_room(room_id, &tx).await?;
|
||||
return Ok((project.id, room));
|
||||
}
|
||||
@@ -131,13 +130,21 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_project(&self, project_id: ProjectId) -> Result<()> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
project::Entity::delete_by_id(project_id).exec(&*tx).await?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Unshares the given project.
|
||||
pub async fn unshare_project(
|
||||
&self,
|
||||
project_id: ProjectId,
|
||||
connection: ConnectionId,
|
||||
user_id: Option<UserId>,
|
||||
) -> Result<TransactionGuard<(Option<proto::Room>, Vec<ConnectionId>)>> {
|
||||
) -> Result<TransactionGuard<(bool, Option<proto::Room>, Vec<ConnectionId>)>> {
|
||||
self.project_transaction(project_id, |tx| async move {
|
||||
let guest_connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
|
||||
let project = project::Entity::find_by_id(project_id)
|
||||
@@ -150,10 +157,7 @@ impl Database {
|
||||
None
|
||||
};
|
||||
if project.host_connection()? == connection {
|
||||
project::Entity::delete(project.into_active_model())
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
return Ok((room, guest_connection_ids));
|
||||
return Ok((true, room, guest_connection_ids));
|
||||
}
|
||||
if let Some(dev_server_project_id) = project.dev_server_project_id {
|
||||
if let Some(user_id) = user_id {
|
||||
@@ -170,7 +174,7 @@ impl Database {
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
return Ok((room, guest_connection_ids));
|
||||
return Ok((false, room, guest_connection_ids));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -598,6 +602,17 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn find_dev_server_project(&self, id: DevServerProjectId) -> Result<project::Model> {
|
||||
self.transaction(|tx| async move {
|
||||
Ok(project::Entity::find()
|
||||
.filter(project::Column::DevServerProjectId.eq(id))
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("no such project"))?)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Adds the given connection to the specified project
|
||||
/// in the current room.
|
||||
pub async fn join_project(
|
||||
|
||||
@@ -138,6 +138,7 @@ pub struct Config {
|
||||
pub zed_client_checksum_seed: Option<String>,
|
||||
pub slack_panics_webhook: Option<String>,
|
||||
pub auto_join_channel_id: Option<ChannelId>,
|
||||
pub supermaven_admin_api_key: Option<Arc<str>>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
|
||||
@@ -34,6 +34,7 @@ pub use connection_pool::{ConnectionPool, ZedVersion};
|
||||
use core::fmt::{self, Debug, Formatter};
|
||||
use open_ai::{OpenAiEmbeddingModel, OPEN_AI_API_URL};
|
||||
use sha2::Digest;
|
||||
use supermaven_api::{CreateExternalUserRequest, SupermavenAdminApi};
|
||||
|
||||
use futures::{
|
||||
channel::oneshot,
|
||||
@@ -148,7 +149,8 @@ struct Session {
|
||||
peer: Arc<Peer>,
|
||||
connection_pool: Arc<parking_lot::Mutex<ConnectionPool>>,
|
||||
live_kit_client: Option<Arc<dyn live_kit_server::api::Client>>,
|
||||
http_client: IsahcHttpClient,
|
||||
supermaven_client: Option<Arc<SupermavenAdminApi>>,
|
||||
http_client: Arc<IsahcHttpClient>,
|
||||
rate_limiter: Arc<RateLimiter>,
|
||||
_executor: Executor,
|
||||
}
|
||||
@@ -189,6 +191,14 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_staff(&self) -> bool {
|
||||
match &self.principal {
|
||||
Principal::User(user) => user.admin,
|
||||
Principal::Impersonated { .. } => true,
|
||||
Principal::DevServer(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn dev_server_id(&self) -> Option<DevServerId> {
|
||||
match &self.principal {
|
||||
Principal::User(_) | Principal::Impersonated { .. } => None,
|
||||
@@ -233,6 +243,14 @@ impl UserSession {
|
||||
pub fn user_id(&self) -> UserId {
|
||||
self.0.user_id().unwrap()
|
||||
}
|
||||
|
||||
pub fn email(&self) -> Option<String> {
|
||||
match &self.0.principal {
|
||||
Principal::User(user) => user.email_address.clone(),
|
||||
Principal::Impersonated { user, .. } => user.email_address.clone(),
|
||||
Principal::DevServer(..) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for UserSession {
|
||||
@@ -413,7 +431,10 @@ impl Server {
|
||||
.add_request_handler(user_handler(join_hosted_project))
|
||||
.add_request_handler(user_handler(rejoin_dev_server_projects))
|
||||
.add_request_handler(user_handler(create_dev_server_project))
|
||||
.add_request_handler(user_handler(delete_dev_server_project))
|
||||
.add_request_handler(user_handler(create_dev_server))
|
||||
.add_request_handler(user_handler(regenerate_dev_server_token))
|
||||
.add_request_handler(user_handler(rename_dev_server))
|
||||
.add_request_handler(user_handler(delete_dev_server))
|
||||
.add_request_handler(dev_server_handler(share_dev_server_project))
|
||||
.add_request_handler(dev_server_handler(shutdown_dev_server))
|
||||
@@ -560,6 +581,7 @@ impl Server {
|
||||
.add_request_handler(user_handler(get_private_user_info))
|
||||
.add_message_handler(user_message_handler(acknowledge_channel_message))
|
||||
.add_message_handler(user_message_handler(acknowledge_buffer_version))
|
||||
.add_request_handler(user_handler(get_supermaven_api_key))
|
||||
.add_streaming_request_handler({
|
||||
let app_state = app_state.clone();
|
||||
move |request, response, session| {
|
||||
@@ -937,13 +959,22 @@ impl Server {
|
||||
tracing::info!("connection opened");
|
||||
|
||||
let http_client = match IsahcHttpClient::new() {
|
||||
Ok(http_client) => http_client,
|
||||
Ok(http_client) => Arc::new(http_client),
|
||||
Err(error) => {
|
||||
tracing::error!(?error, "failed to create HTTP client");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let supermaven_client = if let Some(supermaven_admin_api_key) = this.app_state.config.supermaven_admin_api_key.clone() {
|
||||
Some(Arc::new(SupermavenAdminApi::new(
|
||||
supermaven_admin_api_key.to_string(),
|
||||
http_client.clone(),
|
||||
)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let session = Session {
|
||||
principal: principal.clone(),
|
||||
connection_id,
|
||||
@@ -954,6 +985,7 @@ impl Server {
|
||||
http_client,
|
||||
rate_limiter: this.app_state.rate_limiter.clone(),
|
||||
_executor: executor.clone(),
|
||||
supermaven_client,
|
||||
};
|
||||
|
||||
if let Err(error) = this.send_initial_client_update(connection_id, &principal, zed_version, send_connection_id, &session).await {
|
||||
@@ -2000,23 +2032,34 @@ async fn unshare_project_internal(
|
||||
user_id: Option<UserId>,
|
||||
session: &Session,
|
||||
) -> Result<()> {
|
||||
let (room, guest_connection_ids) = &*session
|
||||
.db()
|
||||
.await
|
||||
.unshare_project(project_id, connection_id, user_id)
|
||||
.await?;
|
||||
let delete = {
|
||||
let room_guard = session
|
||||
.db()
|
||||
.await
|
||||
.unshare_project(project_id, connection_id, user_id)
|
||||
.await?;
|
||||
|
||||
let message = proto::UnshareProject {
|
||||
project_id: project_id.to_proto(),
|
||||
let (delete, room, guest_connection_ids) = &*room_guard;
|
||||
|
||||
let message = proto::UnshareProject {
|
||||
project_id: project_id.to_proto(),
|
||||
};
|
||||
|
||||
broadcast(
|
||||
Some(connection_id),
|
||||
guest_connection_ids.iter().copied(),
|
||||
|conn_id| session.peer.send(conn_id, message.clone()),
|
||||
);
|
||||
if let Some(room) = room {
|
||||
room_updated(room, &session.peer);
|
||||
}
|
||||
|
||||
*delete
|
||||
};
|
||||
|
||||
broadcast(
|
||||
Some(connection_id),
|
||||
guest_connection_ids.iter().copied(),
|
||||
|conn_id| session.peer.send(conn_id, message.clone()),
|
||||
);
|
||||
if let Some(room) = room {
|
||||
room_updated(room, &session.peer);
|
||||
if delete {
|
||||
let db = session.db().await;
|
||||
db.delete_project(project_id).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -2313,6 +2356,12 @@ async fn create_dev_server(
|
||||
let access_token = auth::random_token();
|
||||
let hashed_access_token = auth::hash_access_token(&access_token);
|
||||
|
||||
if request.name.is_empty() {
|
||||
return Err(proto::ErrorCode::Forbidden
|
||||
.message("Dev server name cannot be empty".to_string())
|
||||
.anyhow())?;
|
||||
}
|
||||
|
||||
let (dev_server, status) = session
|
||||
.db()
|
||||
.await
|
||||
@@ -2329,6 +2378,71 @@ async fn create_dev_server(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn regenerate_dev_server_token(
|
||||
request: proto::RegenerateDevServerToken,
|
||||
response: Response<proto::RegenerateDevServerToken>,
|
||||
session: UserSession,
|
||||
) -> Result<()> {
|
||||
let dev_server_id = DevServerId(request.dev_server_id as i32);
|
||||
let access_token = auth::random_token();
|
||||
let hashed_access_token = auth::hash_access_token(&access_token);
|
||||
|
||||
let connection_id = session
|
||||
.connection_pool()
|
||||
.await
|
||||
.dev_server_connection_id(dev_server_id);
|
||||
if let Some(connection_id) = connection_id {
|
||||
shutdown_dev_server_internal(dev_server_id, connection_id, &session).await?;
|
||||
session
|
||||
.peer
|
||||
.send(connection_id, proto::ShutdownDevServer {})?;
|
||||
let _ = remove_dev_server_connection(dev_server_id, &session).await;
|
||||
}
|
||||
|
||||
let status = session
|
||||
.db()
|
||||
.await
|
||||
.update_dev_server_token(dev_server_id, &hashed_access_token, session.user_id())
|
||||
.await?;
|
||||
|
||||
send_dev_server_projects_update(session.user_id(), status, &session).await;
|
||||
|
||||
response.send(proto::RegenerateDevServerTokenResponse {
|
||||
dev_server_id: dev_server_id.to_proto(),
|
||||
access_token: auth::generate_dev_server_token(dev_server_id.0 as usize, access_token),
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn rename_dev_server(
|
||||
request: proto::RenameDevServer,
|
||||
response: Response<proto::RenameDevServer>,
|
||||
session: UserSession,
|
||||
) -> Result<()> {
|
||||
if request.name.trim().is_empty() {
|
||||
return Err(proto::ErrorCode::Forbidden
|
||||
.message("Dev server name cannot be empty".to_string())
|
||||
.anyhow())?;
|
||||
}
|
||||
|
||||
let dev_server_id = DevServerId(request.dev_server_id as i32);
|
||||
let dev_server = session.db().await.get_dev_server(dev_server_id).await?;
|
||||
if dev_server.user_id != session.user_id() {
|
||||
return Err(anyhow!(ErrorCode::Forbidden))?;
|
||||
}
|
||||
|
||||
let status = session
|
||||
.db()
|
||||
.await
|
||||
.rename_dev_server(dev_server_id, &request.name, session.user_id())
|
||||
.await?;
|
||||
|
||||
send_dev_server_projects_update(session.user_id(), status, &session).await;
|
||||
|
||||
response.send(proto::Ack {})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_dev_server(
|
||||
request: proto::DeleteDevServer,
|
||||
response: Response<proto::DeleteDevServer>,
|
||||
@@ -2349,6 +2463,7 @@ async fn delete_dev_server(
|
||||
session
|
||||
.peer
|
||||
.send(connection_id, proto::ShutdownDevServer {})?;
|
||||
let _ = remove_dev_server_connection(dev_server_id, &session).await;
|
||||
}
|
||||
|
||||
let status = session
|
||||
@@ -2363,6 +2478,68 @@ async fn delete_dev_server(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_dev_server_project(
|
||||
request: proto::DeleteDevServerProject,
|
||||
response: Response<proto::DeleteDevServerProject>,
|
||||
session: UserSession,
|
||||
) -> Result<()> {
|
||||
let dev_server_project_id = DevServerProjectId(request.dev_server_project_id as i32);
|
||||
let dev_server_project = session
|
||||
.db()
|
||||
.await
|
||||
.get_dev_server_project(dev_server_project_id)
|
||||
.await?;
|
||||
|
||||
let dev_server = session
|
||||
.db()
|
||||
.await
|
||||
.get_dev_server(dev_server_project.dev_server_id)
|
||||
.await?;
|
||||
if dev_server.user_id != session.user_id() {
|
||||
return Err(anyhow!(ErrorCode::Forbidden))?;
|
||||
}
|
||||
|
||||
let dev_server_connection_id = session
|
||||
.connection_pool()
|
||||
.await
|
||||
.dev_server_connection_id(dev_server.id);
|
||||
|
||||
if let Some(dev_server_connection_id) = dev_server_connection_id {
|
||||
let project = session
|
||||
.db()
|
||||
.await
|
||||
.find_dev_server_project(dev_server_project_id)
|
||||
.await;
|
||||
if let Ok(project) = project {
|
||||
unshare_project_internal(
|
||||
project.id,
|
||||
dev_server_connection_id,
|
||||
Some(session.user_id()),
|
||||
&session,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
let (projects, status) = session
|
||||
.db()
|
||||
.await
|
||||
.delete_dev_server_project(dev_server_project_id, dev_server.id, session.user_id())
|
||||
.await?;
|
||||
|
||||
if let Some(dev_server_connection_id) = dev_server_connection_id {
|
||||
session.peer.send(
|
||||
dev_server_connection_id,
|
||||
proto::DevServerInstructions { projects },
|
||||
)?;
|
||||
}
|
||||
|
||||
send_dev_server_projects_update(session.user_id(), status, &session).await;
|
||||
|
||||
response.send(proto::Ack {})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn rejoin_dev_server_projects(
|
||||
request: proto::RejoinRemoteProjects,
|
||||
response: Response<proto::RejoinRemoteProjects>,
|
||||
@@ -2459,7 +2636,8 @@ async fn shutdown_dev_server(
|
||||
session: DevServerSession,
|
||||
) -> Result<()> {
|
||||
response.send(proto::Ack {})?;
|
||||
shutdown_dev_server_internal(session.dev_server_id(), session.connection_id, &session).await
|
||||
shutdown_dev_server_internal(session.dev_server_id(), session.connection_id, &session).await?;
|
||||
remove_dev_server_connection(session.dev_server_id(), &session).await
|
||||
}
|
||||
|
||||
async fn shutdown_dev_server_internal(
|
||||
@@ -2499,6 +2677,21 @@ async fn shutdown_dev_server_internal(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_dev_server_connection(dev_server_id: DevServerId, session: &Session) -> Result<()> {
|
||||
let dev_server_connection = session
|
||||
.connection_pool()
|
||||
.await
|
||||
.dev_server_connection_id(dev_server_id);
|
||||
|
||||
if let Some(dev_server_connection) = dev_server_connection {
|
||||
session
|
||||
.connection_pool()
|
||||
.await
|
||||
.remove_connection(dev_server_connection)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates other participants with changes to the project
|
||||
async fn update_project(
|
||||
request: proto::UpdateProject,
|
||||
@@ -4147,7 +4340,7 @@ async fn complete_with_open_ai(
|
||||
api_key: Arc<str>,
|
||||
) -> Result<()> {
|
||||
let mut completion_stream = open_ai::stream_completion(
|
||||
&session.http_client,
|
||||
session.http_client.as_ref(),
|
||||
OPEN_AI_API_URL,
|
||||
&api_key,
|
||||
crate::ai::language_model_request_to_open_ai(request)?,
|
||||
@@ -4211,7 +4404,7 @@ async fn complete_with_google_ai(
|
||||
api_key: Arc<str>,
|
||||
) -> Result<()> {
|
||||
let mut stream = google_ai::stream_generate_content(
|
||||
&session.http_client,
|
||||
session.http_client.clone(),
|
||||
google_ai::API_URL,
|
||||
api_key.as_ref(),
|
||||
crate::ai::language_model_request_to_google_ai(request)?,
|
||||
@@ -4295,7 +4488,7 @@ async fn complete_with_anthropic(
|
||||
.collect();
|
||||
|
||||
let mut stream = anthropic::stream_completion(
|
||||
&session.http_client,
|
||||
session.http_client.clone(),
|
||||
"https://api.anthropic.com",
|
||||
&api_key,
|
||||
anthropic::Request {
|
||||
@@ -4419,7 +4612,7 @@ async fn count_tokens_with_language_model(
|
||||
let api_key = google_ai_api_key
|
||||
.ok_or_else(|| anyhow!("no Google AI API key configured on the server"))?;
|
||||
let tokens_response = google_ai::count_tokens(
|
||||
&session.http_client,
|
||||
session.http_client.as_ref(),
|
||||
google_ai::API_URL,
|
||||
&api_key,
|
||||
crate::ai::count_tokens_request_to_google_ai(request)?,
|
||||
@@ -4438,7 +4631,7 @@ impl RateLimit for ComputeEmbeddingsRateLimit {
|
||||
std::env::var("EMBED_TEXTS_RATE_LIMIT_PER_HOUR")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(120) // Picked arbitrarily
|
||||
.unwrap_or(5000) // Picked arbitrarily
|
||||
}
|
||||
|
||||
fn refill_duration() -> chrono::Duration {
|
||||
@@ -4467,7 +4660,7 @@ async fn compute_embeddings(
|
||||
let embeddings = match request.model.as_str() {
|
||||
"openai/text-embedding-3-small" => {
|
||||
open_ai::embed(
|
||||
&session.http_client,
|
||||
session.http_client.as_ref(),
|
||||
OPEN_AI_API_URL,
|
||||
&api_key,
|
||||
OpenAiEmbeddingModel::TextEmbedding3Small,
|
||||
@@ -4510,25 +4703,6 @@ async fn compute_embeddings(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct GetCachedEmbeddingsRateLimit;
|
||||
|
||||
impl RateLimit for GetCachedEmbeddingsRateLimit {
|
||||
fn capacity() -> usize {
|
||||
std::env::var("EMBED_TEXTS_RATE_LIMIT_PER_HOUR")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(120) // Picked arbitrarily
|
||||
}
|
||||
|
||||
fn refill_duration() -> chrono::Duration {
|
||||
chrono::Duration::hours(1)
|
||||
}
|
||||
|
||||
fn db_name() -> &'static str {
|
||||
"get-cached-embeddings"
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_embeddings(
|
||||
request: proto::GetCachedEmbeddings,
|
||||
response: Response<proto::GetCachedEmbeddings>,
|
||||
@@ -4536,11 +4710,6 @@ async fn get_cached_embeddings(
|
||||
) -> Result<()> {
|
||||
authorize_access_to_language_models(&session).await?;
|
||||
|
||||
session
|
||||
.rate_limiter
|
||||
.check::<GetCachedEmbeddingsRateLimit>(session.user_id())
|
||||
.await?;
|
||||
|
||||
let db = session.db().await;
|
||||
let embeddings = db.get_embeddings(&request.model, &request.digests).await?;
|
||||
|
||||
@@ -4563,6 +4732,37 @@ async fn authorize_access_to_language_models(session: &UserSession) -> Result<()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a Supermaven API key for the user
|
||||
async fn get_supermaven_api_key(
|
||||
_request: proto::GetSupermavenApiKey,
|
||||
response: Response<proto::GetSupermavenApiKey>,
|
||||
session: UserSession,
|
||||
) -> Result<()> {
|
||||
let user_id: String = session.user_id().to_string();
|
||||
if !session.is_staff() {
|
||||
return Err(anyhow!("supermaven not enabled for this account"))?;
|
||||
}
|
||||
|
||||
let email = session
|
||||
.email()
|
||||
.ok_or_else(|| anyhow!("user must have an email"))?;
|
||||
|
||||
let supermaven_admin_api = session
|
||||
.supermaven_client
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("supermaven not configured"))?;
|
||||
|
||||
let result = supermaven_admin_api
|
||||
.try_get_or_create_user(CreateExternalUserRequest { id: user_id, email })
|
||||
.await?;
|
||||
|
||||
response.send(proto::GetSupermavenApiKeyResponse {
|
||||
api_key: result.api_key,
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start receiving chat updates for a channel
|
||||
async fn join_channel_chat(
|
||||
request: proto::JoinChannelChat,
|
||||
|
||||
@@ -263,6 +263,191 @@ async fn test_dev_server_leave_room(
|
||||
cx2.update(|cx| assert!(workspace.read(cx).project().read(cx).is_disconnected()));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_dev_server_delete(
|
||||
cx1: &mut gpui::TestAppContext,
|
||||
cx2: &mut gpui::TestAppContext,
|
||||
cx3: &mut gpui::TestAppContext,
|
||||
) {
|
||||
let (server, client1, client2, channel_id) = TestServer::start2(cx1, cx2).await;
|
||||
|
||||
let (_dev_server, remote_workspace) =
|
||||
create_dev_server_project(&server, client1.app_state.clone(), cx1, cx3).await;
|
||||
|
||||
cx1.update(|cx| {
|
||||
workspace::join_channel(
|
||||
channel_id,
|
||||
client1.app_state.clone(),
|
||||
Some(remote_workspace),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx1.executor().run_until_parked();
|
||||
|
||||
remote_workspace
|
||||
.update(cx1, |ws, cx| {
|
||||
assert!(ws.project().read(cx).is_shared());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
join_channel(channel_id, &client2, cx2).await.unwrap();
|
||||
cx2.executor().run_until_parked();
|
||||
|
||||
cx1.update(|cx| {
|
||||
dev_server_projects::Store::global(cx).update(cx, |store, cx| {
|
||||
store.delete_dev_server_project(store.dev_server_projects().first().unwrap().id, cx)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx1.executor().run_until_parked();
|
||||
|
||||
let (workspace, cx2) = client2.active_workspace(cx2);
|
||||
cx2.update(|cx| assert!(workspace.read(cx).project().read(cx).is_disconnected()));
|
||||
|
||||
cx1.update(|cx| {
|
||||
dev_server_projects::Store::global(cx).update(cx, |store, _| {
|
||||
assert_eq!(store.dev_server_projects().len(), 0);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_dev_server_rename(
|
||||
cx1: &mut gpui::TestAppContext,
|
||||
cx2: &mut gpui::TestAppContext,
|
||||
cx3: &mut gpui::TestAppContext,
|
||||
) {
|
||||
let (server, client1, client2, channel_id) = TestServer::start2(cx1, cx2).await;
|
||||
|
||||
let (_dev_server, remote_workspace) =
|
||||
create_dev_server_project(&server, client1.app_state.clone(), cx1, cx3).await;
|
||||
|
||||
cx1.update(|cx| {
|
||||
workspace::join_channel(
|
||||
channel_id,
|
||||
client1.app_state.clone(),
|
||||
Some(remote_workspace),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx1.executor().run_until_parked();
|
||||
|
||||
remote_workspace
|
||||
.update(cx1, |ws, cx| {
|
||||
assert!(ws.project().read(cx).is_shared());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
join_channel(channel_id, &client2, cx2).await.unwrap();
|
||||
cx2.executor().run_until_parked();
|
||||
|
||||
cx1.update(|cx| {
|
||||
dev_server_projects::Store::global(cx).update(cx, |store, cx| {
|
||||
store.rename_dev_server(
|
||||
store.dev_servers().first().unwrap().id,
|
||||
"name-edited".to_string(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx1.executor().run_until_parked();
|
||||
|
||||
cx1.update(|cx| {
|
||||
dev_server_projects::Store::global(cx).update(cx, |store, _| {
|
||||
assert_eq!(store.dev_servers().first().unwrap().name, "name-edited");
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_dev_server_refresh_access_token(
|
||||
cx1: &mut gpui::TestAppContext,
|
||||
cx2: &mut gpui::TestAppContext,
|
||||
cx3: &mut gpui::TestAppContext,
|
||||
cx4: &mut gpui::TestAppContext,
|
||||
) {
|
||||
let (server, client1, client2, channel_id) = TestServer::start2(cx1, cx2).await;
|
||||
|
||||
let (_dev_server, remote_workspace) =
|
||||
create_dev_server_project(&server, client1.app_state.clone(), cx1, cx3).await;
|
||||
|
||||
cx1.update(|cx| {
|
||||
workspace::join_channel(
|
||||
channel_id,
|
||||
client1.app_state.clone(),
|
||||
Some(remote_workspace),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx1.executor().run_until_parked();
|
||||
|
||||
remote_workspace
|
||||
.update(cx1, |ws, cx| {
|
||||
assert!(ws.project().read(cx).is_shared());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
join_channel(channel_id, &client2, cx2).await.unwrap();
|
||||
cx2.executor().run_until_parked();
|
||||
|
||||
// Regenerate the access token
|
||||
let new_token_response = cx1
|
||||
.update(|cx| {
|
||||
dev_server_projects::Store::global(cx).update(cx, |store, cx| {
|
||||
store.regenerate_dev_server_token(store.dev_servers().first().unwrap().id, cx)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx1.executor().run_until_parked();
|
||||
|
||||
// Assert that the other client was disconnected
|
||||
let (workspace, cx2) = client2.active_workspace(cx2);
|
||||
cx2.update(|cx| assert!(workspace.read(cx).project().read(cx).is_disconnected()));
|
||||
|
||||
// Assert that the owner of the dev server does not see the dev server as online anymore
|
||||
let (workspace, cx1) = client1.active_workspace(cx1);
|
||||
cx1.update(|cx| {
|
||||
assert!(workspace.read(cx).project().read(cx).is_disconnected());
|
||||
dev_server_projects::Store::global(cx).update(cx, |store, _| {
|
||||
assert_eq!(
|
||||
store.dev_servers().first().unwrap().status,
|
||||
DevServerStatus::Offline
|
||||
);
|
||||
})
|
||||
});
|
||||
|
||||
// Reconnect the dev server with the new token
|
||||
let _dev_server = server
|
||||
.create_dev_server(new_token_response.access_token, cx4)
|
||||
.await;
|
||||
|
||||
cx1.executor().run_until_parked();
|
||||
|
||||
// Assert that the dev server is online again
|
||||
cx1.update(|cx| {
|
||||
dev_server_projects::Store::global(cx).update(cx, |store, _| {
|
||||
assert_eq!(store.dev_servers().len(), 1);
|
||||
assert_eq!(
|
||||
store.dev_servers().first().unwrap().status,
|
||||
DevServerStatus::Online
|
||||
);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_dev_server_reconnect(
|
||||
cx1: &mut gpui::TestAppContext,
|
||||
|
||||
@@ -667,7 +667,7 @@ async fn test_collaborating_with_code_actions(
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
editor.toggle_code_actions(
|
||||
&ToggleCodeActions {
|
||||
deployed_from_indicator: false,
|
||||
deployed_from_indicator: None,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
@@ -2073,7 +2073,7 @@ struct Row10;"#};
|
||||
.as_singleton()
|
||||
.unwrap()
|
||||
.update(cx, |buffer, cx| {
|
||||
buffer.set_diff_base(Some(base_text.to_string()), cx);
|
||||
buffer.set_diff_base(Some(base_text.into()), cx);
|
||||
});
|
||||
});
|
||||
editor_cx_b.update_editor(|editor, cx| {
|
||||
@@ -2083,7 +2083,7 @@ struct Row10;"#};
|
||||
.as_singleton()
|
||||
.unwrap()
|
||||
.update(cx, |buffer, cx| {
|
||||
buffer.set_diff_base(Some(base_text.to_string()), cx);
|
||||
buffer.set_diff_base(Some(base_text.into()), cx);
|
||||
});
|
||||
});
|
||||
cx_a.executor().run_until_parked();
|
||||
@@ -2110,10 +2110,7 @@ struct Row10;"#};
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let all_hunks = editor_hunks(editor, &snapshot, cx);
|
||||
let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
|
||||
assert_eq!(
|
||||
expanded_hunks_background_highlights(editor, &snapshot),
|
||||
Vec::new(),
|
||||
);
|
||||
assert_eq!(expanded_hunks_background_highlights(editor, cx), Vec::new());
|
||||
assert_eq!(
|
||||
all_hunks,
|
||||
vec![
|
||||
@@ -2135,8 +2132,8 @@ struct Row10;"#};
|
||||
let all_hunks = editor_hunks(editor, &snapshot, cx);
|
||||
let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
|
||||
assert_eq!(
|
||||
expanded_hunks_background_highlights(editor, &snapshot),
|
||||
vec![1..3, 8..9],
|
||||
expanded_hunks_background_highlights(editor, cx),
|
||||
vec![1..=2, 8..=8],
|
||||
);
|
||||
assert_eq!(
|
||||
all_hunks,
|
||||
@@ -2170,10 +2167,7 @@ struct Row10;"#};
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let all_hunks = editor_hunks(editor, &snapshot, cx);
|
||||
let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
|
||||
assert_eq!(
|
||||
expanded_hunks_background_highlights(editor, &snapshot),
|
||||
Vec::new(),
|
||||
);
|
||||
assert_eq!(expanded_hunks_background_highlights(editor, cx), Vec::new());
|
||||
assert_eq!(
|
||||
all_hunks,
|
||||
vec![(
|
||||
@@ -2189,8 +2183,8 @@ struct Row10;"#};
|
||||
let all_hunks = editor_hunks(editor, &snapshot, cx);
|
||||
let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
|
||||
assert_eq!(
|
||||
expanded_hunks_background_highlights(editor, &snapshot),
|
||||
Vec::new(),
|
||||
expanded_hunks_background_highlights(editor, cx),
|
||||
vec![5..=5]
|
||||
);
|
||||
assert_eq!(
|
||||
all_hunks,
|
||||
|
||||
@@ -2570,7 +2570,10 @@ async fn test_git_diff_base_change(
|
||||
// Smoke test diffing
|
||||
|
||||
buffer_local_a.read_with(cx_a, |buffer, _| {
|
||||
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
|
||||
assert_eq!(
|
||||
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
|
||||
Some(diff_base.as_str())
|
||||
);
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
&buffer,
|
||||
@@ -2591,7 +2594,10 @@ async fn test_git_diff_base_change(
|
||||
// Smoke test diffing
|
||||
|
||||
buffer_remote_a.read_with(cx_b, |buffer, _| {
|
||||
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
|
||||
assert_eq!(
|
||||
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
|
||||
Some(diff_base.as_str())
|
||||
);
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
&buffer,
|
||||
@@ -2611,7 +2617,10 @@ async fn test_git_diff_base_change(
|
||||
// Smoke test new diffing
|
||||
|
||||
buffer_local_a.read_with(cx_a, |buffer, _| {
|
||||
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
|
||||
assert_eq!(
|
||||
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
|
||||
Some(new_diff_base.as_str())
|
||||
);
|
||||
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
@@ -2624,7 +2633,10 @@ async fn test_git_diff_base_change(
|
||||
// Smoke test B
|
||||
|
||||
buffer_remote_a.read_with(cx_b, |buffer, _| {
|
||||
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
|
||||
assert_eq!(
|
||||
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
|
||||
Some(new_diff_base.as_str())
|
||||
);
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
&buffer,
|
||||
@@ -2664,7 +2676,10 @@ async fn test_git_diff_base_change(
|
||||
// Smoke test diffing
|
||||
|
||||
buffer_local_b.read_with(cx_a, |buffer, _| {
|
||||
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
|
||||
assert_eq!(
|
||||
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
|
||||
Some(diff_base.as_str())
|
||||
);
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
&buffer,
|
||||
@@ -2685,7 +2700,10 @@ async fn test_git_diff_base_change(
|
||||
// Smoke test diffing
|
||||
|
||||
buffer_remote_b.read_with(cx_b, |buffer, _| {
|
||||
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
|
||||
assert_eq!(
|
||||
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
|
||||
Some(diff_base.as_str())
|
||||
);
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
&buffer,
|
||||
@@ -2705,7 +2723,10 @@ async fn test_git_diff_base_change(
|
||||
// Smoke test new diffing
|
||||
|
||||
buffer_local_b.read_with(cx_a, |buffer, _| {
|
||||
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
|
||||
assert_eq!(
|
||||
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
|
||||
Some(new_diff_base.as_str())
|
||||
);
|
||||
println!("{:?}", buffer.as_rope().to_string());
|
||||
println!("{:?}", buffer.diff_base());
|
||||
println!(
|
||||
@@ -2727,7 +2748,10 @@ async fn test_git_diff_base_change(
|
||||
// Smoke test B
|
||||
|
||||
buffer_remote_b.read_with(cx_b, |buffer, _| {
|
||||
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
|
||||
assert_eq!(
|
||||
buffer.diff_base().map(|rope| rope.to_string()).as_deref(),
|
||||
Some(new_diff_base.as_str())
|
||||
);
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_row_range(0..4),
|
||||
&buffer,
|
||||
@@ -6105,7 +6129,7 @@ async fn test_right_click_menu_behind_collab_panel(cx: &mut TestAppContext) {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_cmd_k_left(cx: &mut TestAppContext) {
|
||||
async fn test_pane_split_left(cx: &mut TestAppContext) {
|
||||
let (_, client) = TestServer::start1(cx).await;
|
||||
let (workspace, cx) = client.build_test_workspace(cx).await;
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ use collab_ui::channel_view::ChannelView;
|
||||
use collections::{HashMap, HashSet};
|
||||
use fs::FakeFs;
|
||||
use futures::{channel::oneshot, StreamExt as _};
|
||||
use git::GitHostingProviderRegistry;
|
||||
use gpui::{BackgroundExecutor, Context, Model, Task, TestAppContext, View, VisualTestContext};
|
||||
use language::LanguageRegistry;
|
||||
use node_runtime::FakeNodeRuntime;
|
||||
@@ -257,6 +258,11 @@ impl TestServer {
|
||||
})
|
||||
});
|
||||
|
||||
let git_hosting_provider_registry =
|
||||
cx.update(|cx| GitHostingProviderRegistry::default_global(cx));
|
||||
git_hosting_provider_registry
|
||||
.register_hosting_provider(Arc::new(git_hosting_providers::Github));
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
|
||||
let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
|
||||
@@ -422,8 +428,10 @@ impl TestServer {
|
||||
node_runtime: app_state.node_runtime.clone(),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
TestClient {
|
||||
app_state,
|
||||
@@ -655,6 +663,7 @@ impl TestServer {
|
||||
auto_join_channel_id: None,
|
||||
migrations_path: None,
|
||||
seed_path: None,
|
||||
supermaven_admin_api_key: None,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ picker.workspace = true
|
||||
project.workspace = true
|
||||
recent_projects.workspace = true
|
||||
dev_server_projects.workspace = true
|
||||
release_channel.workspace = true
|
||||
rich_text.workspace = true
|
||||
rpc.workspace = true
|
||||
schemars.workspace = true
|
||||
|
||||
@@ -572,7 +572,7 @@ impl ChatPanel {
|
||||
)
|
||||
.child(
|
||||
self.render_popover_buttons(&cx, message_id, can_delete_message, can_edit_message)
|
||||
.neg_mt_2p5(),
|
||||
.mt_neg_2p5(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1408,6 +1408,11 @@ impl CollabPanel {
|
||||
});
|
||||
}
|
||||
|
||||
if self.context_menu.is_some() {
|
||||
self.context_menu.take();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
self.update_entries(false, cx);
|
||||
}
|
||||
|
||||
@@ -2149,7 +2154,7 @@ impl CollabPanel {
|
||||
.child(list(self.list_state.clone()).size_full())
|
||||
.child(
|
||||
v_flex()
|
||||
.child(div().mx_2().border_primary(cx).border_t())
|
||||
.child(div().mx_2().border_primary(cx).border_t_1())
|
||||
.child(
|
||||
v_flex()
|
||||
.p_2()
|
||||
|
||||
@@ -677,7 +677,7 @@ impl CollabTitlebarItem {
|
||||
client::Status::UpgradeRequired => {
|
||||
let auto_updater = auto_update::AutoUpdater::get(cx);
|
||||
let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
|
||||
Some(AutoUpdateStatus::Updated) => "Please restart Zed to Collaborate",
|
||||
Some(AutoUpdateStatus::Updated { .. }) => "Please restart Zed to Collaborate",
|
||||
Some(AutoUpdateStatus::Installing)
|
||||
| Some(AutoUpdateStatus::Downloading)
|
||||
| Some(AutoUpdateStatus::Checking) => "Updating...",
|
||||
@@ -691,7 +691,7 @@ impl CollabTitlebarItem {
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(|_, cx| {
|
||||
if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
|
||||
if auto_updater.read(cx).status() == AutoUpdateStatus::Updated {
|
||||
if auto_updater.read(cx).status().is_updated() {
|
||||
workspace::restart(&Default::default(), cx);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -14,12 +14,13 @@ pub use collab_panel::CollabPanel;
|
||||
pub use collab_titlebar_item::CollabTitlebarItem;
|
||||
use gpui::{
|
||||
actions, point, AppContext, DevicePixels, Pixels, PlatformDisplay, Size, Task,
|
||||
WindowBackgroundAppearance, WindowContext, WindowKind, WindowOptions,
|
||||
WindowBackgroundAppearance, WindowBounds, WindowContext, WindowKind, WindowOptions,
|
||||
};
|
||||
use panel_settings::MessageEditorSettings;
|
||||
pub use panel_settings::{
|
||||
ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
|
||||
};
|
||||
use release_channel::ReleaseChannel;
|
||||
use settings::Settings;
|
||||
use workspace::{notifications::DetachAndPromptErr, AppState};
|
||||
|
||||
@@ -96,6 +97,7 @@ pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
|
||||
fn notification_window_options(
|
||||
screen: Rc<dyn PlatformDisplay>,
|
||||
window_size: Size<Pixels>,
|
||||
cx: &AppContext,
|
||||
) -> WindowOptions {
|
||||
let notification_margin_width = DevicePixels::from(16);
|
||||
let notification_margin_height = DevicePixels::from(-0) - DevicePixels::from(48);
|
||||
@@ -112,16 +114,17 @@ fn notification_window_options(
|
||||
size: window_size.into(),
|
||||
};
|
||||
|
||||
let app_id = ReleaseChannel::global(cx).app_id();
|
||||
|
||||
WindowOptions {
|
||||
bounds: Some(bounds),
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
titlebar: None,
|
||||
focus: false,
|
||||
show: true,
|
||||
kind: WindowKind::PopUp,
|
||||
is_movable: false,
|
||||
display_id: Some(screen.id()),
|
||||
fullscreen: false,
|
||||
window_background: WindowBackgroundAppearance::default(),
|
||||
app_id: Some("dev.zed.Zed".to_owned()),
|
||||
app_id: Some(app_id.to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ impl RenderOnce for FacePile {
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.rev()
|
||||
.map(|(ix, player)| div().when(ix > 0, |div| div.neg_ml_1()).child(player)),
|
||||
.map(|(ix, player)| div().when(ix > 0, |div| div.ml_neg_1()).child(player)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ impl CollabNotification {
|
||||
}
|
||||
|
||||
impl ParentElement for CollabNotification {
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,18 +32,22 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
};
|
||||
|
||||
for screen in unique_screens {
|
||||
let options = notification_window_options(screen, window_size);
|
||||
let window = cx
|
||||
.open_window(options, |cx| {
|
||||
cx.new_view(|_| {
|
||||
IncomingCallNotification::new(
|
||||
incoming_call.clone(),
|
||||
app_state.clone(),
|
||||
)
|
||||
if let Some(options) = cx
|
||||
.update(|cx| notification_window_options(screen, window_size, cx))
|
||||
.log_err()
|
||||
{
|
||||
let window = cx
|
||||
.open_window(options, |cx| {
|
||||
cx.new_view(|_| {
|
||||
IncomingCallNotification::new(
|
||||
incoming_call.clone(),
|
||||
app_state.clone(),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
notification_windows.push(window);
|
||||
.unwrap();
|
||||
notification_windows.push(window);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,11 +55,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
.detach();
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
struct RespondToCall {
|
||||
accept: bool,
|
||||
}
|
||||
|
||||
struct IncomingCallNotificationState {
|
||||
call: IncomingCall,
|
||||
app_state: Weak<AppState>,
|
||||
|
||||
@@ -26,7 +26,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
};
|
||||
|
||||
for screen in cx.displays() {
|
||||
let options = notification_window_options(screen, window_size);
|
||||
let options = notification_window_options(screen, window_size, cx);
|
||||
let window = cx.open_window(options, |cx| {
|
||||
cx.new_view(|_| {
|
||||
ProjectSharedNotification::new(
|
||||
|
||||
@@ -27,28 +27,39 @@ anyhow.workspace = true
|
||||
async-compression.workspace = true
|
||||
async-tar.workspace = true
|
||||
collections.workspace = true
|
||||
client.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
editor.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
lsp.workspace = true
|
||||
menu.workspace = true
|
||||
node_runtime.workspace = true
|
||||
parking_lot.workspace = true
|
||||
project.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
async-std = { version = "1.12.0", features = ["unstable"] }
|
||||
|
||||
[dev-dependencies]
|
||||
clock.workspace = true
|
||||
indoc.workspace = true
|
||||
serde_json.workspace = true
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
fs = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
lsp = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
rpc = { workspace = true, features = ["test-support"] }
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
theme = { workspace = true, features = ["test-support"] }
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
mod copilot_completion_provider;
|
||||
pub mod request;
|
||||
mod sign_in;
|
||||
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
@@ -10,9 +13,9 @@ use gpui::{
|
||||
ModelContext, Task, WeakModel,
|
||||
};
|
||||
use language::{
|
||||
language_settings::{all_language_settings, language_settings},
|
||||
point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, Language,
|
||||
LanguageServerName, PointUtf16, ToPointUtf16,
|
||||
language_settings::{all_language_settings, language_settings, InlineCompletionProvider},
|
||||
point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, Language, PointUtf16,
|
||||
ToPointUtf16,
|
||||
};
|
||||
use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId};
|
||||
use node_runtime::NodeRuntime;
|
||||
@@ -32,6 +35,9 @@ use util::{
|
||||
fs::remove_matching, github::latest_github_release, http::HttpClient, maybe, paths, ResultExt,
|
||||
};
|
||||
|
||||
pub use copilot_completion_provider::CopilotCompletionProvider;
|
||||
pub use sign_in::CopilotCodeVerification;
|
||||
|
||||
actions!(
|
||||
copilot,
|
||||
[
|
||||
@@ -144,7 +150,6 @@ impl CopilotServer {
|
||||
}
|
||||
|
||||
struct RunningCopilotServer {
|
||||
name: LanguageServerName,
|
||||
lsp: Arc<LanguageServer>,
|
||||
sign_in_status: SignInStatus,
|
||||
registered_buffers: HashMap<EntityId, RegisteredBuffer>,
|
||||
@@ -354,7 +359,9 @@ impl Copilot {
|
||||
let server_id = self.server_id;
|
||||
let http = self.http.clone();
|
||||
let node_runtime = self.node_runtime.clone();
|
||||
if all_language_settings(None, cx).copilot_enabled(None, None) {
|
||||
if all_language_settings(None, cx).inline_completions.provider
|
||||
== InlineCompletionProvider::Copilot
|
||||
{
|
||||
if matches!(self.server, CopilotServer::Disabled) {
|
||||
let start_task = cx
|
||||
.spawn(move |this, cx| {
|
||||
@@ -393,7 +400,6 @@ impl Copilot {
|
||||
http: http.clone(),
|
||||
node_runtime,
|
||||
server: CopilotServer::Running(RunningCopilotServer {
|
||||
name: LanguageServerName(Arc::from("copilot")),
|
||||
lsp: Arc::new(server),
|
||||
sign_in_status: SignInStatus::Authorized,
|
||||
registered_buffers: Default::default(),
|
||||
@@ -423,11 +429,17 @@ impl Copilot {
|
||||
env: None,
|
||||
};
|
||||
|
||||
let root_path = if cfg!(target_os = "windows") {
|
||||
Path::new("C:/")
|
||||
} else {
|
||||
Path::new("/")
|
||||
};
|
||||
|
||||
let server = LanguageServer::new(
|
||||
Arc::new(Mutex::new(None)),
|
||||
new_server_id,
|
||||
binary,
|
||||
Path::new("/"),
|
||||
root_path,
|
||||
None,
|
||||
cx.clone(),
|
||||
)?;
|
||||
@@ -467,7 +479,6 @@ impl Copilot {
|
||||
match server {
|
||||
Ok((server, status)) => {
|
||||
this.server = CopilotServer::Running(RunningCopilotServer {
|
||||
name: LanguageServerName(Arc::from("copilot")),
|
||||
lsp: server,
|
||||
sign_in_status: SignInStatus::SignedOut,
|
||||
registered_buffers: Default::default(),
|
||||
@@ -607,9 +618,9 @@ impl Copilot {
|
||||
cx.background_executor().spawn(start_task)
|
||||
}
|
||||
|
||||
pub fn language_server(&self) -> Option<(&LanguageServerName, &Arc<LanguageServer>)> {
|
||||
pub fn language_server(&self) -> Option<&Arc<LanguageServer>> {
|
||||
if let CopilotServer::Running(server) = &self.server {
|
||||
Some((&server.name, &server.lsp))
|
||||
Some(&server.lsp)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -943,12 +954,9 @@ impl Copilot {
|
||||
}
|
||||
|
||||
fn id_for_language(language: Option<&Arc<Language>>) -> String {
|
||||
let language_name = language.map(|language| language.name());
|
||||
match language_name.as_deref() {
|
||||
Some("Plain Text") => "plaintext".to_string(),
|
||||
Some(language_name) => language_name.to_lowercase(),
|
||||
None => "plaintext".to_string(),
|
||||
}
|
||||
language
|
||||
.map(|language| language.lsp_id())
|
||||
.unwrap_or_else(|| "plaintext".to_string())
|
||||
}
|
||||
|
||||
fn uri_for_buffer(buffer: &Model<Buffer>, cx: &AppContext) -> lsp::Url {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use crate::{Completion, Copilot};
|
||||
use anyhow::Result;
|
||||
use client::telemetry::Telemetry;
|
||||
use copilot::Copilot;
|
||||
use editor::{Direction, InlineCompletionProvider};
|
||||
use gpui::{AppContext, EntityId, Model, ModelContext, Task};
|
||||
use language::language_settings::AllLanguageSettings;
|
||||
use language::{language_settings::all_language_settings, Buffer, OffsetRangeExt, ToOffset};
|
||||
use language::{
|
||||
language_settings::{all_language_settings, AllLanguageSettings},
|
||||
Buffer, OffsetRangeExt, ToOffset,
|
||||
};
|
||||
use settings::Settings;
|
||||
use std::{path::Path, sync::Arc, time::Duration};
|
||||
|
||||
@@ -13,7 +15,7 @@ pub const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
|
||||
pub struct CopilotCompletionProvider {
|
||||
cycled: bool,
|
||||
buffer_id: Option<EntityId>,
|
||||
completions: Vec<copilot::Completion>,
|
||||
completions: Vec<Completion>,
|
||||
active_completion_index: usize,
|
||||
file_extension: Option<String>,
|
||||
pending_refresh: Task<Result<()>>,
|
||||
@@ -42,11 +44,11 @@ impl CopilotCompletionProvider {
|
||||
self
|
||||
}
|
||||
|
||||
fn active_completion(&self) -> Option<&copilot::Completion> {
|
||||
fn active_completion(&self) -> Option<&Completion> {
|
||||
self.completions.get(self.active_completion_index)
|
||||
}
|
||||
|
||||
fn push_completion(&mut self, new_completion: copilot::Completion) {
|
||||
fn push_completion(&mut self, new_completion: Completion) {
|
||||
for completion in &self.completions {
|
||||
if completion.text == new_completion.text && completion.range == new_completion.range {
|
||||
return;
|
||||
@@ -71,7 +73,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
|
||||
let file = buffer.file();
|
||||
let language = buffer.language_at(cursor_position);
|
||||
let settings = all_language_settings(file, cx);
|
||||
settings.copilot_enabled(language.as_ref(), file.map(|f| f.path().as_ref()))
|
||||
settings.inline_completions_enabled(language.as_ref(), file.map(|f| f.path().as_ref()))
|
||||
}
|
||||
|
||||
fn refresh(
|
||||
@@ -196,7 +198,10 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
|
||||
|
||||
fn discard(&mut self, cx: &mut ModelContext<Self>) {
|
||||
let settings = AllLanguageSettings::get_global(cx);
|
||||
if !settings.copilot.feature_enabled {
|
||||
|
||||
let copilot_enabled = settings.inline_completions_enabled(None, None);
|
||||
|
||||
if !copilot_enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -210,12 +215,12 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
|
||||
}
|
||||
}
|
||||
|
||||
fn active_completion_text(
|
||||
&self,
|
||||
fn active_completion_text<'a>(
|
||||
&'a self,
|
||||
buffer: &Model<Buffer>,
|
||||
cursor_position: language::Anchor,
|
||||
cx: &AppContext,
|
||||
) -> Option<&str> {
|
||||
cx: &'a AppContext,
|
||||
) -> Option<&'a str> {
|
||||
let buffer_id = buffer.entity_id();
|
||||
let buffer = buffer.read(cx);
|
||||
let completion = self.active_completion()?;
|
||||
@@ -298,7 +303,9 @@ mod tests {
|
||||
)
|
||||
.await;
|
||||
let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
|
||||
cx.update_editor(|editor, cx| editor.set_inline_completion_provider(copilot_provider, cx));
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.set_inline_completion_provider(Some(copilot_provider), cx)
|
||||
});
|
||||
|
||||
// When inserting, ensure autocompletion is favored over Copilot suggestions.
|
||||
cx.set_state(indoc! {"
|
||||
@@ -318,7 +325,7 @@ mod tests {
|
||||
);
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![copilot::request::Completion {
|
||||
vec![crate::request::Completion {
|
||||
text: "one.copilot1".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
|
||||
..Default::default()
|
||||
@@ -360,7 +367,7 @@ mod tests {
|
||||
);
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![copilot::request::Completion {
|
||||
vec![crate::request::Completion {
|
||||
text: "one.copilot1".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
|
||||
..Default::default()
|
||||
@@ -393,7 +400,7 @@ mod tests {
|
||||
);
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![copilot::request::Completion {
|
||||
vec![crate::request::Completion {
|
||||
text: "one.copilot1".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
|
||||
..Default::default()
|
||||
@@ -426,7 +433,7 @@ mod tests {
|
||||
// After debouncing, new Copilot completions should be requested.
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![copilot::request::Completion {
|
||||
vec![crate::request::Completion {
|
||||
text: "one.copilot2".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)),
|
||||
..Default::default()
|
||||
@@ -503,7 +510,7 @@ mod tests {
|
||||
});
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![copilot::request::Completion {
|
||||
vec![crate::request::Completion {
|
||||
text: " let x = 4;".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
|
||||
..Default::default()
|
||||
@@ -553,7 +560,9 @@ mod tests {
|
||||
)
|
||||
.await;
|
||||
let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
|
||||
cx.update_editor(|editor, cx| editor.set_inline_completion_provider(copilot_provider, cx));
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.set_inline_completion_provider(Some(copilot_provider), cx)
|
||||
});
|
||||
|
||||
// Setup the editor with a completion request.
|
||||
cx.set_state(indoc! {"
|
||||
@@ -573,7 +582,7 @@ mod tests {
|
||||
);
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![copilot::request::Completion {
|
||||
vec![crate::request::Completion {
|
||||
text: "one.copilot1".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
|
||||
..Default::default()
|
||||
@@ -615,7 +624,7 @@ mod tests {
|
||||
);
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![copilot::request::Completion {
|
||||
vec![crate::request::Completion {
|
||||
text: "one.123. copilot\n 456".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
|
||||
..Default::default()
|
||||
@@ -675,7 +684,9 @@ mod tests {
|
||||
)
|
||||
.await;
|
||||
let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
|
||||
cx.update_editor(|editor, cx| editor.set_inline_completion_provider(copilot_provider, cx));
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.set_inline_completion_provider(Some(copilot_provider), cx)
|
||||
});
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
one
|
||||
@@ -685,7 +696,7 @@ mod tests {
|
||||
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![copilot::request::Completion {
|
||||
vec![crate::request::Completion {
|
||||
text: "two.foo()".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
|
||||
..Default::default()
|
||||
@@ -756,13 +767,13 @@ mod tests {
|
||||
let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.set_inline_completion_provider(copilot_provider, cx)
|
||||
editor.set_inline_completion_provider(Some(copilot_provider), cx)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![copilot::request::Completion {
|
||||
vec![crate::request::Completion {
|
||||
text: "b = 2 + a".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 5)),
|
||||
..Default::default()
|
||||
@@ -788,7 +799,7 @@ mod tests {
|
||||
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![copilot::request::Completion {
|
||||
vec![crate::request::Completion {
|
||||
text: "d = 4 + c".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 6)),
|
||||
..Default::default()
|
||||
@@ -829,11 +840,129 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_copilot_does_not_prevent_completion_triggers(
|
||||
executor: BackgroundExecutor,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let (copilot, copilot_lsp) = Copilot::fake(cx);
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
|
||||
..lsp::CompletionOptions::default()
|
||||
}),
|
||||
..lsp::ServerCapabilities::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.set_inline_completion_provider(Some(copilot_provider), cx)
|
||||
});
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
one
|
||||
twˇ
|
||||
three
|
||||
"});
|
||||
|
||||
let _ = handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one
|
||||
tw|<>
|
||||
three
|
||||
"},
|
||||
vec!["completion_a", "completion_b"],
|
||||
);
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
text: "two.foo()".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
cx.update_editor(|editor, cx| editor.next_inline_completion(&Default::default(), cx));
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(!editor.context_menu_visible(), "Even there are some completions available, those are not triggered when active copilot suggestion is present");
|
||||
assert!(editor.has_active_inline_completion(cx));
|
||||
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one\ntw\nthree\n");
|
||||
});
|
||||
|
||||
cx.simulate_keystroke("o");
|
||||
let _ = handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one
|
||||
two|<>
|
||||
three
|
||||
"},
|
||||
vec!["completion_a_2", "completion_b_2"],
|
||||
);
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
text: "two.foo()".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 3)),
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(!editor.context_menu_visible());
|
||||
assert!(editor.has_active_inline_completion(cx));
|
||||
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one\ntwo\nthree\n");
|
||||
});
|
||||
|
||||
cx.simulate_keystroke(".");
|
||||
let _ = handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one
|
||||
two.|<>
|
||||
three
|
||||
"},
|
||||
vec!["something_else()"],
|
||||
);
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
text: "two.foo()".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 4)),
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(
|
||||
editor.context_menu_visible(),
|
||||
"On completion trigger input, the completions should be fetched and visible"
|
||||
);
|
||||
assert!(
|
||||
!editor.has_active_inline_completion(cx),
|
||||
"On completion trigger input, copilot suggestion should be dismissed"
|
||||
);
|
||||
assert_eq!(editor.display_text(cx), "one\ntwo.\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one\ntwo.\nthree\n");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_copilot_disabled_globs(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings
|
||||
.copilot
|
||||
.inline_completions
|
||||
.get_or_insert(Default::default())
|
||||
.disabled_globs = Some(vec![".env*".to_string()]);
|
||||
});
|
||||
@@ -888,15 +1017,15 @@ mod tests {
|
||||
let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.set_inline_completion_provider(copilot_provider, cx)
|
||||
editor.set_inline_completion_provider(Some(copilot_provider), cx)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let mut copilot_requests = copilot_lsp
|
||||
.handle_request::<copilot::request::GetCompletions, _, _>(
|
||||
.handle_request::<crate::request::GetCompletions, _, _>(
|
||||
move |_params, _cx| async move {
|
||||
Ok(copilot::request::GetCompletionsResult {
|
||||
completions: vec![copilot::request::Completion {
|
||||
Ok(crate::request::GetCompletionsResult {
|
||||
completions: vec![crate::request::Completion {
|
||||
text: "next line".into(),
|
||||
range: lsp::Range::new(
|
||||
lsp::Position::new(1, 0),
|
||||
@@ -931,21 +1060,21 @@ mod tests {
|
||||
|
||||
fn handle_copilot_completion_request(
|
||||
lsp: &lsp::FakeLanguageServer,
|
||||
completions: Vec<copilot::request::Completion>,
|
||||
completions_cycling: Vec<copilot::request::Completion>,
|
||||
completions: Vec<crate::request::Completion>,
|
||||
completions_cycling: Vec<crate::request::Completion>,
|
||||
) {
|
||||
lsp.handle_request::<copilot::request::GetCompletions, _, _>(move |_params, _cx| {
|
||||
lsp.handle_request::<crate::request::GetCompletions, _, _>(move |_params, _cx| {
|
||||
let completions = completions.clone();
|
||||
async move {
|
||||
Ok(copilot::request::GetCompletionsResult {
|
||||
Ok(crate::request::GetCompletionsResult {
|
||||
completions: completions.clone(),
|
||||
})
|
||||
}
|
||||
});
|
||||
lsp.handle_request::<copilot::request::GetCompletionsCycling, _, _>(move |_params, _cx| {
|
||||
lsp.handle_request::<crate::request::GetCompletionsCycling, _, _>(move |_params, _cx| {
|
||||
let completions_cycling = completions_cycling.clone();
|
||||
async move {
|
||||
Ok(copilot::request::GetCompletionsResult {
|
||||
Ok(crate::request::GetCompletionsResult {
|
||||
completions: completions_cycling.clone(),
|
||||
})
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use copilot::{request::PromptUserDeviceFlow, Copilot, Status};
|
||||
use crate::{request::PromptUserDeviceFlow, Copilot, Status};
|
||||
use gpui::{
|
||||
div, svg, AppContext, ClipboardItem, DismissEvent, Element, EventEmitter, FocusHandle,
|
||||
FocusableView, InteractiveElement, IntoElement, Model, MouseDownEvent, ParentElement, Render,
|
||||
@@ -26,7 +26,7 @@ impl EventEmitter<DismissEvent> for CopilotCodeVerification {}
|
||||
impl ModalView for CopilotCodeVerification {}
|
||||
|
||||
impl CopilotCodeVerification {
|
||||
pub(crate) fn new(copilot: &Model<Copilot>, cx: &mut ViewContext<Self>) -> Self {
|
||||
pub fn new(copilot: &Model<Copilot>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let status = copilot.read(cx).status();
|
||||
Self {
|
||||
status,
|
||||
@@ -60,7 +60,7 @@ impl CopilotCodeVerification {
|
||||
h_flex()
|
||||
.w_full()
|
||||
.p_1()
|
||||
.border()
|
||||
.border_1()
|
||||
.border_muted(cx)
|
||||
.rounded_md()
|
||||
.cursor_pointer()
|
||||
@@ -1,403 +0,0 @@
|
||||
use crate::sign_in::CopilotCodeVerification;
|
||||
use anyhow::Result;
|
||||
use copilot::{Copilot, SignOut, Status};
|
||||
use editor::{scroll::Autoscroll, Editor};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
div, Action, AnchorCorner, AppContext, AsyncWindowContext, Entity, IntoElement, ParentElement,
|
||||
Render, Subscription, View, ViewContext, WeakView, WindowContext,
|
||||
};
|
||||
use language::{
|
||||
language_settings::{self, all_language_settings, AllLanguageSettings},
|
||||
File, Language,
|
||||
};
|
||||
use settings::{update_settings_file, Settings, SettingsStore};
|
||||
use std::{path::Path, sync::Arc};
|
||||
use util::{paths, ResultExt};
|
||||
use workspace::notifications::NotificationId;
|
||||
use workspace::{
|
||||
create_and_open_local_file,
|
||||
item::ItemHandle,
|
||||
ui::{
|
||||
popover_menu, ButtonCommon, Clickable, ContextMenu, IconButton, IconName, IconSize, Tooltip,
|
||||
},
|
||||
StatusItemView, Toast, Workspace,
|
||||
};
|
||||
use zed_actions::OpenBrowser;
|
||||
|
||||
const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
|
||||
|
||||
struct CopilotStartingToast;
|
||||
|
||||
struct CopilotErrorToast;
|
||||
|
||||
pub struct CopilotButton {
|
||||
editor_subscription: Option<(Subscription, usize)>,
|
||||
editor_enabled: Option<bool>,
|
||||
language: Option<Arc<Language>>,
|
||||
file: Option<Arc<dyn File>>,
|
||||
fs: Arc<dyn Fs>,
|
||||
}
|
||||
|
||||
impl Render for CopilotButton {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let all_language_settings = all_language_settings(None, cx);
|
||||
if !all_language_settings.copilot.feature_enabled {
|
||||
return div();
|
||||
}
|
||||
|
||||
let Some(copilot) = Copilot::global(cx) else {
|
||||
return div();
|
||||
};
|
||||
let status = copilot.read(cx).status();
|
||||
|
||||
let enabled = self
|
||||
.editor_enabled
|
||||
.unwrap_or_else(|| all_language_settings.copilot_enabled(None, None));
|
||||
|
||||
let icon = match status {
|
||||
Status::Error(_) => IconName::CopilotError,
|
||||
Status::Authorized => {
|
||||
if enabled {
|
||||
IconName::Copilot
|
||||
} else {
|
||||
IconName::CopilotDisabled
|
||||
}
|
||||
}
|
||||
_ => IconName::CopilotInit,
|
||||
};
|
||||
|
||||
if let Status::Error(e) = status {
|
||||
return div().child(
|
||||
IconButton::new("copilot-error", icon)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
if let Some(workspace) = cx.window_handle().downcast::<Workspace>() {
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.show_toast(
|
||||
Toast::new(
|
||||
NotificationId::unique::<CopilotErrorToast>(),
|
||||
format!("Copilot can't be started: {}", e),
|
||||
)
|
||||
.on_click(
|
||||
"Reinstall Copilot",
|
||||
|cx| {
|
||||
if let Some(copilot) = Copilot::global(cx) {
|
||||
copilot
|
||||
.update(cx, |copilot, cx| {
|
||||
copilot.reinstall(cx)
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
},
|
||||
),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::text("GitHub Copilot", cx)),
|
||||
);
|
||||
}
|
||||
let this = cx.view().clone();
|
||||
|
||||
div().child(
|
||||
popover_menu("copilot")
|
||||
.menu(move |cx| match status {
|
||||
Status::Authorized => {
|
||||
Some(this.update(cx, |this, cx| this.build_copilot_menu(cx)))
|
||||
}
|
||||
_ => Some(this.update(cx, |this, cx| this.build_copilot_start_menu(cx))),
|
||||
})
|
||||
.anchor(AnchorCorner::BottomRight)
|
||||
.trigger(
|
||||
IconButton::new("copilot-icon", icon)
|
||||
.tooltip(|cx| Tooltip::text("GitHub Copilot", cx)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl CopilotButton {
|
||||
pub fn new(fs: Arc<dyn Fs>, cx: &mut ViewContext<Self>) -> Self {
|
||||
if let Some(copilot) = Copilot::global(cx) {
|
||||
cx.observe(&copilot, |_, _, cx| cx.notify()).detach()
|
||||
}
|
||||
|
||||
cx.observe_global::<SettingsStore>(move |_, cx| cx.notify())
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
editor_subscription: None,
|
||||
editor_enabled: None,
|
||||
language: None,
|
||||
file: None,
|
||||
fs,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_copilot_start_menu(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
|
||||
let fs = self.fs.clone();
|
||||
ContextMenu::build(cx, |menu, _| {
|
||||
menu.entry("Sign In", None, initiate_sign_in).entry(
|
||||
"Disable Copilot",
|
||||
None,
|
||||
move |cx| hide_copilot(fs.clone(), cx),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_copilot_menu(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
|
||||
let fs = self.fs.clone();
|
||||
|
||||
ContextMenu::build(cx, move |mut menu, cx| {
|
||||
if let Some(language) = self.language.clone() {
|
||||
let fs = fs.clone();
|
||||
let language_enabled =
|
||||
language_settings::language_settings(Some(&language), None, cx)
|
||||
.show_copilot_suggestions;
|
||||
|
||||
menu = menu.entry(
|
||||
format!(
|
||||
"{} Suggestions for {}",
|
||||
if language_enabled { "Hide" } else { "Show" },
|
||||
language.name()
|
||||
),
|
||||
None,
|
||||
move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx),
|
||||
);
|
||||
}
|
||||
|
||||
let settings = AllLanguageSettings::get_global(cx);
|
||||
|
||||
if let Some(file) = &self.file {
|
||||
let path = file.path().clone();
|
||||
let path_enabled = settings.copilot_enabled_for_path(&path);
|
||||
|
||||
menu = menu.entry(
|
||||
format!(
|
||||
"{} Suggestions for This Path",
|
||||
if path_enabled { "Hide" } else { "Show" }
|
||||
),
|
||||
None,
|
||||
move |cx| {
|
||||
if let Some(workspace) = cx.window_handle().downcast::<Workspace>() {
|
||||
if let Ok(workspace) = workspace.root_view(cx) {
|
||||
let workspace = workspace.downgrade();
|
||||
cx.spawn(|cx| {
|
||||
configure_disabled_globs(
|
||||
workspace,
|
||||
path_enabled.then_some(path.clone()),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let globally_enabled = settings.copilot_enabled(None, None);
|
||||
menu.entry(
|
||||
if globally_enabled {
|
||||
"Hide Suggestions for All Files"
|
||||
} else {
|
||||
"Show Suggestions for All Files"
|
||||
},
|
||||
None,
|
||||
move |cx| toggle_copilot_globally(fs.clone(), cx),
|
||||
)
|
||||
.separator()
|
||||
.link(
|
||||
"Copilot Settings",
|
||||
OpenBrowser {
|
||||
url: COPILOT_SETTINGS_URL.to_string(),
|
||||
}
|
||||
.boxed_clone(),
|
||||
)
|
||||
.action("Sign Out", SignOut.boxed_clone())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_enabled(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
|
||||
let editor = editor.read(cx);
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let suggestion_anchor = editor.selections.newest_anchor().start;
|
||||
let language = snapshot.language_at(suggestion_anchor);
|
||||
let file = snapshot.file_at(suggestion_anchor).cloned();
|
||||
self.editor_enabled = {
|
||||
let file = file.as_ref();
|
||||
Some(
|
||||
file.map(|file| !file.is_private()).unwrap_or(true)
|
||||
&& all_language_settings(file, cx)
|
||||
.copilot_enabled(language, file.map(|file| file.path().as_ref())),
|
||||
)
|
||||
};
|
||||
self.language = language.cloned();
|
||||
self.file = file;
|
||||
|
||||
cx.notify()
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusItemView for CopilotButton {
|
||||
fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
|
||||
if let Some(editor) = item.and_then(|item| item.act_as::<Editor>(cx)) {
|
||||
self.editor_subscription = Some((
|
||||
cx.observe(&editor, Self::update_enabled),
|
||||
editor.entity_id().as_u64() as usize,
|
||||
));
|
||||
self.update_enabled(editor, cx);
|
||||
} else {
|
||||
self.language = None;
|
||||
self.editor_subscription = None;
|
||||
self.editor_enabled = None;
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
async fn configure_disabled_globs(
|
||||
workspace: WeakView<Workspace>,
|
||||
path_to_disable: Option<Arc<Path>>,
|
||||
mut cx: AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
let settings_editor = workspace
|
||||
.update(&mut cx, |_, cx| {
|
||||
create_and_open_local_file(&paths::SETTINGS, cx, || {
|
||||
settings::initial_user_settings_content().as_ref().into()
|
||||
})
|
||||
})?
|
||||
.await?
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
settings_editor.downgrade().update(&mut cx, |item, cx| {
|
||||
let text = item.buffer().read(cx).snapshot(cx).text();
|
||||
|
||||
let settings = cx.global::<SettingsStore>();
|
||||
let edits = settings.edits_for_update::<AllLanguageSettings>(&text, |file| {
|
||||
let copilot = file.copilot.get_or_insert_with(Default::default);
|
||||
let globs = copilot.disabled_globs.get_or_insert_with(|| {
|
||||
settings
|
||||
.get::<AllLanguageSettings>(None)
|
||||
.copilot
|
||||
.disabled_globs
|
||||
.iter()
|
||||
.map(|glob| glob.glob().to_string())
|
||||
.collect()
|
||||
});
|
||||
|
||||
if let Some(path_to_disable) = &path_to_disable {
|
||||
globs.push(path_to_disable.to_string_lossy().into_owned());
|
||||
} else {
|
||||
globs.clear();
|
||||
}
|
||||
});
|
||||
|
||||
if !edits.is_empty() {
|
||||
item.change_selections(Some(Autoscroll::newest()), cx, |selections| {
|
||||
selections.select_ranges(edits.iter().map(|e| e.0.clone()));
|
||||
});
|
||||
|
||||
// When *enabling* a path, don't actually perform an edit, just select the range.
|
||||
if path_to_disable.is_some() {
|
||||
item.edit(edits.iter().cloned(), cx);
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
fn toggle_copilot_globally(fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||
let show_copilot_suggestions = all_language_settings(None, cx).copilot_enabled(None, None);
|
||||
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
|
||||
file.defaults.show_copilot_suggestions = Some(!show_copilot_suggestions)
|
||||
});
|
||||
}
|
||||
|
||||
fn toggle_copilot_for_language(language: Arc<Language>, fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||
let show_copilot_suggestions =
|
||||
all_language_settings(None, cx).copilot_enabled(Some(&language), None);
|
||||
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
|
||||
file.languages
|
||||
.entry(language.name())
|
||||
.or_default()
|
||||
.show_copilot_suggestions = Some(!show_copilot_suggestions);
|
||||
});
|
||||
}
|
||||
|
||||
fn hide_copilot(fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
|
||||
file.features.get_or_insert(Default::default()).copilot = Some(false);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn initiate_sign_in(cx: &mut WindowContext) {
|
||||
let Some(copilot) = Copilot::global(cx) else {
|
||||
return;
|
||||
};
|
||||
let status = copilot.read(cx).status();
|
||||
let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
|
||||
return;
|
||||
};
|
||||
match status {
|
||||
Status::Starting { task } => {
|
||||
let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(workspace) = workspace.update(cx, |workspace, cx| {
|
||||
workspace.show_toast(
|
||||
Toast::new(
|
||||
NotificationId::unique::<CopilotStartingToast>(),
|
||||
"Copilot is starting...",
|
||||
),
|
||||
cx,
|
||||
);
|
||||
workspace.weak_handle()
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
task.await;
|
||||
if let Some(copilot) = cx.update(|cx| Copilot::global(cx)).ok().flatten() {
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| match copilot.read(cx).status() {
|
||||
Status::Authorized => workspace.show_toast(
|
||||
Toast::new(
|
||||
NotificationId::unique::<CopilotStartingToast>(),
|
||||
"Copilot has started!",
|
||||
),
|
||||
cx,
|
||||
),
|
||||
_ => {
|
||||
workspace.dismiss_toast(
|
||||
&NotificationId::unique::<CopilotStartingToast>(),
|
||||
cx,
|
||||
);
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
_ => {
|
||||
copilot.update(cx, |this, cx| this.sign_in(cx)).detach();
|
||||
workspace
|
||||
.update(cx, |this, cx| {
|
||||
this.toggle_modal(cx, |cx| CopilotCodeVerification::new(&copilot, cx));
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
pub mod copilot_button;
|
||||
mod copilot_completion_provider;
|
||||
mod sign_in;
|
||||
|
||||
pub use copilot_button::*;
|
||||
pub use copilot_completion_provider::*;
|
||||
pub use sign_in::*;
|
||||
@@ -173,6 +173,39 @@ impl Store {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn rename_dev_server(
|
||||
&mut self,
|
||||
dev_server_id: DevServerId,
|
||||
name: String,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let client = self.client.clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
client
|
||||
.request(proto::RenameDevServer {
|
||||
dev_server_id: dev_server_id.0,
|
||||
name,
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn regenerate_dev_server_token(
|
||||
&mut self,
|
||||
dev_server_id: DevServerId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<proto::RegenerateDevServerTokenResponse>> {
|
||||
let client = self.client.clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
client
|
||||
.request(proto::RegenerateDevServerToken {
|
||||
dev_server_id: dev_server_id.0,
|
||||
})
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
pub fn delete_dev_server(
|
||||
&mut self,
|
||||
id: DevServerId,
|
||||
@@ -188,4 +221,20 @@ impl Store {
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn delete_dev_server_project(
|
||||
&mut self,
|
||||
id: DevServerProjectId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let client = self.client.clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
client
|
||||
.request(proto::DeleteDevServerProject {
|
||||
dev_server_project_id: id.0,
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
snippet.workspace = true
|
||||
sum_tree.workspace = true
|
||||
task.workspace = true
|
||||
text.workspace = true
|
||||
time.workspace = true
|
||||
time_format.workspace = true
|
||||
|
||||
@@ -52,8 +52,9 @@ pub struct SelectToEndOfLine {
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||
pub struct ToggleCodeActions {
|
||||
// Display row from which the action was deployed.
|
||||
#[serde(default)]
|
||||
pub deployed_from_indicator: bool,
|
||||
pub deployed_from_indicator: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||
|
||||
@@ -61,7 +61,7 @@ struct CommitAvatarAsset {
|
||||
impl Hash for CommitAvatarAsset {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.sha.hash(state);
|
||||
self.remote.host.hash(state);
|
||||
self.remote.host.name().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,8 +128,7 @@ impl Render for BlameEntryTooltip {
|
||||
|
||||
let author_email = self.blame_entry.author_mail.clone();
|
||||
|
||||
let pretty_commit_id = format!("{}", self.blame_entry.sha);
|
||||
let short_commit_id = pretty_commit_id.chars().take(6).collect::<String>();
|
||||
let short_commit_id = self.blame_entry.sha.display_short();
|
||||
let absolute_timestamp = blame_entry_absolute_timestamp(&self.blame_entry, cx);
|
||||
|
||||
let message = self
|
||||
|
||||
@@ -477,6 +477,11 @@ impl DisplaySnapshot {
|
||||
.to_inlay_offset(anchor.to_offset(&self.buffer_snapshot))
|
||||
}
|
||||
|
||||
pub fn display_point_to_anchor(&self, point: DisplayPoint, bias: Bias) -> Anchor {
|
||||
self.buffer_snapshot
|
||||
.anchor_at(point.to_offset(&self, bias), bias)
|
||||
}
|
||||
|
||||
fn display_point_to_inlay_point(&self, point: DisplayPoint, bias: Bias) -> InlayPoint {
|
||||
let block_point = point.0;
|
||||
let wrap_point = self.block_snapshot.to_wrap_point(block_point);
|
||||
@@ -721,6 +726,10 @@ impl DisplaySnapshot {
|
||||
DisplayPoint(clipped)
|
||||
}
|
||||
|
||||
pub fn clip_ignoring_line_ends(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint {
|
||||
DisplayPoint(self.block_snapshot.clip_point(point.0, bias))
|
||||
}
|
||||
|
||||
pub fn clip_at_line_end(&self, point: DisplayPoint) -> DisplayPoint {
|
||||
let mut point = point.0;
|
||||
if point.column == self.line_len(point.row) {
|
||||
|
||||
@@ -6,7 +6,6 @@ use gpui::{ElementId, HighlightStyle, Hsla};
|
||||
use language::{Chunk, Edit, Point, TextSummary};
|
||||
use multi_buffer::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
cmp::{self, Ordering},
|
||||
iter,
|
||||
ops::{Add, AddAssign, Deref, DerefMut, Range, Sub},
|
||||
@@ -1066,28 +1065,6 @@ impl<'a> Iterator for FoldChunks<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
struct HighlightEndpoint {
|
||||
offset: InlayOffset,
|
||||
is_start: bool,
|
||||
tag: Option<TypeId>,
|
||||
style: HighlightStyle,
|
||||
}
|
||||
|
||||
impl PartialOrd for HighlightEndpoint {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for HighlightEndpoint {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.offset
|
||||
.cmp(&other.offset)
|
||||
.then_with(|| other.is_start.cmp(&self.is_start))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
|
||||
pub struct FoldOffset(pub usize);
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::{
|
||||
JoinLines,
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use gpui::{div, TestAppContext, VisualTestContext, WindowOptions};
|
||||
use gpui::{div, TestAppContext, VisualTestContext, WindowBounds, WindowOptions};
|
||||
use indoc::indoc;
|
||||
use language::{
|
||||
language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
|
||||
@@ -2826,6 +2826,31 @@ async fn test_join_lines_with_git_diff_base(
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_custom_newlines_cause_no_false_positive_diffs(
|
||||
executor: BackgroundExecutor,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
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_diff_base(Some("Line 0\r\nLine 1\r\nLine 2\r\nLine 3"));
|
||||
executor.run_until_parked();
|
||||
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert_eq!(
|
||||
editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.snapshot(cx)
|
||||
.git_diff_hunks_in_range(0..u32::MAX)
|
||||
.collect::<Vec<_>>(),
|
||||
Vec::new(),
|
||||
"Should not have any diffs for files with custom newlines"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
@@ -7468,10 +7493,10 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
|
||||
let follower = cx.update(|cx| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
bounds: Some(Bounds::from_corners(
|
||||
window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
|
||||
gpui::Point::new(0.into(), 0.into()),
|
||||
gpui::Point::new(10.into(), 80.into()),
|
||||
)),
|
||||
))),
|
||||
..Default::default()
|
||||
},
|
||||
|cx| cx.new_view(|cx| build_editor(buffer.clone(), cx)),
|
||||
@@ -9518,8 +9543,8 @@ async fn test_toggle_hunk_diff(executor: BackgroundExecutor, cx: &mut gpui::Test
|
||||
let all_hunks = editor_hunks(editor, &snapshot, cx);
|
||||
let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
|
||||
assert_eq!(
|
||||
expanded_hunks_background_highlights(editor, &snapshot),
|
||||
vec![1..2, 7..8, 9..10],
|
||||
expanded_hunks_background_highlights(editor, cx),
|
||||
vec![1..=1, 7..=7, 9..=9],
|
||||
"After expanding, all git additions should be highlighted for Modified (split into added and removed) and Added hunks"
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -9546,7 +9571,7 @@ async fn test_toggle_hunk_diff(executor: BackgroundExecutor, cx: &mut gpui::Test
|
||||
let all_hunks = editor_hunks(editor, &snapshot, cx);
|
||||
let all_expanded_hunks = expanded_hunks(editor, &snapshot, cx);
|
||||
assert_eq!(
|
||||
expanded_hunks_background_highlights(editor, &snapshot),
|
||||
expanded_hunks_background_highlights(editor, cx),
|
||||
Vec::new(),
|
||||
"After cancelling in editor, no git highlights should be left"
|
||||
);
|
||||
@@ -9659,8 +9684,8 @@ async fn test_toggled_diff_base_change(
|
||||
let all_hunks = editor_hunks(editor, &snapshot, cx);
|
||||
let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
|
||||
assert_eq!(
|
||||
expanded_hunks_background_highlights(editor, &snapshot),
|
||||
vec![9..11, 13..15],
|
||||
expanded_hunks_background_highlights(editor, cx),
|
||||
vec![9..=10, 13..=14],
|
||||
"After expanding, all git additions should be highlighted for Modified (split into added and removed) and Added hunks"
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -9688,7 +9713,7 @@ async fn test_toggled_diff_base_change(
|
||||
let all_hunks = editor_hunks(editor, &snapshot, cx);
|
||||
let all_expanded_hunks = expanded_hunks(editor, &snapshot, cx);
|
||||
assert_eq!(
|
||||
expanded_hunks_background_highlights(editor, &snapshot),
|
||||
expanded_hunks_background_highlights(editor, cx),
|
||||
Vec::new(),
|
||||
"After diff base is changed, old git highlights should be removed"
|
||||
);
|
||||
@@ -9833,8 +9858,8 @@ async fn test_fold_unfold_diff(executor: BackgroundExecutor, cx: &mut gpui::Test
|
||||
let all_hunks = editor_hunks(editor, &snapshot, cx);
|
||||
let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
|
||||
assert_eq!(
|
||||
expanded_hunks_background_highlights(editor, &snapshot),
|
||||
vec![9..11, 13..15, 19..20]
|
||||
expanded_hunks_background_highlights(editor, cx),
|
||||
vec![9..=10, 13..=14, 19..=19]
|
||||
);
|
||||
assert_eq!(
|
||||
all_hunks,
|
||||
@@ -9898,8 +9923,8 @@ async fn test_fold_unfold_diff(executor: BackgroundExecutor, cx: &mut gpui::Test
|
||||
let all_hunks = editor_hunks(editor, &snapshot, cx);
|
||||
let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
|
||||
assert_eq!(
|
||||
expanded_hunks_background_highlights(editor, &snapshot),
|
||||
vec![5..6],
|
||||
expanded_hunks_background_highlights(editor, cx),
|
||||
vec![0..=0, 5..=5],
|
||||
"Only one hunk is left not folded, its highlight should be visible"
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -9979,8 +10004,8 @@ async fn test_fold_unfold_diff(executor: BackgroundExecutor, cx: &mut gpui::Test
|
||||
let all_hunks = editor_hunks(editor, &snapshot, cx);
|
||||
let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
|
||||
assert_eq!(
|
||||
expanded_hunks_background_highlights(editor, &snapshot),
|
||||
vec![9..11, 13..15, 19..20],
|
||||
expanded_hunks_background_highlights(editor, cx),
|
||||
vec![9..=10, 13..=14, 19..=19],
|
||||
"After unfolding, all hunk diffs should be visible again"
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -10147,10 +10172,7 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext)
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let all_hunks = editor_hunks(editor, &snapshot, cx);
|
||||
let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
|
||||
assert_eq!(
|
||||
expanded_hunks_background_highlights(editor, &snapshot),
|
||||
Vec::new(),
|
||||
);
|
||||
assert_eq!(expanded_hunks_background_highlights(editor, cx), Vec::new());
|
||||
assert_eq!(all_hunks, expected_all_hunks);
|
||||
assert_eq!(all_expanded_hunks, Vec::new());
|
||||
});
|
||||
@@ -10165,8 +10187,8 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext)
|
||||
let all_hunks = editor_hunks(editor, &snapshot, cx);
|
||||
let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
|
||||
assert_eq!(
|
||||
expanded_hunks_background_highlights(editor, &snapshot),
|
||||
vec![18..19, 33..34],
|
||||
expanded_hunks_background_highlights(editor, cx),
|
||||
vec![18..=18, 33..=33],
|
||||
);
|
||||
assert_eq!(all_hunks, expected_all_hunks_shifted);
|
||||
assert_eq!(all_hunks, all_expanded_hunks);
|
||||
@@ -10180,10 +10202,7 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext)
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let all_hunks = editor_hunks(editor, &snapshot, cx);
|
||||
let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
|
||||
assert_eq!(
|
||||
expanded_hunks_background_highlights(editor, &snapshot),
|
||||
Vec::new(),
|
||||
);
|
||||
assert_eq!(expanded_hunks_background_highlights(editor, cx), Vec::new());
|
||||
assert_eq!(all_hunks, expected_all_hunks);
|
||||
assert_eq!(all_expanded_hunks, Vec::new());
|
||||
});
|
||||
@@ -10197,8 +10216,8 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext)
|
||||
let all_hunks = editor_hunks(editor, &snapshot, cx);
|
||||
let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
|
||||
assert_eq!(
|
||||
expanded_hunks_background_highlights(editor, &snapshot),
|
||||
vec![18..19, 33..34],
|
||||
expanded_hunks_background_highlights(editor, cx),
|
||||
vec![18..=18, 33..=33],
|
||||
);
|
||||
assert_eq!(all_hunks, expected_all_hunks_shifted);
|
||||
assert_eq!(all_hunks, all_expanded_hunks);
|
||||
@@ -10212,10 +10231,7 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext)
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let all_hunks = editor_hunks(editor, &snapshot, cx);
|
||||
let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
|
||||
assert_eq!(
|
||||
expanded_hunks_background_highlights(editor, &snapshot),
|
||||
Vec::new(),
|
||||
);
|
||||
assert_eq!(expanded_hunks_background_highlights(editor, cx), Vec::new());
|
||||
assert_eq!(all_hunks, expected_all_hunks);
|
||||
assert_eq!(all_expanded_hunks, Vec::new());
|
||||
});
|
||||
@@ -10304,8 +10320,8 @@ async fn test_edits_around_toggled_additions(
|
||||
vec![("".to_string(), DiffHunkStatus::Added, 4..7)]
|
||||
);
|
||||
assert_eq!(
|
||||
expanded_hunks_background_highlights(editor, &snapshot),
|
||||
vec![4..7]
|
||||
expanded_hunks_background_highlights(editor, cx),
|
||||
vec![4..=6]
|
||||
);
|
||||
assert_eq!(all_hunks, all_expanded_hunks);
|
||||
});
|
||||
@@ -10340,8 +10356,8 @@ async fn test_edits_around_toggled_additions(
|
||||
vec![("".to_string(), DiffHunkStatus::Added, 4..8)]
|
||||
);
|
||||
assert_eq!(
|
||||
expanded_hunks_background_highlights(editor, &snapshot),
|
||||
vec![4..8],
|
||||
expanded_hunks_background_highlights(editor, cx),
|
||||
vec![4..=6],
|
||||
"Edited hunk should have one more line added"
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -10381,8 +10397,8 @@ async fn test_edits_around_toggled_additions(
|
||||
vec![("".to_string(), DiffHunkStatus::Added, 4..9)]
|
||||
);
|
||||
assert_eq!(
|
||||
expanded_hunks_background_highlights(editor, &snapshot),
|
||||
vec![4..9],
|
||||
expanded_hunks_background_highlights(editor, cx),
|
||||
vec![4..=6],
|
||||
"Edited hunk should have one more line added"
|
||||
);
|
||||
assert_eq!(all_hunks, all_expanded_hunks);
|
||||
@@ -10421,8 +10437,8 @@ async fn test_edits_around_toggled_additions(
|
||||
vec![("".to_string(), DiffHunkStatus::Added, 4..8)]
|
||||
);
|
||||
assert_eq!(
|
||||
expanded_hunks_background_highlights(editor, &snapshot),
|
||||
vec![4..8],
|
||||
expanded_hunks_background_highlights(editor, cx),
|
||||
vec![4..=6],
|
||||
"Deleting a line should shrint the hunk"
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -10465,8 +10481,8 @@ async fn test_edits_around_toggled_additions(
|
||||
vec![("".to_string(), DiffHunkStatus::Added, 5..6)]
|
||||
);
|
||||
assert_eq!(
|
||||
expanded_hunks_background_highlights(editor, &snapshot),
|
||||
vec![5..6]
|
||||
expanded_hunks_background_highlights(editor, cx),
|
||||
vec![5..=5]
|
||||
);
|
||||
assert_eq!(all_hunks, all_expanded_hunks);
|
||||
});
|
||||
@@ -10508,7 +10524,7 @@ async fn test_edits_around_toggled_additions(
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
expanded_hunks_background_highlights(editor, &snapshot),
|
||||
expanded_hunks_background_highlights(editor, cx),
|
||||
Vec::new(),
|
||||
"Should close all stale expanded addition hunks"
|
||||
);
|
||||
@@ -10607,10 +10623,7 @@ async fn test_edits_around_toggled_deletions(
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let all_hunks = editor_hunks(editor, &snapshot, cx);
|
||||
let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
|
||||
assert_eq!(
|
||||
expanded_hunks_background_highlights(editor, &snapshot),
|
||||
Vec::new()
|
||||
);
|
||||
assert_eq!(expanded_hunks_background_highlights(editor, cx), Vec::new());
|
||||
assert_eq!(
|
||||
all_hunks,
|
||||
vec![(
|
||||
@@ -10647,7 +10660,7 @@ async fn test_edits_around_toggled_deletions(
|
||||
let all_hunks = editor_hunks(editor, &snapshot, cx);
|
||||
let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
|
||||
assert_eq!(
|
||||
expanded_hunks_background_highlights(editor, &snapshot),
|
||||
expanded_hunks_background_highlights(editor, cx),
|
||||
Vec::new(),
|
||||
"Deleted hunks do not highlight current editor's background"
|
||||
);
|
||||
@@ -10685,10 +10698,7 @@ async fn test_edits_around_toggled_deletions(
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let all_hunks = editor_hunks(editor, &snapshot, cx);
|
||||
let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
|
||||
assert_eq!(
|
||||
expanded_hunks_background_highlights(editor, &snapshot),
|
||||
Vec::new()
|
||||
);
|
||||
assert_eq!(expanded_hunks_background_highlights(editor, cx), Vec::new());
|
||||
assert_eq!(
|
||||
all_hunks,
|
||||
vec![(
|
||||
@@ -10732,8 +10742,8 @@ async fn test_edits_around_toggled_deletions(
|
||||
)]
|
||||
);
|
||||
assert_eq!(
|
||||
expanded_hunks_background_highlights(editor, &snapshot),
|
||||
vec![7..8],
|
||||
expanded_hunks_background_highlights(editor, cx),
|
||||
vec![7..=7],
|
||||
"Modified expanded hunks should display additions and highlight their background"
|
||||
);
|
||||
assert_eq!(all_hunks, all_expanded_hunks);
|
||||
@@ -10826,8 +10836,8 @@ async fn test_edits_around_toggled_modifications(
|
||||
let all_hunks = editor_hunks(editor, &snapshot, cx);
|
||||
let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
|
||||
assert_eq!(
|
||||
expanded_hunks_background_highlights(editor, &snapshot),
|
||||
vec![6..7],
|
||||
expanded_hunks_background_highlights(editor, cx),
|
||||
vec![6..=6],
|
||||
);
|
||||
assert_eq!(
|
||||
all_hunks,
|
||||
@@ -10869,8 +10879,8 @@ async fn test_edits_around_toggled_modifications(
|
||||
let all_hunks = editor_hunks(editor, &snapshot, cx);
|
||||
let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
|
||||
assert_eq!(
|
||||
expanded_hunks_background_highlights(editor, &snapshot),
|
||||
vec![6..9],
|
||||
expanded_hunks_background_highlights(editor, cx),
|
||||
vec![6..=6],
|
||||
"Modified hunk should grow highlighted lines on more text additions"
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -10915,9 +10925,8 @@ async fn test_edits_around_toggled_modifications(
|
||||
let all_hunks = editor_hunks(editor, &snapshot, cx);
|
||||
let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
|
||||
assert_eq!(
|
||||
expanded_hunks_background_highlights(editor, &snapshot),
|
||||
vec![6..9],
|
||||
"Modified hunk should grow deleted lines on text deletions above"
|
||||
expanded_hunks_background_highlights(editor, cx),
|
||||
vec![6..=8],
|
||||
);
|
||||
assert_eq!(
|
||||
all_hunks,
|
||||
@@ -10925,7 +10934,8 @@ async fn test_edits_around_toggled_modifications(
|
||||
"const B: u32 = 42;\nconst C: u32 = 42;\n".to_string(),
|
||||
DiffHunkStatus::Modified,
|
||||
6..9
|
||||
)]
|
||||
)],
|
||||
"Modified hunk should grow deleted lines on text deletions above"
|
||||
);
|
||||
assert_eq!(all_hunks, all_expanded_hunks);
|
||||
});
|
||||
@@ -10959,8 +10969,8 @@ async fn test_edits_around_toggled_modifications(
|
||||
let all_hunks = editor_hunks(editor, &snapshot, cx);
|
||||
let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
|
||||
assert_eq!(
|
||||
expanded_hunks_background_highlights(editor, &snapshot),
|
||||
vec![6..10],
|
||||
expanded_hunks_background_highlights(editor, cx),
|
||||
vec![6..=9],
|
||||
"Modified hunk should grow deleted lines on text modifications above"
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -11003,8 +11013,8 @@ async fn test_edits_around_toggled_modifications(
|
||||
let all_hunks = editor_hunks(editor, &snapshot, cx);
|
||||
let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
|
||||
assert_eq!(
|
||||
expanded_hunks_background_highlights(editor, &snapshot),
|
||||
vec![6..9],
|
||||
expanded_hunks_background_highlights(editor, cx),
|
||||
vec![6..=8],
|
||||
"Modified hunk should grow shrink lines on modification lines removal"
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -11044,7 +11054,7 @@ async fn test_edits_around_toggled_modifications(
|
||||
let all_hunks = editor_hunks(editor, &snapshot, cx);
|
||||
let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
|
||||
assert_eq!(
|
||||
expanded_hunks_background_highlights(editor, &snapshot),
|
||||
expanded_hunks_background_highlights(editor, cx),
|
||||
Vec::new(),
|
||||
"Modified hunk should turn into a removed one on all modified lines removal"
|
||||
);
|
||||
@@ -11147,8 +11157,8 @@ async fn test_multiple_expanded_hunks_merge(
|
||||
let all_hunks = editor_hunks(editor, &snapshot, cx);
|
||||
let all_expanded_hunks = expanded_hunks(&editor, &snapshot, cx);
|
||||
assert_eq!(
|
||||
expanded_hunks_background_highlights(editor, &snapshot),
|
||||
vec![6..7],
|
||||
expanded_hunks_background_highlights(editor, cx),
|
||||
vec![6..=6],
|
||||
);
|
||||
assert_eq!(
|
||||
all_hunks,
|
||||
@@ -11351,7 +11361,7 @@ fn assert_hunk_revert(
|
||||
.as_singleton()
|
||||
.unwrap()
|
||||
.update(cx, |buffer, cx| {
|
||||
buffer.set_diff_base(Some(base_text.to_string()), cx);
|
||||
buffer.set_diff_base(Some(base_text.into()), cx);
|
||||
});
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
@@ -12,10 +12,11 @@ use crate::{
|
||||
items::BufferSearchHighlights,
|
||||
mouse_context_menu::{self, MouseContextMenu},
|
||||
scroll::scroll_amount::ScrollAmount,
|
||||
CursorShape, DisplayPoint, DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode,
|
||||
EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts, GutterDimensions, HalfPageDown,
|
||||
HalfPageUp, HoveredCursor, HunkToExpand, LineDown, LineUp, OpenExcerpts, PageDown, PageUp,
|
||||
Point, SelectPhase, Selection, SoftWrap, ToPoint, CURSORS_VISIBLE_FOR, MAX_LINE_LEN,
|
||||
CodeActionsMenu, CursorShape, DisplayPoint, DocumentHighlightRead, DocumentHighlightWrite,
|
||||
Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts,
|
||||
GutterDimensions, HalfPageDown, HalfPageUp, HoveredCursor, HunkToExpand, LineDown, LineUp,
|
||||
OpenExcerpts, PageDown, PageUp, Point, SelectPhase, Selection, SoftWrap, ToPoint,
|
||||
CURSORS_VISIBLE_FOR, MAX_LINE_LEN,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use client::ParticipantIndex;
|
||||
@@ -427,10 +428,12 @@ impl EditorElement {
|
||||
let mut click_count = event.click_count;
|
||||
let mut modifiers = event.modifiers;
|
||||
|
||||
if gutter_hitbox.is_hovered(cx) {
|
||||
click_count = 3; // Simulate triple-click when clicking the gutter to select lines
|
||||
} else if let Some(hovered_hunk) = hovered_hunk {
|
||||
if let Some(hovered_hunk) = hovered_hunk {
|
||||
editor.expand_diff_hunk(None, hovered_hunk, cx);
|
||||
cx.notify();
|
||||
return;
|
||||
} else if gutter_hitbox.is_hovered(cx) {
|
||||
click_count = 3; // Simulate triple-click when clicking the gutter to select lines
|
||||
} else if !text_hitbox.is_hovered(cx) {
|
||||
return;
|
||||
}
|
||||
@@ -635,7 +638,11 @@ impl EditorElement {
|
||||
editor.update_hovered_link(point_for_position, &position_map.snapshot, modifiers, cx);
|
||||
|
||||
if let Some(point) = point_for_position.as_valid() {
|
||||
hover_at(editor, Some(point), cx);
|
||||
let anchor = position_map
|
||||
.snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_before(point.to_offset(&position_map.snapshot, Bias::Left));
|
||||
hover_at(editor, Some(anchor), cx);
|
||||
Self::update_visible_cursor(editor, point, position_map, cx);
|
||||
} else {
|
||||
hover_at(editor, None, cx);
|
||||
@@ -1372,6 +1379,66 @@ impl EditorElement {
|
||||
Some(shaped_lines)
|
||||
}
|
||||
|
||||
fn layout_run_indicators(
|
||||
&self,
|
||||
line_height: Pixels,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
gutter_dimensions: &GutterDimensions,
|
||||
gutter_hitbox: &Hitbox,
|
||||
snapshot: &EditorSnapshot,
|
||||
cx: &mut WindowContext,
|
||||
) -> Vec<AnyElement> {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let active_task_indicator_row =
|
||||
if let Some(crate::ContextMenu::CodeActions(CodeActionsMenu {
|
||||
deployed_from_indicator,
|
||||
actions,
|
||||
..
|
||||
})) = editor.context_menu.read().as_ref()
|
||||
{
|
||||
actions
|
||||
.tasks
|
||||
.as_ref()
|
||||
.map(|tasks| tasks.position.row)
|
||||
.or_else(|| *deployed_from_indicator)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
editor
|
||||
.tasks
|
||||
.iter()
|
||||
.filter_map(|((_, row), (multibuffer_offset, _))| {
|
||||
if snapshot.is_line_folded(*row) {
|
||||
return None;
|
||||
}
|
||||
let display_row = snapshot
|
||||
.buffer_snapshot
|
||||
.offset_to_point(*multibuffer_offset)
|
||||
.to_display_point(&snapshot.display_snapshot)
|
||||
.row();
|
||||
|
||||
let button = editor.render_run_indicator(
|
||||
&self.style,
|
||||
Some(*row) == active_task_indicator_row,
|
||||
display_row,
|
||||
cx,
|
||||
);
|
||||
|
||||
let button = prepaint_gutter_button(
|
||||
button,
|
||||
display_row,
|
||||
line_height,
|
||||
gutter_dimensions,
|
||||
scroll_pixel_position,
|
||||
gutter_hitbox,
|
||||
cx,
|
||||
);
|
||||
Some(button)
|
||||
})
|
||||
.collect_vec()
|
||||
})
|
||||
}
|
||||
|
||||
fn layout_code_actions_indicator(
|
||||
&self,
|
||||
line_height: Pixels,
|
||||
@@ -1383,35 +1450,28 @@ impl EditorElement {
|
||||
) -> Option<AnyElement> {
|
||||
let mut active = false;
|
||||
let mut button = None;
|
||||
let row = newest_selection_head.row();
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
active = matches!(
|
||||
editor.context_menu.read().as_ref(),
|
||||
Some(crate::ContextMenu::CodeActions(_))
|
||||
);
|
||||
button = editor.render_code_actions_indicator(&self.style, active, cx);
|
||||
if let Some(crate::ContextMenu::CodeActions(CodeActionsMenu {
|
||||
deployed_from_indicator,
|
||||
..
|
||||
})) = editor.context_menu.read().as_ref()
|
||||
{
|
||||
active = deployed_from_indicator.map_or(true, |indicator_row| indicator_row == row);
|
||||
};
|
||||
button = editor.render_code_actions_indicator(&self.style, row, active, cx);
|
||||
});
|
||||
|
||||
let mut button = button?.into_any_element();
|
||||
let available_space = size(
|
||||
AvailableSpace::MinContent,
|
||||
AvailableSpace::Definite(line_height),
|
||||
let button = prepaint_gutter_button(
|
||||
button?,
|
||||
row,
|
||||
line_height,
|
||||
gutter_dimensions,
|
||||
scroll_pixel_position,
|
||||
gutter_hitbox,
|
||||
cx,
|
||||
);
|
||||
let indicator_size = button.layout_as_root(available_space, cx);
|
||||
|
||||
let blame_width = gutter_dimensions
|
||||
.git_blame_entries_width
|
||||
.unwrap_or(Pixels::ZERO);
|
||||
|
||||
let mut x = blame_width;
|
||||
let available_width = gutter_dimensions.margin + gutter_dimensions.left_padding
|
||||
- indicator_size.width
|
||||
- blame_width;
|
||||
x += available_width / 2.;
|
||||
|
||||
let mut y = newest_selection_head.row() as f32 * line_height - scroll_pixel_position.y;
|
||||
y += (line_height - indicator_size.height) / 2.;
|
||||
|
||||
button.prepaint_as_root(gutter_hitbox.origin + point(x, y), available_space, cx);
|
||||
Some(button)
|
||||
}
|
||||
|
||||
@@ -1767,7 +1827,7 @@ impl EditorElement {
|
||||
.pr(gpui::px(8.))
|
||||
.rounded_md()
|
||||
.shadow_md()
|
||||
.border()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().editor_subheader_background)
|
||||
.justify_between()
|
||||
@@ -2033,6 +2093,10 @@ impl EditorElement {
|
||||
crate::ContextMenuOrigin::GutterIndicator(row) => {
|
||||
// Context menu was spawned via a click on a gutter. Ensure it's a bit closer to the indicator than just a plain first column of the
|
||||
// text field.
|
||||
let snapshot = self.editor.update(cx, |this, cx| this.snapshot(cx));
|
||||
let row = Point::new(row, 0)
|
||||
.to_display_point(&snapshot.display_snapshot)
|
||||
.row();
|
||||
let x = -gutter_overshoot;
|
||||
let y = (row + 1) as f32 * line_height - scroll_pixel_position.y;
|
||||
(x, y)
|
||||
@@ -2349,6 +2413,10 @@ impl EditorElement {
|
||||
}
|
||||
});
|
||||
|
||||
for test_indicators in layout.test_indicators.iter_mut() {
|
||||
test_indicators.paint(cx);
|
||||
}
|
||||
|
||||
if let Some(indicator) = layout.code_actions_indicator.as_mut() {
|
||||
indicator.paint(cx);
|
||||
}
|
||||
@@ -2798,7 +2866,6 @@ impl EditorElement {
|
||||
let snapshot = layout.position_map.snapshot.clone();
|
||||
let theme = cx.theme().clone();
|
||||
let scrollbar_settings = EditorSettings::get_global(cx).scrollbar;
|
||||
let max_row = layout.max_row;
|
||||
|
||||
editor.scrollbar_marker_state.dirty = false;
|
||||
editor.scrollbar_marker_state.pending_refresh =
|
||||
@@ -2807,12 +2874,12 @@ impl EditorElement {
|
||||
let scrollbar_markers = cx
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
let max_point = snapshot.display_snapshot.buffer_snapshot.max_point();
|
||||
let mut marker_quads = Vec::new();
|
||||
|
||||
if scrollbar_settings.git_diff {
|
||||
let marker_row_ranges = snapshot
|
||||
.buffer_snapshot
|
||||
.git_diff_hunks_in_range(0..max_row)
|
||||
.git_diff_hunks_in_range(0..max_point.row)
|
||||
.map(|hunk| {
|
||||
let start_display_row =
|
||||
Point::new(hunk.associated_range.start, 0)
|
||||
@@ -2881,9 +2948,6 @@ impl EditorElement {
|
||||
}
|
||||
|
||||
if scrollbar_settings.diagnostics {
|
||||
let max_point =
|
||||
snapshot.display_snapshot.buffer_snapshot.max_point();
|
||||
|
||||
let diagnostics = snapshot
|
||||
.buffer_snapshot
|
||||
.diagnostics_in_range::<_, Point>(
|
||||
@@ -3222,6 +3286,39 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
fn prepaint_gutter_button(
|
||||
button: IconButton,
|
||||
row: u32,
|
||||
line_height: Pixels,
|
||||
gutter_dimensions: &GutterDimensions,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
gutter_hitbox: &Hitbox,
|
||||
cx: &mut WindowContext<'_>,
|
||||
) -> AnyElement {
|
||||
let mut button = button.into_any_element();
|
||||
let available_space = size(
|
||||
AvailableSpace::MinContent,
|
||||
AvailableSpace::Definite(line_height),
|
||||
);
|
||||
let indicator_size = button.layout_as_root(available_space, cx);
|
||||
|
||||
let blame_width = gutter_dimensions
|
||||
.git_blame_entries_width
|
||||
.unwrap_or(Pixels::ZERO);
|
||||
|
||||
let mut x = blame_width;
|
||||
let available_width = gutter_dimensions.margin + gutter_dimensions.left_padding
|
||||
- indicator_size.width
|
||||
- blame_width;
|
||||
x += available_width / 2.;
|
||||
|
||||
let mut y = row as f32 * line_height - scroll_pixel_position.y;
|
||||
y += (line_height - indicator_size.height) / 2.;
|
||||
|
||||
button.prepaint_as_root(gutter_hitbox.origin + point(x, y), available_space, cx);
|
||||
button
|
||||
}
|
||||
|
||||
fn render_inline_blame_entry(
|
||||
blame: &gpui::Model<GitBlame>,
|
||||
blame_entry: BlameEntry,
|
||||
@@ -3277,8 +3374,7 @@ fn render_blame_entry(
|
||||
|
||||
let relative_timestamp = blame_entry_relative_timestamp(&blame_entry, cx);
|
||||
|
||||
let pretty_commit_id = format!("{}", blame_entry.sha);
|
||||
let short_commit_id = pretty_commit_id.chars().take(6).collect::<String>();
|
||||
let short_commit_id = blame_entry.sha.display_short();
|
||||
|
||||
let author_name = blame_entry.author.as_deref().unwrap_or("<no name>");
|
||||
let name = util::truncate_and_trailoff(author_name, 20);
|
||||
@@ -3568,7 +3664,7 @@ impl Element for EditorElement {
|
||||
let mut style = Style::default();
|
||||
style.size.width = relative(1.).into();
|
||||
style.size.height = self.style.text.line_height_in_pixels(rem_size).into();
|
||||
cx.request_layout(&style, None)
|
||||
cx.request_layout(style, None)
|
||||
}
|
||||
EditorMode::AutoHeight { max_lines } => {
|
||||
let editor_handle = cx.view().clone();
|
||||
@@ -3592,7 +3688,7 @@ impl Element for EditorElement {
|
||||
let mut style = Style::default();
|
||||
style.size.width = relative(1.).into();
|
||||
style.size.height = relative(1.).into();
|
||||
cx.request_layout(&style, None)
|
||||
cx.request_layout(style, None)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3937,18 +4033,41 @@ impl Element for EditorElement {
|
||||
cx,
|
||||
);
|
||||
if gutter_settings.code_actions {
|
||||
code_actions_indicator = self.layout_code_actions_indicator(
|
||||
line_height,
|
||||
newest_selection_head,
|
||||
scroll_pixel_position,
|
||||
&gutter_dimensions,
|
||||
&gutter_hitbox,
|
||||
cx,
|
||||
);
|
||||
let newest_selection_point =
|
||||
newest_selection_head.to_point(&snapshot.display_snapshot);
|
||||
let buffer = snapshot
|
||||
.buffer_snapshot
|
||||
.buffer_line_for_row(newest_selection_point.row);
|
||||
if let Some((buffer, range)) = buffer {
|
||||
let buffer_id = buffer.remote_id();
|
||||
let row = range.start.row;
|
||||
let has_test_indicator =
|
||||
self.editor.read(cx).tasks.contains_key(&(buffer_id, row));
|
||||
|
||||
if !has_test_indicator {
|
||||
code_actions_indicator = self.layout_code_actions_indicator(
|
||||
line_height,
|
||||
newest_selection_head,
|
||||
scroll_pixel_position,
|
||||
&gutter_dimensions,
|
||||
&gutter_hitbox,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let test_indicators = self.layout_run_indicators(
|
||||
line_height,
|
||||
scroll_pixel_position,
|
||||
&gutter_dimensions,
|
||||
&gutter_hitbox,
|
||||
&snapshot,
|
||||
cx,
|
||||
);
|
||||
|
||||
if !context_menu_visible && !cx.has_active_drag() {
|
||||
self.layout_hover_popovers(
|
||||
&snapshot,
|
||||
@@ -4034,7 +4153,6 @@ impl Element for EditorElement {
|
||||
gutter_dimensions,
|
||||
content_origin,
|
||||
scrollbar_layout,
|
||||
max_row,
|
||||
active_rows,
|
||||
highlighted_rows,
|
||||
highlighted_ranges,
|
||||
@@ -4049,6 +4167,7 @@ impl Element for EditorElement {
|
||||
visible_cursors,
|
||||
selections,
|
||||
mouse_context_menu,
|
||||
test_indicators,
|
||||
code_actions_indicator,
|
||||
fold_indicators,
|
||||
tab_invisible,
|
||||
@@ -4166,8 +4285,8 @@ pub struct EditorLayout {
|
||||
cursors: Vec<(DisplayPoint, Hsla)>,
|
||||
visible_cursors: Vec<CursorLayout>,
|
||||
selections: Vec<(PlayerColor, Vec<SelectionLayout>)>,
|
||||
max_row: u32,
|
||||
code_actions_indicator: Option<AnyElement>,
|
||||
test_indicators: Vec<AnyElement>,
|
||||
fold_indicators: Vec<Option<AnyElement>>,
|
||||
mouse_context_menu: Option<AnyElement>,
|
||||
tab_invisible: ShapedLine,
|
||||
@@ -4382,20 +4501,26 @@ fn layout_line(
|
||||
) -> Result<ShapedLine> {
|
||||
let mut line = snapshot.line(row);
|
||||
|
||||
if line.len() > MAX_LINE_LEN {
|
||||
let mut len = MAX_LINE_LEN;
|
||||
while !line.is_char_boundary(len) {
|
||||
len -= 1;
|
||||
}
|
||||
let len = {
|
||||
let line_len = line.len();
|
||||
if line_len > MAX_LINE_LEN {
|
||||
let mut len = MAX_LINE_LEN;
|
||||
while !line.is_char_boundary(len) {
|
||||
len -= 1;
|
||||
}
|
||||
|
||||
line.truncate(len);
|
||||
}
|
||||
line.truncate(len);
|
||||
len
|
||||
} else {
|
||||
line_len
|
||||
}
|
||||
};
|
||||
|
||||
cx.text_system().shape_line(
|
||||
line.into(),
|
||||
style.text.font_size.to_pixels(cx.rem_size()),
|
||||
&[TextRun {
|
||||
len: snapshot.line_len(row) as usize,
|
||||
len,
|
||||
font: style.text.font(),
|
||||
color: Hsla::default(),
|
||||
background_color: None,
|
||||
|
||||
@@ -4,10 +4,7 @@ use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use git::{
|
||||
blame::{Blame, BlameEntry},
|
||||
hosting_provider::HostingProvider,
|
||||
permalink::{build_commit_permalink, parse_git_remote_url},
|
||||
pull_request::{extract_pull_request, PullRequest},
|
||||
Oid,
|
||||
parse_git_remote_url, GitHostingProvider, GitHostingProviderRegistry, Oid, PullRequest,
|
||||
};
|
||||
use gpui::{Model, ModelContext, Subscription, Task};
|
||||
use language::{markdown, Bias, Buffer, BufferSnapshot, Edit, LanguageRegistry, ParsedMarkdown};
|
||||
@@ -50,13 +47,23 @@ impl<'a> sum_tree::Dimension<'a, GitBlameEntrySummary> for u32 {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone)]
|
||||
pub struct GitRemote {
|
||||
pub host: HostingProvider,
|
||||
pub host: Arc<dyn GitHostingProvider + Send + Sync + 'static>,
|
||||
pub owner: String,
|
||||
pub repo: String,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for GitRemote {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("GitRemote")
|
||||
.field("host", &self.host.name())
|
||||
.field("owner", &self.owner)
|
||||
.field("repo", &self.repo)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl GitRemote {
|
||||
pub fn host_supports_avatars(&self) -> bool {
|
||||
self.host.supports_avatars()
|
||||
@@ -323,6 +330,7 @@ impl GitBlame {
|
||||
let snapshot = self.buffer.read(cx).snapshot();
|
||||
let blame = self.project.read(cx).blame_buffer(&self.buffer, None, cx);
|
||||
let languages = self.project.read(cx).languages().clone();
|
||||
let provider_registry = GitHostingProviderRegistry::default_global(cx);
|
||||
|
||||
self.task = cx.spawn(|this, mut cx| async move {
|
||||
let result = cx
|
||||
@@ -338,9 +346,14 @@ impl GitBlame {
|
||||
} = blame.await?;
|
||||
|
||||
let entries = build_blame_entry_sum_tree(entries, snapshot.max_point().row);
|
||||
let commit_details =
|
||||
parse_commit_messages(messages, remote_url, &permalinks, &languages)
|
||||
.await;
|
||||
let commit_details = parse_commit_messages(
|
||||
messages,
|
||||
remote_url,
|
||||
&permalinks,
|
||||
provider_registry,
|
||||
&languages,
|
||||
)
|
||||
.await;
|
||||
|
||||
anyhow::Ok((entries, commit_details))
|
||||
}
|
||||
@@ -431,19 +444,22 @@ async fn parse_commit_messages(
|
||||
messages: impl IntoIterator<Item = (Oid, String)>,
|
||||
remote_url: Option<String>,
|
||||
deprecated_permalinks: &HashMap<Oid, Url>,
|
||||
provider_registry: Arc<GitHostingProviderRegistry>,
|
||||
languages: &Arc<LanguageRegistry>,
|
||||
) -> HashMap<Oid, CommitDetails> {
|
||||
let mut commit_details = HashMap::default();
|
||||
|
||||
let parsed_remote_url = remote_url.as_deref().and_then(parse_git_remote_url);
|
||||
let parsed_remote_url = remote_url
|
||||
.as_deref()
|
||||
.and_then(|remote_url| parse_git_remote_url(provider_registry, remote_url));
|
||||
|
||||
for (oid, message) in messages {
|
||||
let parsed_message = parse_markdown(&message, &languages).await;
|
||||
|
||||
let permalink = if let Some(git_remote) = parsed_remote_url.as_ref() {
|
||||
Some(build_commit_permalink(
|
||||
git::permalink::BuildCommitPermalinkParams {
|
||||
remote: git_remote,
|
||||
let permalink = if let Some((provider, git_remote)) = parsed_remote_url.as_ref() {
|
||||
Some(provider.build_commit_permalink(
|
||||
git_remote,
|
||||
git::BuildCommitPermalinkParams {
|
||||
sha: oid.to_string().as_str(),
|
||||
},
|
||||
))
|
||||
@@ -455,15 +471,17 @@ async fn parse_commit_messages(
|
||||
deprecated_permalinks.get(&oid).cloned()
|
||||
};
|
||||
|
||||
let remote = parsed_remote_url.as_ref().map(|remote| GitRemote {
|
||||
host: remote.provider.clone(),
|
||||
owner: remote.owner.to_string(),
|
||||
repo: remote.repo.to_string(),
|
||||
});
|
||||
let remote = parsed_remote_url
|
||||
.as_ref()
|
||||
.map(|(provider, remote)| GitRemote {
|
||||
host: provider.clone(),
|
||||
owner: remote.owner.to_string(),
|
||||
repo: remote.repo.to_string(),
|
||||
});
|
||||
|
||||
let pull_request = parsed_remote_url
|
||||
.as_ref()
|
||||
.and_then(|remote| extract_pull_request(remote, &message));
|
||||
.and_then(|(provider, remote)| provider.extract_pull_request(remote, &message));
|
||||
|
||||
commit_details.insert(
|
||||
oid,
|
||||
|
||||
@@ -732,7 +732,7 @@ mod tests {
|
||||
|
||||
cx.cx
|
||||
.cx
|
||||
.simulate_mouse_move(screen_coord.unwrap(), Modifiers::command_shift());
|
||||
.simulate_mouse_move(screen_coord.unwrap(), None, Modifiers::command_shift());
|
||||
|
||||
requests.next().await;
|
||||
cx.run_until_parked();
|
||||
@@ -802,7 +802,7 @@ mod tests {
|
||||
])))
|
||||
});
|
||||
|
||||
cx.simulate_mouse_move(hover_point, Modifiers::secondary_key());
|
||||
cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
|
||||
requests.next().await;
|
||||
cx.background_executor.run_until_parked();
|
||||
cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
|
||||
@@ -828,7 +828,7 @@ mod tests {
|
||||
])))
|
||||
});
|
||||
|
||||
cx.simulate_mouse_move(hover_point, Modifiers::secondary_key());
|
||||
cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
|
||||
requests.next().await;
|
||||
cx.background_executor.run_until_parked();
|
||||
cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
|
||||
@@ -847,7 +847,7 @@ mod tests {
|
||||
// No definitions returned
|
||||
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
|
||||
});
|
||||
cx.simulate_mouse_move(hover_point, Modifiers::secondary_key());
|
||||
cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
|
||||
|
||||
requests.next().await;
|
||||
cx.background_executor.run_until_parked();
|
||||
@@ -863,7 +863,7 @@ mod tests {
|
||||
fn test() { do_work(); }
|
||||
fn do_work() { teˇst(); }
|
||||
"});
|
||||
cx.simulate_mouse_move(hover_point, Modifiers::none());
|
||||
cx.simulate_mouse_move(hover_point, None, Modifiers::none());
|
||||
|
||||
// Assert no link highlights
|
||||
cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
|
||||
@@ -907,7 +907,7 @@ mod tests {
|
||||
fn do_work() { test(); }
|
||||
"});
|
||||
|
||||
cx.simulate_mouse_move(hover_point, Modifiers::secondary_key());
|
||||
cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
|
||||
cx.background_executor.run_until_parked();
|
||||
cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
|
||||
fn test() { do_work(); }
|
||||
@@ -919,7 +919,7 @@ mod tests {
|
||||
fn test() { do_work(); }
|
||||
fn do_work() { tesˇt(); }
|
||||
"});
|
||||
cx.simulate_mouse_move(hover_point, Modifiers::secondary_key());
|
||||
cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
|
||||
cx.background_executor.run_until_parked();
|
||||
cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
|
||||
fn test() { do_work(); }
|
||||
@@ -1009,7 +1009,7 @@ mod tests {
|
||||
s.set_pending_anchor_range(anchor_range, crate::SelectMode::Character)
|
||||
});
|
||||
});
|
||||
cx.simulate_mouse_move(hover_point, Modifiers::secondary_key());
|
||||
cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
|
||||
cx.background_executor.run_until_parked();
|
||||
assert!(requests.try_next().is_err());
|
||||
cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
|
||||
@@ -1123,7 +1123,7 @@ mod tests {
|
||||
});
|
||||
// Press cmd to trigger highlight
|
||||
let hover_point = cx.pixel_position_for(midpoint);
|
||||
cx.simulate_mouse_move(hover_point, Modifiers::secondary_key());
|
||||
cx.simulate_mouse_move(hover_point, None, Modifiers::secondary_key());
|
||||
cx.background_executor.run_until_parked();
|
||||
cx.update_editor(|editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
@@ -1142,7 +1142,7 @@ mod tests {
|
||||
assert_set_eq!(actual_highlights, vec![&expected_highlight]);
|
||||
});
|
||||
|
||||
cx.simulate_mouse_move(hover_point, Modifiers::none());
|
||||
cx.simulate_mouse_move(hover_point, None, Modifiers::none());
|
||||
// Assert no link highlights
|
||||
cx.update_editor(|editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
@@ -1186,7 +1186,7 @@ mod tests {
|
||||
Let's test a [complex](https://zed.dev/channel/had-(ˇoops)) case.
|
||||
"});
|
||||
|
||||
cx.simulate_mouse_move(screen_coord, Modifiers::secondary_key());
|
||||
cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
|
||||
cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
|
||||
Let's test a [complex](«https://zed.dev/channel/had-(oops)ˇ») case.
|
||||
"});
|
||||
@@ -1214,7 +1214,7 @@ mod tests {
|
||||
let screen_coord =
|
||||
cx.pixel_position(indoc! {"https://zed.dev/relˇeases is a cool webpage."});
|
||||
|
||||
cx.simulate_mouse_move(screen_coord, Modifiers::secondary_key());
|
||||
cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
|
||||
cx.assert_editor_text_highlights::<HoveredLinkState>(
|
||||
indoc! {"«https://zed.dev/releasesˇ» is a cool webpage."},
|
||||
);
|
||||
@@ -1239,7 +1239,7 @@ mod tests {
|
||||
let screen_coord =
|
||||
cx.pixel_position(indoc! {"A cool webpage is https://zed.dev/releˇases"});
|
||||
|
||||
cx.simulate_mouse_move(screen_coord, Modifiers::secondary_key());
|
||||
cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
|
||||
cx.assert_editor_text_highlights::<HoveredLinkState>(
|
||||
indoc! {"A cool webpage is «https://zed.dev/releasesˇ»"},
|
||||
);
|
||||
|
||||
@@ -10,9 +10,10 @@ use gpui::{
|
||||
ParentElement, Pixels, SharedString, Size, StatefulInteractiveElement, Styled, Task,
|
||||
ViewContext, WeakView,
|
||||
};
|
||||
use language::{markdown, Bias, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown};
|
||||
use language::{markdown, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown};
|
||||
|
||||
use lsp::DiagnosticSeverity;
|
||||
use multi_buffer::ToOffset;
|
||||
use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart};
|
||||
use settings::Settings;
|
||||
use smol::stream::StreamExt;
|
||||
@@ -30,16 +31,16 @@ pub const HOVER_POPOVER_GAP: Pixels = px(10.);
|
||||
|
||||
/// Bindable action which uses the most recent selection head to trigger a hover
|
||||
pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {
|
||||
let head = editor.selections.newest_display(cx).head();
|
||||
let head = editor.selections.newest_anchor().head();
|
||||
show_hover(editor, head, true, cx);
|
||||
}
|
||||
|
||||
/// The internal hover action dispatches between `show_hover` or `hide_hover`
|
||||
/// depending on whether a point to hover over is provided.
|
||||
pub fn hover_at(editor: &mut Editor, point: Option<DisplayPoint>, cx: &mut ViewContext<Editor>) {
|
||||
pub fn hover_at(editor: &mut Editor, anchor: Option<Anchor>, cx: &mut ViewContext<Editor>) {
|
||||
if EditorSettings::get_global(cx).hover_popover_enabled {
|
||||
if let Some(point) = point {
|
||||
show_hover(editor, point, false, cx);
|
||||
if let Some(anchor) = anchor {
|
||||
show_hover(editor, anchor, false, cx);
|
||||
} else {
|
||||
hide_hover(editor, cx);
|
||||
}
|
||||
@@ -159,7 +160,7 @@ pub fn hide_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool {
|
||||
/// Triggered by the `Hover` action when the cursor may be over a symbol.
|
||||
fn show_hover(
|
||||
editor: &mut Editor,
|
||||
point: DisplayPoint,
|
||||
anchor: Anchor,
|
||||
ignore_timeout: bool,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
@@ -168,27 +169,20 @@ fn show_hover(
|
||||
}
|
||||
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let multibuffer_offset = point.to_offset(&snapshot.display_snapshot, Bias::Left);
|
||||
|
||||
let (buffer, buffer_position) = if let Some(output) = editor
|
||||
.buffer
|
||||
.read(cx)
|
||||
.text_anchor_for_position(multibuffer_offset, cx)
|
||||
{
|
||||
output
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
let (buffer, buffer_position) =
|
||||
if let Some(output) = editor.buffer.read(cx).text_anchor_for_position(anchor, cx) {
|
||||
output
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let excerpt_id = if let Some((excerpt_id, _, _)) = editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.excerpt_containing(multibuffer_offset, cx)
|
||||
{
|
||||
excerpt_id
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
let excerpt_id =
|
||||
if let Some((excerpt_id, _, _)) = editor.buffer().read(cx).excerpt_containing(anchor, cx) {
|
||||
excerpt_id
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let project = if let Some(project) = editor.project.clone() {
|
||||
project
|
||||
@@ -206,9 +200,10 @@ fn show_hover(
|
||||
.as_text_range()
|
||||
.map(|range| {
|
||||
let hover_range = range.to_offset(&snapshot.buffer_snapshot);
|
||||
let offset = anchor.to_offset(&snapshot.buffer_snapshot);
|
||||
// LSP returns a hover result for the end index of ranges that should be hovered, so we need to
|
||||
// use an inclusive range here to check if we should dismiss the popover
|
||||
(hover_range.start..=hover_range.end).contains(&multibuffer_offset)
|
||||
(hover_range.start..=hover_range.end).contains(&offset)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
})
|
||||
@@ -220,11 +215,6 @@ fn show_hover(
|
||||
}
|
||||
}
|
||||
|
||||
// Get input anchor
|
||||
let anchor = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_at(multibuffer_offset, Bias::Left);
|
||||
|
||||
// Don't request again if the location is the same as the previous request
|
||||
if let Some(triggered_from) = &editor.hover_state.triggered_from {
|
||||
if triggered_from
|
||||
@@ -267,7 +257,7 @@ fn show_hover(
|
||||
// If there's a diagnostic, assign it on the hover state and notify
|
||||
let local_diagnostic = snapshot
|
||||
.buffer_snapshot
|
||||
.diagnostics_in_range::<_, usize>(multibuffer_offset..multibuffer_offset, false)
|
||||
.diagnostics_in_range::<_, usize>(anchor..anchor, false)
|
||||
// Find the entry with the most specific range
|
||||
.min_by_key(|entry| entry.range.end - entry.range.start)
|
||||
.map(|entry| DiagnosticEntry {
|
||||
@@ -635,6 +625,7 @@ mod tests {
|
||||
use lsp::LanguageServerId;
|
||||
use project::{HoverBlock, HoverBlockKind};
|
||||
use smol::stream::StreamExt;
|
||||
use text::Bias;
|
||||
use unindent::Unindent;
|
||||
use util::test::marked_text_ranges;
|
||||
|
||||
@@ -659,7 +650,13 @@ mod tests {
|
||||
fn test() { printˇln!(); }
|
||||
"});
|
||||
|
||||
cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
|
||||
cx.update_editor(|editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let anchor = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
|
||||
hover_at(editor, Some(anchor), cx)
|
||||
});
|
||||
assert!(!cx.editor(|editor, _| editor.hover_state.visible()));
|
||||
|
||||
// After delay, hover should be visible.
|
||||
@@ -705,7 +702,13 @@ mod tests {
|
||||
let mut request = cx
|
||||
.lsp
|
||||
.handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
|
||||
cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
|
||||
cx.update_editor(|editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let anchor = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
|
||||
hover_at(editor, Some(anchor), cx)
|
||||
});
|
||||
cx.background_executor
|
||||
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
|
||||
request.next().await;
|
||||
|
||||