Compare commits
420 Commits
linux-fix-
...
language-m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
500b581a46 | ||
|
|
ccc6e13f99 | ||
|
|
aceb5581b3 | ||
|
|
24c8bad8de | ||
|
|
8f6ea25a95 | ||
|
|
ed94bd41eb | ||
|
|
8949460bd7 | ||
|
|
c6c5907693 | ||
|
|
dea928b00c | ||
|
|
77b2da2b42 | ||
|
|
5a8c2a4a88 | ||
|
|
d46e494bd9 | ||
|
|
82435075a5 | ||
|
|
40748b0a15 | ||
|
|
3ee3c6a3bd | ||
|
|
6cc3a4d95c | ||
|
|
b58dfe502e | ||
|
|
03e2f240ee | ||
|
|
145cd798c0 | ||
|
|
9ef9baef6f | ||
|
|
d2a2faf7a2 | ||
|
|
10f7ca65cf | ||
|
|
354427413a | ||
|
|
9813297892 | ||
|
|
78bc3a9a36 | ||
|
|
73de99bee0 | ||
|
|
0ed1b29b01 | ||
|
|
5b754915e4 | ||
|
|
9298d3b525 | ||
|
|
89739d5874 | ||
|
|
d272e402ea | ||
|
|
5c93506e9f | ||
|
|
7df8b6fe10 | ||
|
|
6fba1e46a8 | ||
|
|
988ee93a81 | ||
|
|
00a505e41a | ||
|
|
ed9f6e2141 | ||
|
|
fe7d53cb96 | ||
|
|
edca195e3c | ||
|
|
d3b3e072a7 | ||
|
|
6b04b668ad | ||
|
|
4072ad2858 | ||
|
|
cb0b8b4c4b | ||
|
|
c58a8f1a04 | ||
|
|
abb46473c9 | ||
|
|
9bdb154a9b | ||
|
|
f69c8ca74e | ||
|
|
04a79780d8 | ||
|
|
4dd05a80e0 | ||
|
|
44c479c50c | ||
|
|
c8709978a1 | ||
|
|
f78f6a6e1e | ||
|
|
fefc91c6ad | ||
|
|
3076567f6b | ||
|
|
6eb537643a | ||
|
|
40eb84109d | ||
|
|
51601cf6bd | ||
|
|
2c545ce0bc | ||
|
|
58e9952d7b | ||
|
|
25c8cf0c5c | ||
|
|
d501a877a0 | ||
|
|
97abf35529 | ||
|
|
0150192e26 | ||
|
|
710c387395 | ||
|
|
5a6c55149a | ||
|
|
d5b0df6efa | ||
|
|
4e2a08edb7 | ||
|
|
c20a1ee032 | ||
|
|
f5f73efa8a | ||
|
|
d8c93e1bfd | ||
|
|
95b06097ee | ||
|
|
963b0c010a | ||
|
|
558808b97d | ||
|
|
4b19eac5c8 | ||
|
|
47174cea50 | ||
|
|
0129d4e250 | ||
|
|
3d4f275c52 | ||
|
|
cd2533de5a | ||
|
|
acc9648753 | ||
|
|
bc35235800 | ||
|
|
2ca83b2f17 | ||
|
|
ddf07253c4 | ||
|
|
aff7a83815 | ||
|
|
8524e87319 | ||
|
|
59c005b086 | ||
|
|
522692ef50 | ||
|
|
d665f28671 | ||
|
|
8c4fb34f6e | ||
|
|
d4891a62bb | ||
|
|
17bc0d1b17 | ||
|
|
db0d843fb1 | ||
|
|
ad4e52842c | ||
|
|
ca18549e02 | ||
|
|
5cbb360952 | ||
|
|
5ff7c893be | ||
|
|
99e4b3a4cf | ||
|
|
9af4b6bfc7 | ||
|
|
c84d432b5f | ||
|
|
6cea9813ad | ||
|
|
490a75aee6 | ||
|
|
4f364d6d09 | ||
|
|
89d2ace713 | ||
|
|
84a44bef8a | ||
|
|
6b9ddbfef2 | ||
|
|
8af8493da6 | ||
|
|
39edbe1c50 | ||
|
|
f6fa6600bc | ||
|
|
b55961b57a | ||
|
|
01b836a191 | ||
|
|
41180b8d81 | ||
|
|
81475ac4cd | ||
|
|
ba59e66314 | ||
|
|
5ede48337c | ||
|
|
3701e190ce | ||
|
|
5dc26c261d | ||
|
|
64d815a176 | ||
|
|
5dc54863a4 | ||
|
|
e4ba336971 | ||
|
|
195a270e18 | ||
|
|
3a26a4809d | ||
|
|
479c5df491 | ||
|
|
51404d4ea0 | ||
|
|
05c4c7872c | ||
|
|
0af6e442a7 | ||
|
|
7003b0f211 | ||
|
|
f489c8b79f | ||
|
|
be02b2faf4 | ||
|
|
258a8a37d8 | ||
|
|
78e0f71a28 | ||
|
|
59104a08fd | ||
|
|
7aa28c9b24 | ||
|
|
bb1d52b485 | ||
|
|
ca035dbdd8 | ||
|
|
71cc95d315 | ||
|
|
3707734f0a | ||
|
|
e19627d92f | ||
|
|
bb75d87285 | ||
|
|
eecbf203dc | ||
|
|
7fe5c27597 | ||
|
|
221edfc267 | ||
|
|
d95c424d18 | ||
|
|
d6d56191da | ||
|
|
2e87e1d26e | ||
|
|
e8862c45cc | ||
|
|
0c28b6a11a | ||
|
|
16fce64d3a | ||
|
|
b075ce8f04 | ||
|
|
54828ab836 | ||
|
|
6322351f00 | ||
|
|
78091fa91e | ||
|
|
d5735dab9a | ||
|
|
c793bbde84 | ||
|
|
03c54623d4 | ||
|
|
0afb3abfd2 | ||
|
|
2b46a4a0e9 | ||
|
|
bedf57db89 | ||
|
|
4855da53df | ||
|
|
15d3e54ae3 | ||
|
|
064bdab459 | ||
|
|
38cb95f427 | ||
|
|
7cc2538fe1 | ||
|
|
fc19cc0ddf | ||
|
|
e6def62c23 | ||
|
|
ff2347dff5 | ||
|
|
6319ae0b4a | ||
|
|
a8bd602334 | ||
|
|
af45db6d1e | ||
|
|
fab4b01655 | ||
|
|
2f6cb49d84 | ||
|
|
411ee7a47c | ||
|
|
831f7dbbc0 | ||
|
|
78fd378702 | ||
|
|
d5a6ca4914 | ||
|
|
ea69846281 | ||
|
|
ff8486e67f | ||
|
|
9bc3c6810b | ||
|
|
45ae0dcc2d | ||
|
|
e40c49a143 | ||
|
|
0d43d484f6 | ||
|
|
6ca09bd4ba | ||
|
|
53f702c92f | ||
|
|
b03653321f | ||
|
|
993109aee1 | ||
|
|
4cb45e63f4 | ||
|
|
1413b5af93 | ||
|
|
d9c21b4eb1 | ||
|
|
44f66aa426 | ||
|
|
3b84b106e2 | ||
|
|
3539a7c04a | ||
|
|
a8481099ca | ||
|
|
6c28b7e8b8 | ||
|
|
0d8e6e6b12 | ||
|
|
bf03f66d02 | ||
|
|
0f59607100 | ||
|
|
902d7150fe | ||
|
|
55ba80ddd1 | ||
|
|
dcb8dc16ca | ||
|
|
eb7a09b459 | ||
|
|
1248788780 | ||
|
|
64bb79b71d | ||
|
|
a5af5b2883 | ||
|
|
068b1c235c | ||
|
|
8edfd0a963 | ||
|
|
0ed5327b1c | ||
|
|
10d3ad4e33 | ||
|
|
066cdc2297 | ||
|
|
01ba1ddef7 | ||
|
|
86167138a9 | ||
|
|
e0c1ab650e | ||
|
|
407188f816 | ||
|
|
6181ac6bad | ||
|
|
0705fb9b97 | ||
|
|
042be3529d | ||
|
|
1a40e98413 | ||
|
|
af8e7af265 | ||
|
|
ce51c264a6 | ||
|
|
702fd8f168 | ||
|
|
2e758dcb64 | ||
|
|
38d9ee3731 | ||
|
|
95c69d0696 | ||
|
|
599102573a | ||
|
|
da281d6d8f | ||
|
|
22dc88ed3d | ||
|
|
3c0310273b | ||
|
|
14bf07c916 | ||
|
|
284559742d | ||
|
|
85acc2be44 | ||
|
|
6c70a809ec | ||
|
|
a35947c883 | ||
|
|
f8ad5fe3e9 | ||
|
|
e2cfbc54ad | ||
|
|
0a13b9ee01 | ||
|
|
eb7b5a7131 | ||
|
|
7798f64d1b | ||
|
|
21764c38dd | ||
|
|
cfbf5dca7a | ||
|
|
2f43d52e7e | ||
|
|
e1f4dfc068 | ||
|
|
9e3c5f3e12 | ||
|
|
f780504b68 | ||
|
|
76b0120665 | ||
|
|
0ac9af94e0 | ||
|
|
ec086945fc | ||
|
|
8451dba6a7 | ||
|
|
7f56f4e78e | ||
|
|
6fa347dff7 | ||
|
|
c3df9b79c6 | ||
|
|
72dac24acf | ||
|
|
001f17c011 | ||
|
|
3c3dad6830 | ||
|
|
1b28f93c64 | ||
|
|
2fd00a8f35 | ||
|
|
bee3441c78 | ||
|
|
113546f766 | ||
|
|
5e9f9b4edd | ||
|
|
ec95a33d8c | ||
|
|
b82350979f | ||
|
|
e16bbe048f | ||
|
|
ab41eddd8b | ||
|
|
4cb8d6f40e | ||
|
|
127b9ed857 | ||
|
|
c30f6a1582 | ||
|
|
8ccd2a0c99 | ||
|
|
57b87be3a0 | ||
|
|
80c14c9198 | ||
|
|
ec9e700e70 | ||
|
|
a06189bbed | ||
|
|
53b0720d54 | ||
|
|
b6ea393d14 | ||
|
|
98659eabf1 | ||
|
|
3722275cfa | ||
|
|
ef84ce76e3 | ||
|
|
44a58647e4 | ||
|
|
4e98c23463 | ||
|
|
1914a42b1c | ||
|
|
6afed19a00 | ||
|
|
2509af723f | ||
|
|
8078e58494 | ||
|
|
b69c3129d0 | ||
|
|
e2c5ce588b | ||
|
|
7005aaa54d | ||
|
|
9db269735d | ||
|
|
77e88c1ded | ||
|
|
57c40299a5 | ||
|
|
0d5485bd6c | ||
|
|
a600799840 | ||
|
|
05b6581147 | ||
|
|
43d1a8040d | ||
|
|
e829a8c3b0 | ||
|
|
87845a349d | ||
|
|
953393f6ce | ||
|
|
75f8be6a0f | ||
|
|
e174f16d50 | ||
|
|
aa60fc2f19 | ||
|
|
5548773b2e | ||
|
|
3eac83eece | ||
|
|
243a0e764d | ||
|
|
6fa6e0718c | ||
|
|
834089feb1 | ||
|
|
9174858225 | ||
|
|
a910f192db | ||
|
|
5f5e6b8616 | ||
|
|
07dbd2bce8 | ||
|
|
48581167b7 | ||
|
|
00dfd217d8 | ||
|
|
22490f7968 | ||
|
|
880940856d | ||
|
|
c354793871 | ||
|
|
2f057785f7 | ||
|
|
fd39f20842 | ||
|
|
0c7e745be8 | ||
|
|
3000f6ea22 | ||
|
|
377e24b798 | ||
|
|
a0c0f1ebcd | ||
|
|
70ce06cb95 | ||
|
|
9a5b97db00 | ||
|
|
0b75afd322 | ||
|
|
4fd698a093 | ||
|
|
b50846205c | ||
|
|
a574036efd | ||
|
|
89641acf2f | ||
|
|
611bf2d905 | ||
|
|
f476a8bc2a | ||
|
|
d3d0d01571 | ||
|
|
29d29f5a90 | ||
|
|
9824e40878 | ||
|
|
1ad8d6ab1c | ||
|
|
8745719687 | ||
|
|
c7c19609b3 | ||
|
|
428c143fbb | ||
|
|
f3460d440c | ||
|
|
071270fe88 | ||
|
|
a59dd7d06d | ||
|
|
868284876d | ||
|
|
6bbe9a2253 | ||
|
|
7a05db6d3d | ||
|
|
3587e9726b | ||
|
|
a96782cc6b | ||
|
|
0289c312c9 | ||
|
|
63a8095879 | ||
|
|
1768c0d996 | ||
|
|
27e9c68988 | ||
|
|
ad2ddf1200 | ||
|
|
d6e271c956 | ||
|
|
da29e33f50 | ||
|
|
3fd118f8e1 | ||
|
|
27beb9e697 | ||
|
|
c7d56302d2 | ||
|
|
74cb92f9cc | ||
|
|
8a659b0c60 | ||
|
|
25050e8027 | ||
|
|
2d9479667f | ||
|
|
1c617474fe | ||
|
|
1a0708f28c | ||
|
|
62e790074c | ||
|
|
c5b22eee2d | ||
|
|
e4bb666eab | ||
|
|
910f668f4d | ||
|
|
8e79609288 | ||
|
|
47122a3115 | ||
|
|
edd613062a | ||
|
|
3cd6719b30 | ||
|
|
afc0650a49 | ||
|
|
14c2fab8ab | ||
|
|
c752763301 | ||
|
|
2f65c3c6e6 | ||
|
|
959f0dcded | ||
|
|
be2df79d5c | ||
|
|
344e5e1cf2 | ||
|
|
ed86b86dc7 | ||
|
|
726f23e867 | ||
|
|
b1efea1100 | ||
|
|
2b21c89e3c | ||
|
|
d0fa012bf8 | ||
|
|
338df5de1d | ||
|
|
5f98b9617a | ||
|
|
18e2b43d6d | ||
|
|
5e3d85c023 | ||
|
|
ae55d35f19 | ||
|
|
d0142b820f | ||
|
|
b218d8778d | ||
|
|
de8ef08143 | ||
|
|
66b73c2d60 | ||
|
|
ab8d25e0a2 | ||
|
|
95e360b170 | ||
|
|
f0d979576d | ||
|
|
fbcc5ccdb9 | ||
|
|
29b5253a1d | ||
|
|
94c3101fb0 | ||
|
|
a6e0c8aca1 | ||
|
|
d12b8c3945 | ||
|
|
356fcec337 | ||
|
|
08123a270a | ||
|
|
6eb8e83411 | ||
|
|
4c51ee7816 | ||
|
|
660cf214c7 | ||
|
|
b2565fadfb | ||
|
|
2cff075c53 | ||
|
|
819bb2663d | ||
|
|
dc141d0f61 | ||
|
|
22cf73acec | ||
|
|
1d46a52c62 | ||
|
|
fda975fb76 | ||
|
|
0f32145ecb | ||
|
|
6fe665ab94 | ||
|
|
279c5ab81f | ||
|
|
99901801f4 | ||
|
|
4dc98026c4 | ||
|
|
c83d1c23d7 | ||
|
|
39a2cdb13f | ||
|
|
8f942bf647 | ||
|
|
1ecd13ba50 | ||
|
|
c118012223 | ||
|
|
7a30937e21 | ||
|
|
3c5d141a04 | ||
|
|
bf7c6a676a | ||
|
|
a259042f92 | ||
|
|
436a8fa0ce | ||
|
|
55c47305c8 | ||
|
|
6ff01b17ca |
@@ -1,15 +0,0 @@
|
||||
# This config is different from config.toml in this directory, as the latter is recognized by Cargo.
|
||||
# This file is placed in $HOME/.cargo/config.toml on CI runs. Cargo then merges Zeds .cargo/config.toml with $HOME/.cargo/config.toml
|
||||
# with preference for settings from Zeds config.toml.
|
||||
# TL;DR: If a value is set in both ci-config.toml and config.toml, config.toml value takes precedence.
|
||||
# Arrays are merged together though. See: https://doc.rust-lang.org/cargo/reference/config.html#hierarchical-structure
|
||||
# The intent for this file is to configure CI build process with a divergance from Zed developers experience; for example, in this config file
|
||||
# we use `-D warnings` for rustflags (which makes compilation fail in presence of warnings during build process). Placing that in developers `config.toml`
|
||||
# would be incovenient.
|
||||
# We *could* override things like RUSTFLAGS manually by setting them as environment variables, but that is less DRY; worse yet, if you forget to set proper environment variables
|
||||
# in one spot, that's going to trigger a rebuild of all of the artifacts. Using ci-config.toml we can define these overrides for CI in one spot and not worry about it.
|
||||
[build]
|
||||
rustflags = ["-D", "warnings"]
|
||||
|
||||
[alias]
|
||||
xtask = "run --package xtask --"
|
||||
5
.cargo/collab-config.toml
Normal file
@@ -0,0 +1,5 @@
|
||||
# This file is used to build collab in a Docker image.
|
||||
# In particular, we don't use clang.
|
||||
[build]
|
||||
# v0 mangling scheme provides more detailed backtraces around closures
|
||||
rustflags = ["-C", "symbol-mangling-version=v0", "--cfg", "tokio_unstable"]
|
||||
@@ -4,3 +4,11 @@ rustflags = ["-C", "symbol-mangling-version=v0", "--cfg", "tokio_unstable"]
|
||||
|
||||
[alias]
|
||||
xtask = "run --package xtask --"
|
||||
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
linker = "clang"
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "clang"
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
|
||||
38
.github/ISSUE_TEMPLATE/0_feature_request.yml
vendored
@@ -2,23 +2,23 @@ name: Feature Request
|
||||
description: "Tip: open this issue template from within Zed with the `request feature` command palette action"
|
||||
labels: ["admin read", "triage", "enhancement"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Check for existing issues
|
||||
description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (👍) on it.
|
||||
options:
|
||||
- label: Completed
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the feature
|
||||
description: A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Check for existing issues
|
||||
description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (👍) on it.
|
||||
options:
|
||||
- label: Completed
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: |
|
||||
If applicable, add mockups / screenshots to help present your vision of the feature
|
||||
description: Drag images into the text input below
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the feature
|
||||
description: A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: |
|
||||
If applicable, add mockups / screenshots to help present your vision of the feature
|
||||
description: Drag images into the text input below
|
||||
validations:
|
||||
required: false
|
||||
|
||||
75
.github/ISSUE_TEMPLATE/1_bug_report.yml
vendored
@@ -1,40 +1,45 @@
|
||||
name: Bug Report
|
||||
description: |
|
||||
Use this template for **non-crash-related** bug reports.
|
||||
Tip: open this issue template from within Zed with the `file bug report` command palette action.
|
||||
Use this template for **non-crash-related** bug reports.
|
||||
Tip: open this issue template from within Zed with the `file bug report` command palette action.
|
||||
labels: ["admin read", "triage", "defect"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Check for existing issues
|
||||
description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (👍) on it.
|
||||
options:
|
||||
- label: Completed
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the bug / provide steps to reproduce it
|
||||
description: A clear and concise description of what the bug is.
|
||||
validations:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Check for existing issues
|
||||
description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (👍) on it.
|
||||
options:
|
||||
- label: Completed
|
||||
required: true
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: Environment
|
||||
description: Run the `copy system specs into clipboard` command palette action and paste the output in the field below.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: If applicable, add mockups / screenshots to help explain present your vision of the feature
|
||||
description: Drag issues into the text input below
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: If applicable, attach your `~/Library/Logs/Zed/Zed.log` file to this issue.
|
||||
description: |
|
||||
Drag Zed.log into the text input below.
|
||||
If you only need the most recent lines, you can run the `zed: open log` command palette action to see the last 1000.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the bug / provide steps to reproduce it
|
||||
description: A clear and concise description of what the bug is.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: Environment
|
||||
description: Run the `copy system specs into clipboard` command palette action and paste the output in the field below.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: If applicable, add mockups / screenshots to help explain present your vision of the feature
|
||||
description: Drag issues into the text input below
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: If applicable, attach your `~/Library/Logs/Zed/Zed.log` file to this issue.
|
||||
description: |
|
||||
Drag Zed.log into the text input below.
|
||||
If you only need the most recent lines, you can run the `zed: open log` command palette action to see the last 1000.
|
||||
value: |
|
||||
<details><summary>Zed.log</summary><pre>
|
||||
<!-- Click below this line and paste or drag-and-drop your log-->
|
||||
|
||||
<!-- Click above this line and paste or drag-and-drop your log--></pre></details>
|
||||
validations:
|
||||
required: false
|
||||
|
||||
61
.github/ISSUE_TEMPLATE/2_crash_report.yml
vendored
@@ -1,33 +1,38 @@
|
||||
name: Crash Report
|
||||
description: |
|
||||
Use this template for crash reports.
|
||||
Use this template for crash reports.
|
||||
labels: ["admin read", "triage", "defect", "panic / crash"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Check for existing issues
|
||||
description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (👍) on it.
|
||||
options:
|
||||
- label: Completed
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the bug / provide steps to reproduce it
|
||||
description: A clear and concise description of what the bug is.
|
||||
validations:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Check for existing issues
|
||||
description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (👍) on it.
|
||||
options:
|
||||
- label: Completed
|
||||
required: true
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: Environment
|
||||
description: Run the `copy system specs into clipboard` command palette action and paste the output in the field below.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: If applicable, attach your `~/Library/Logs/Zed/Zed.log` file to this issue.
|
||||
description: |
|
||||
Drag Zed.log into the text input below.
|
||||
If you only need the most recent lines, you can run the `zed: open log` command palette action to see the last 1000.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the bug / provide steps to reproduce it
|
||||
description: A clear and concise description of what the bug is.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: Environment
|
||||
description: Run the `copy system specs into clipboard` command palette action and paste the output in the field below.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: If applicable, attach your `~/Library/Logs/Zed/Zed.log` file to this issue.
|
||||
description: |
|
||||
Drag Zed.log into the text input below.
|
||||
If you only need the most recent lines, you can run the `zed: open log` command palette action to see the last 1000.
|
||||
value: |
|
||||
<details><summary>Zed.log</summary><pre>
|
||||
<!-- Click below this line and paste or drag-and-drop your log-->
|
||||
|
||||
<!-- Click above this line and paste or drag-and-drop your log--></pre></details>
|
||||
validations:
|
||||
required: false
|
||||
|
||||
116
.github/workflows/ci.yml
vendored
@@ -23,6 +23,7 @@ env:
|
||||
|
||||
jobs:
|
||||
style:
|
||||
timeout-minutes: 60
|
||||
name: Check formatting and spelling
|
||||
runs-on:
|
||||
- self-hosted
|
||||
@@ -37,9 +38,6 @@ jobs:
|
||||
- name: Remove untracked files
|
||||
run: git clean -df
|
||||
|
||||
- name: Set up default .cargo/config.toml
|
||||
run: cp ./.cargo/ci-config.toml ~/.cargo/config.toml
|
||||
|
||||
- name: Check spelling
|
||||
run: |
|
||||
if ! which typos > /dev/null; then
|
||||
@@ -53,6 +51,9 @@ jobs:
|
||||
- name: Check unused dependencies
|
||||
uses: bnjbvr/cargo-machete@main
|
||||
|
||||
- name: Check licenses are present
|
||||
run: script/check-licenses
|
||||
|
||||
- name: Check license generation
|
||||
run: script/generate-licenses /tmp/zed_licenses_output
|
||||
|
||||
@@ -73,10 +74,11 @@ jobs:
|
||||
version: v1.29.0
|
||||
- uses: bufbuild/buf-breaking-action@v1
|
||||
with:
|
||||
input: "crates/rpc/proto/"
|
||||
against: "https://github.com/${GITHUB_REPOSITORY}.git#branch=${BUF_BASE_BRANCH},subdir=crates/rpc/proto/"
|
||||
input: "crates/proto/proto/"
|
||||
against: "https://github.com/${GITHUB_REPOSITORY}.git#branch=${BUF_BASE_BRANCH},subdir=crates/proto/proto/"
|
||||
|
||||
macos_tests:
|
||||
timeout-minutes: 60
|
||||
name: (macOS) Run Clippy and tests
|
||||
runs-on:
|
||||
- self-hosted
|
||||
@@ -88,7 +90,7 @@ jobs:
|
||||
clean: false
|
||||
|
||||
- name: cargo clippy
|
||||
run: cargo xtask clippy
|
||||
run: ./script/clippy
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
@@ -99,8 +101,8 @@ jobs:
|
||||
- name: Build other binaries and features
|
||||
run: cargo build --workspace --bins --all-features; cargo check -p gpui --features "macos-blade"
|
||||
|
||||
# todo(linux): Actually run the tests
|
||||
linux_tests:
|
||||
timeout-minutes: 60
|
||||
name: (Linux) Run Clippy and tests
|
||||
runs-on:
|
||||
- self-hosted
|
||||
@@ -115,13 +117,17 @@ jobs:
|
||||
clean: false
|
||||
|
||||
- name: cargo clippy
|
||||
run: cargo xtask clippy
|
||||
run: ./script/clippy
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
|
||||
- name: Build Zed
|
||||
run: cargo build -p zed
|
||||
|
||||
# todo(windows): Actually run the tests
|
||||
windows_tests:
|
||||
timeout-minutes: 60
|
||||
name: (Windows) Run Clippy and tests
|
||||
runs-on: hosted-windows-1
|
||||
steps:
|
||||
@@ -136,12 +142,13 @@ jobs:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: cargo clippy
|
||||
run: cargo xtask clippy
|
||||
run: ./script/clippy
|
||||
|
||||
- name: Build Zed
|
||||
run: cargo build -p zed
|
||||
|
||||
bundle-mac:
|
||||
timeout-minutes: 60
|
||||
name: Create a macOS bundle
|
||||
runs-on:
|
||||
- self-hosted
|
||||
@@ -252,6 +259,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
bundle-linux:
|
||||
timeout-minutes: 60
|
||||
name: Create a Linux bundle
|
||||
runs-on:
|
||||
- self-hosted
|
||||
@@ -299,10 +307,7 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
|
||||
- name: Create and upload Linux .tar.gz bundle
|
||||
- name: Create Linux .tar.gz bundle
|
||||
run: script/bundle-linux
|
||||
|
||||
- name: Upload Linux bundle to workflow run if main branch or specific label
|
||||
@@ -310,7 +315,7 @@ jobs:
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-linux-gnu.tar.gz
|
||||
path: zed-*.tar.gz
|
||||
path: target/release/zed-*.tar.gz
|
||||
|
||||
- name: Upload app bundle to release
|
||||
uses: softprops/action-gh-release@v1
|
||||
@@ -322,3 +327,86 @@ jobs:
|
||||
body: ""
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
bundle-linux-aarch64:
|
||||
timeout-minutes: 60
|
||||
name: Create arm64 Linux bundle
|
||||
runs-on:
|
||||
- hosted-linux-arm-1
|
||||
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: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
- name: "Setup jq"
|
||||
uses: dcarbone/install-jq-action@v2
|
||||
|
||||
- name: Set up Clang
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y llvm-10 clang-10 build-essential cmake pkg-config libasound2-dev libfontconfig-dev libwayland-dev libxkbcommon-x11-dev libssl-dev libsqlite3-dev libzstd-dev libvulkan1 libgit2-dev
|
||||
echo "/usr/lib/llvm-10/bin" >> $GITHUB_PATH
|
||||
|
||||
- uses: rui314/setup-mold@v1
|
||||
with:
|
||||
mold-version: 2.32.0
|
||||
|
||||
- name: rustup
|
||||
run: |
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
|
||||
- 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') }}
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
version=$(script/get-crate-version zed)
|
||||
channel=$(cat crates/zed/RELEASE_CHANNEL)
|
||||
echo "Publishing version: ${version} on release channel ${channel}"
|
||||
echo "RELEASE_CHANNEL=${channel}" >> $GITHUB_ENV
|
||||
|
||||
expected_tag_name=""
|
||||
case ${channel} in
|
||||
stable)
|
||||
expected_tag_name="v${version}";;
|
||||
preview)
|
||||
expected_tag_name="v${version}-pre";;
|
||||
nightly)
|
||||
expected_tag_name="v${version}-nightly";;
|
||||
*)
|
||||
echo "can't publish a release on channel ${channel}"
|
||||
exit 1;;
|
||||
esac
|
||||
if [[ $GITHUB_REF_NAME != $expected_tag_name ]]; then
|
||||
echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Create and upload Linux .tar.gz bundle
|
||||
run: script/bundle-linux
|
||||
|
||||
- name: Upload Linux bundle to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-aarch64-unknown-linux-gnu.tar.gz
|
||||
path: target/release/zed-*.tar.gz
|
||||
|
||||
- name: Upload app bundle to release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||
with:
|
||||
draft: true
|
||||
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||
files: target/release/zed-linux-aarch64.tar.gz
|
||||
body: ""
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
2
.github/workflows/danger.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 8
|
||||
version: 9
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
5
.github/workflows/deploy_collab.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
uses: ./.github/actions/check_style
|
||||
|
||||
- name: Run clippy
|
||||
run: cargo xtask clippy
|
||||
run: ./script/clippy
|
||||
|
||||
tests:
|
||||
name: Run tests
|
||||
@@ -75,6 +75,9 @@ jobs:
|
||||
with:
|
||||
clean: false
|
||||
|
||||
- name: Set up default .cargo/config.toml
|
||||
run: cp ./.cargo/collab-config.toml ./.cargo/config.toml
|
||||
|
||||
- name: Build docker image
|
||||
run: docker build . --build-arg GITHUB_SHA=$GITHUB_SHA --tag registry.digitalocean.com/zed/collab:$GITHUB_SHA
|
||||
|
||||
|
||||
6
.github/workflows/release_nightly.yml
vendored
@@ -15,6 +15,7 @@ env:
|
||||
|
||||
jobs:
|
||||
style:
|
||||
timeout-minutes: 60
|
||||
name: Check formatting and Clippy lints
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on:
|
||||
@@ -31,8 +32,9 @@ jobs:
|
||||
uses: ./.github/actions/check_style
|
||||
|
||||
- name: Run clippy
|
||||
run: cargo xtask clippy
|
||||
run: ./script/clippy
|
||||
tests:
|
||||
timeout-minutes: 60
|
||||
name: Run tests
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on:
|
||||
@@ -49,6 +51,7 @@ jobs:
|
||||
uses: ./.github/actions/run_tests
|
||||
|
||||
bundle-mac:
|
||||
timeout-minutes: 60
|
||||
name: Create a macOS bundle
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on:
|
||||
@@ -91,6 +94,7 @@ jobs:
|
||||
run: script/upload-nightly macos
|
||||
|
||||
bundle-deb:
|
||||
timeout-minutes: 60
|
||||
name: Create a Linux *.tar.gz bundle
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on:
|
||||
|
||||
1
.gitignore
vendored
@@ -28,3 +28,4 @@ DerivedData/
|
||||
.vscode
|
||||
.wrangler
|
||||
.flatpak-builder
|
||||
.aider*
|
||||
|
||||
17
.mailmap
@@ -9,12 +9,18 @@
|
||||
# Keep these entries sorted alphabetically.
|
||||
# In Zed: `editor: sort lines case sensitive`
|
||||
|
||||
Alex Viscreanu <alexviscreanu@gmail.com>
|
||||
Alex Viscreanu <alexviscreanu@gmail.com> <alexandru.viscreanu@kiwi.com>
|
||||
Antonio Scandurra <me@as-cii.com>
|
||||
Antonio Scandurra <me@as-cii.com> <antonio@zed.dev>
|
||||
Bennet Bo Fenner <bennet@zed.dev>
|
||||
Bennet Bo Fenner <bennet@zed.dev> <53836821+bennetbo@users.noreply.github.com>
|
||||
Bennet Bo Fenner <bennet@zed.dev> <bennetbo@gmx.de>
|
||||
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>
|
||||
Evren Sen <146845123+evrsen@users.noreply.github.com>
|
||||
Fernando Tagawa <tagawafernando@gmail.com>
|
||||
Fernando Tagawa <tagawafernando@gmail.com> <fernando.tagawa.gamail.com@gmail.com>
|
||||
Greg Morenz <greg-morenz@droid.cafe>
|
||||
@@ -48,12 +54,23 @@ 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>
|
||||
Peter Tripp <peter@zed.dev>
|
||||
Peter Tripp <peter@zed.dev> <petertripp@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>
|
||||
Rashid Almheiri <r.muhairi@pm.me>
|
||||
Rashid Almheiri <r.muhairi@pm.me> <69181766+huwaireb@users.noreply.github.com>
|
||||
Richard Feldman <oss@rtfeldman.com>
|
||||
Richard Feldman <oss@rtfeldman.com> <richard@zed.dev>
|
||||
Robert Clover <git@clo4.net>
|
||||
Robert Clover <git@clo4.net> <robert@clover.gdn>
|
||||
Sergey Onufrienko <sergey@onufrienko.com>
|
||||
Thorsten Ball <thorsten@zed.dev>
|
||||
Thorsten Ball <thorsten@zed.dev> <me@thorstenball.com>
|
||||
Thorsten Ball <thorsten@zed.dev> <mrnugget@gmail.com>
|
||||
Vitaly Slobodin <vitaliy.slobodin@gmail.com>
|
||||
Vitaly Slobodin <vitaliy.slobodin@gmail.com> <vitaly_slobodin@fastmail.com>
|
||||
WindSoilder <WindSoilder@outlook.com>
|
||||
张小白 <364772080@qq.com>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[
|
||||
{
|
||||
"label": "clippy",
|
||||
"command": "cargo",
|
||||
"args": ["xtask", "clippy"]
|
||||
"command": "./script/clippy",
|
||||
"args": []
|
||||
}
|
||||
]
|
||||
|
||||
692
Cargo.lock
generated
63
Cargo.toml
@@ -41,6 +41,7 @@ members = [
|
||||
"crates/gpui",
|
||||
"crates/gpui_macros",
|
||||
"crates/headless",
|
||||
"crates/html_to_markdown",
|
||||
"crates/http",
|
||||
"crates/image_viewer",
|
||||
"crates/inline_completion_button",
|
||||
@@ -52,31 +53,38 @@ members = [
|
||||
"crates/languages",
|
||||
"crates/live_kit_client",
|
||||
"crates/live_kit_server",
|
||||
"crates/language_model",
|
||||
"crates/lsp",
|
||||
"crates/markdown",
|
||||
"crates/markdown_preview",
|
||||
"crates/media",
|
||||
"crates/menu",
|
||||
"crates/miner",
|
||||
"crates/multi_buffer",
|
||||
"crates/node_runtime",
|
||||
"crates/notifications",
|
||||
"crates/ollama",
|
||||
"crates/open_ai",
|
||||
"crates/outline",
|
||||
"crates/outline_panel",
|
||||
"crates/paths",
|
||||
"crates/picker",
|
||||
"crates/prettier",
|
||||
"crates/project",
|
||||
"crates/project_panel",
|
||||
"crates/project_symbols",
|
||||
"crates/proto",
|
||||
"crates/quick_action_bar",
|
||||
"crates/recent_projects",
|
||||
"crates/refineable",
|
||||
"crates/refineable/derive_refineable",
|
||||
"crates/release_channel",
|
||||
"crates/dev_server_projects",
|
||||
"crates/repl",
|
||||
"crates/rich_text",
|
||||
"crates/rope",
|
||||
"crates/rpc",
|
||||
"crates/rustdoc_to_markdown",
|
||||
"crates/rustdoc",
|
||||
"crates/task",
|
||||
"crates/tasks_ui",
|
||||
"crates/search",
|
||||
@@ -130,6 +138,7 @@ members = [
|
||||
"extensions/prisma",
|
||||
"extensions/purescript",
|
||||
"extensions/ruby",
|
||||
"extensions/snippets",
|
||||
"extensions/svelte",
|
||||
"extensions/terraform",
|
||||
"extensions/toml",
|
||||
@@ -150,6 +159,7 @@ assets = { path = "crates/assets" }
|
||||
assistant = { path = "crates/assistant" }
|
||||
assistant_slash_command = { path = "crates/assistant_slash_command" }
|
||||
assistant_tooling = { path = "crates/assistant_tooling" }
|
||||
async-watch = "0.3.1"
|
||||
audio = { path = "crates/audio" }
|
||||
auto_update = { path = "crates/auto_update" }
|
||||
base64 = "0.13"
|
||||
@@ -162,10 +172,10 @@ clock = { path = "crates/clock" }
|
||||
collab = { path = "crates/collab" }
|
||||
collab_ui = { path = "crates/collab_ui" }
|
||||
collections = { path = "crates/collections" }
|
||||
color = { path = "crates/color" }
|
||||
command_palette = { path = "crates/command_palette" }
|
||||
command_palette_hooks = { path = "crates/command_palette_hooks" }
|
||||
copilot = { path = "crates/copilot" }
|
||||
dashmap = "5.5.3"
|
||||
db = { path = "crates/db" }
|
||||
diagnostics = { path = "crates/diagnostics" }
|
||||
editor = { path = "crates/editor" }
|
||||
@@ -185,12 +195,14 @@ google_ai = { path = "crates/google_ai" }
|
||||
gpui = { path = "crates/gpui" }
|
||||
gpui_macros = { path = "crates/gpui_macros" }
|
||||
headless = { path = "crates/headless" }
|
||||
html_to_markdown = { path = "crates/html_to_markdown" }
|
||||
http = { path = "crates/http" }
|
||||
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_model = { path = "crates/language_model" }
|
||||
language_selector = { path = "crates/language_selector" }
|
||||
language_tools = { path = "crates/language_tools" }
|
||||
languages = { path = "crates/languages" }
|
||||
@@ -204,13 +216,17 @@ menu = { path = "crates/menu" }
|
||||
multi_buffer = { path = "crates/multi_buffer" }
|
||||
node_runtime = { path = "crates/node_runtime" }
|
||||
notifications = { path = "crates/notifications" }
|
||||
ollama = { path = "crates/ollama" }
|
||||
open_ai = { path = "crates/open_ai" }
|
||||
outline = { path = "crates/outline" }
|
||||
outline_panel = { path = "crates/outline_panel" }
|
||||
paths = { path = "crates/paths" }
|
||||
picker = { path = "crates/picker" }
|
||||
plugin = { path = "crates/plugin" }
|
||||
plugin_macros = { path = "crates/plugin_macros" }
|
||||
prettier = { path = "crates/prettier" }
|
||||
project = { path = "crates/project" }
|
||||
proto = { path = "crates/proto" }
|
||||
worktree = { path = "crates/worktree" }
|
||||
project_panel = { path = "crates/project_panel" }
|
||||
project_symbols = { path = "crates/project_symbols" }
|
||||
@@ -218,10 +234,11 @@ quick_action_bar = { path = "crates/quick_action_bar" }
|
||||
recent_projects = { path = "crates/recent_projects" }
|
||||
release_channel = { path = "crates/release_channel" }
|
||||
dev_server_projects = { path = "crates/dev_server_projects" }
|
||||
repl = { path = "crates/repl" }
|
||||
rich_text = { path = "crates/rich_text" }
|
||||
rope = { path = "crates/rope" }
|
||||
rpc = { path = "crates/rpc" }
|
||||
rustdoc_to_markdown = { path = "crates/rustdoc_to_markdown" }
|
||||
rustdoc = { path = "crates/rustdoc" }
|
||||
task = { path = "crates/task" }
|
||||
tasks_ui = { path = "crates/tasks_ui" }
|
||||
search = { path = "crates/search" }
|
||||
@@ -255,29 +272,32 @@ workspace = { path = "crates/workspace" }
|
||||
zed = { path = "crates/zed" }
|
||||
zed_actions = { path = "crates/zed_actions" }
|
||||
|
||||
alacritty_terminal = "0.23"
|
||||
anyhow = "1.0.57"
|
||||
any_vec = "0.13"
|
||||
ashpd = "0.8.0"
|
||||
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
|
||||
async-dispatcher = { version = "0.1"}
|
||||
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 = "bdaf8c534fbbc9fbca71d1cf272f45640b3a068d" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "bdaf8c534fbbc9fbca71d1cf272f45640b3a068d" }
|
||||
blade-util = { git = "https://github.com/kvark/blade", rev = "bdaf8c534fbbc9fbca71d1cf272f45640b3a068d" }
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7" }
|
||||
blade-util = { git = "https://github.com/kvark/blade", rev = "21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7" }
|
||||
cap-std = "3.0"
|
||||
cargo_toml = "0.20"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
clickhouse = { version = "0.11.6" }
|
||||
cocoa = "0.25"
|
||||
ctor = "0.2.6"
|
||||
signal-hook = "0.3.17"
|
||||
core-foundation = { version = "0.9.3" }
|
||||
core-foundation-sys = "0.8.6"
|
||||
derive_more = "0.99.17"
|
||||
dirs = "4.0"
|
||||
emojis = "0.6.1"
|
||||
env_logger = "0.9"
|
||||
exec = "0.3.1"
|
||||
@@ -285,18 +305,17 @@ fork = "0.1.23"
|
||||
futures = "0.3"
|
||||
futures-batch = "0.6.1"
|
||||
futures-lite = "1.13"
|
||||
git2 = { version = "0.18", default-features = false }
|
||||
git2 = { version = "0.19", default-features = false }
|
||||
globset = "0.4"
|
||||
heed = { version = "0.20.1", features = ["read-txn-no-tls"] }
|
||||
hex = "0.4.3"
|
||||
html5ever = "0.27.0"
|
||||
ignore = "0.4.22"
|
||||
image = "0.23"
|
||||
indexmap = { version = "1.6.2", features = ["serde"] }
|
||||
indoc = "1"
|
||||
# We explicitly disable http2 support in isahc.
|
||||
isahc = { version = "1.7.2", default-features = false, features = [
|
||||
"static-curl",
|
||||
"text-decoding",
|
||||
] }
|
||||
isahc = { version = "1.7.2", default-features = false, features = [ "text-decoding" ] }
|
||||
itertools = "0.11.0"
|
||||
lazy_static = "1.4.0"
|
||||
libc = "0.2"
|
||||
@@ -305,6 +324,7 @@ log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
markup5ever_rcdom = "0.3.0"
|
||||
nanoid = "0.4"
|
||||
nix = "0.28"
|
||||
num-format = "0.4.4"
|
||||
once_cell = "1.19.0"
|
||||
ordered-float = "2.1.1"
|
||||
palette = { version = "0.7.5", default-features = false, features = ["std"] }
|
||||
@@ -321,6 +341,7 @@ rand = "0.8.5"
|
||||
refineable = { path = "./crates/refineable" }
|
||||
regex = "1.5"
|
||||
repair_json = "0.1.0"
|
||||
runtimelib = { version="0.12", default-features = false, features = ["async-dispatcher-runtime"] }
|
||||
rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
|
||||
rust-embed = { version = "8.4", features = ["include-exclude"] }
|
||||
schemars = "0.8"
|
||||
@@ -336,6 +357,8 @@ serde_repr = "0.1"
|
||||
sha2 = "0.10"
|
||||
shellexpand = "2.1.0"
|
||||
shlex = "1.3.0"
|
||||
signal-hook = "0.3.17"
|
||||
similar = "1.3"
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
smol = "1.2"
|
||||
strum = { version = "0.25.0", features = ["derive"] }
|
||||
@@ -397,12 +420,13 @@ wit-component = "0.201"
|
||||
sys-locale = "0.3.1"
|
||||
|
||||
[workspace.dependencies.windows]
|
||||
version = "0.56.0"
|
||||
version = "0.57"
|
||||
features = [
|
||||
"implement",
|
||||
"Foundation_Numerics",
|
||||
"System",
|
||||
"System_Threading",
|
||||
"UI_ViewManagement",
|
||||
"Wdk_System_SystemServices",
|
||||
"Win32_Globalization",
|
||||
"Win32_Graphics_Direct2D",
|
||||
@@ -438,11 +462,12 @@ features = [
|
||||
[patch.crates-io]
|
||||
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "7b4894ba2ae81b988846676f54c0988d4027ef4f" }
|
||||
# Workaround for a broken nightly build of gpui: See #7644 and revisit once 0.5.3 is released.
|
||||
pathfinder_simd = { git = "https://github.com/servo/pathfinder.git", rev = "30419d07660dc11a21e42ef4a7fa329600cff152" }
|
||||
pathfinder_simd = { git = "https://github.com/servo/pathfinder.git", rev = "4968e819c0d9b015437ffc694511e175801a17c7" }
|
||||
|
||||
[profile.dev]
|
||||
split-debuginfo = "unpacked"
|
||||
debug = "limited"
|
||||
codegen-units = 16
|
||||
|
||||
[profile.dev.package]
|
||||
taffy = { opt-level = 3 }
|
||||
@@ -461,6 +486,11 @@ codegen-units = 1
|
||||
[profile.release.package]
|
||||
zed = { codegen-units = 16 }
|
||||
|
||||
[profile.release-fast]
|
||||
inherits = "release"
|
||||
lto = false
|
||||
codegen-units = 16
|
||||
|
||||
[workspace.lints.clippy]
|
||||
dbg_macro = "deny"
|
||||
todo = "deny"
|
||||
@@ -494,10 +524,5 @@ non_canonical_partial_ord_impl = "allow"
|
||||
reversed_empty_ranges = "allow"
|
||||
type_complexity = "allow"
|
||||
|
||||
[workspace.lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = [
|
||||
'cfg(gles)', # used in gpui
|
||||
] }
|
||||
|
||||
[workspace.metadata.cargo-machete]
|
||||
ignored = ["bindgen", "cbindgen", "prost_build", "serde"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax = docker/dockerfile:1.2
|
||||
|
||||
FROM rust:1.78-bookworm as builder
|
||||
FROM rust:1.79-bookworm as builder
|
||||
WORKDIR app
|
||||
COPY . .
|
||||
|
||||
|
||||
6
assets/icons/context.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.6667 2H3.33333C2.59695 2 2 2.59695 2 3.33333V12.6667C2 13.403 2.59695 14 3.33333 14H12.6667C13.403 14 14 13.403 14 12.6667V3.33333C14 2.59695 13.403 2 12.6667 2Z" stroke="#888888" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9 5H5" stroke="#888888" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.5 8H5" stroke="#888888" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9 10.9502H5" stroke="#888888" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 683 B |
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"stems": {
|
||||
"Dockerfile": "docker",
|
||||
"Podfile": "ruby",
|
||||
"Procfile": "heroku",
|
||||
"Dockerfile": "docker"
|
||||
"Procfile": "heroku"
|
||||
},
|
||||
"suffixes": {
|
||||
"astro": "astro",
|
||||
"Emakefile": "erlang",
|
||||
"aac": "audio",
|
||||
"accdb": "storage",
|
||||
"app.src": "erlang",
|
||||
"astro": "astro",
|
||||
"avi": "video",
|
||||
"avif": "image",
|
||||
"bak": "backup",
|
||||
@@ -22,12 +22,12 @@
|
||||
"c": "c",
|
||||
"cc": "cpp",
|
||||
"cjs": "javascript",
|
||||
"coffee": "coffeescript",
|
||||
"conf": "settings",
|
||||
"cpp": "cpp",
|
||||
"css": "css",
|
||||
"csv": "storage",
|
||||
"cts": "typescript",
|
||||
"coffee": "coffeescript",
|
||||
"dart": "dart",
|
||||
"dat": "storage",
|
||||
"db": "storage",
|
||||
@@ -61,12 +61,12 @@
|
||||
"graphql": "graphql",
|
||||
"graphqls": "graphql",
|
||||
"h": "c",
|
||||
"hpp": "cpp",
|
||||
"handlebars": "code",
|
||||
"hbs": "template",
|
||||
"heex": "elixir",
|
||||
"heif": "image",
|
||||
"heic": "image",
|
||||
"heif": "image",
|
||||
"hpp": "cpp",
|
||||
"hrl": "erlang",
|
||||
"hs": "haskell",
|
||||
"htm": "template",
|
||||
@@ -74,6 +74,7 @@
|
||||
"ib": "storage",
|
||||
"ico": "image",
|
||||
"ini": "settings",
|
||||
"inl": "cpp",
|
||||
"j2k": "image",
|
||||
"java": "java",
|
||||
"jfif": "image",
|
||||
@@ -81,9 +82,9 @@
|
||||
"jpeg": "image",
|
||||
"jpg": "image",
|
||||
"js": "javascript",
|
||||
"jsx": "react",
|
||||
"json": "storage",
|
||||
"jsonc": "storage",
|
||||
"jsx": "react",
|
||||
"jxl": "image",
|
||||
"kt": "kotlin",
|
||||
"ldf": "storage",
|
||||
@@ -98,9 +99,9 @@
|
||||
"mdf": "storage",
|
||||
"mdx": "document",
|
||||
"metadata": "code",
|
||||
"mkv": "video",
|
||||
"mjs": "javascript",
|
||||
"mka": "audio",
|
||||
"mkv": "video",
|
||||
"ml": "ocaml",
|
||||
"mli": "ocaml",
|
||||
"mov": "video",
|
||||
@@ -109,8 +110,8 @@
|
||||
"mts": "typescript",
|
||||
"myd": "storage",
|
||||
"myi": "storage",
|
||||
"nu": "terminal",
|
||||
"nim": "nim",
|
||||
"nu": "terminal",
|
||||
"odp": "document",
|
||||
"ods": "document",
|
||||
"odt": "document",
|
||||
@@ -132,33 +133,33 @@
|
||||
"psd": "image",
|
||||
"py": "python",
|
||||
"qoi": "image",
|
||||
"r": "r",
|
||||
"rb": "ruby",
|
||||
"rebar.config": "erlang",
|
||||
"rkt": "code",
|
||||
"rs": "rust",
|
||||
"r": "r",
|
||||
"rtf": "document",
|
||||
"sav": "storage",
|
||||
"sc": "scala",
|
||||
"scala": "scala",
|
||||
"scm": "code",
|
||||
"sdf": "storage",
|
||||
"sh": "terminal",
|
||||
"sql": "storage",
|
||||
"sqlite": "storage",
|
||||
"svelte": "template",
|
||||
"svg": "image",
|
||||
"sc": "scala",
|
||||
"scala": "scala",
|
||||
"sql": "storage",
|
||||
"swift": "swift",
|
||||
"tcl": "tcl",
|
||||
"tf": "terraform",
|
||||
"tfvars": "terraform",
|
||||
"tiff": "image",
|
||||
"toml": "toml",
|
||||
"ts": "typescript",
|
||||
"tsv": "storage",
|
||||
"ttf": "font",
|
||||
"tsx": "react",
|
||||
"ttf": "font",
|
||||
"txt": "document",
|
||||
"tcl": "tcl",
|
||||
"vue": "vue",
|
||||
"wav": "audio",
|
||||
"webm": "video",
|
||||
@@ -190,27 +191,30 @@
|
||||
"audio": {
|
||||
"icon": "icons/file_icons/audio.svg"
|
||||
},
|
||||
"bun": {
|
||||
"icon": "icons/file_icons/bun.svg"
|
||||
},
|
||||
"c": {
|
||||
"icon": "icons/file_icons/c.svg"
|
||||
},
|
||||
"code": {
|
||||
"icon": "icons/file_icons/code.svg"
|
||||
},
|
||||
"coffeescript": {
|
||||
"icon": "icons/file_icons/coffeescript.svg"
|
||||
},
|
||||
"collapsed_chevron": {
|
||||
"icon": "icons/file_icons/chevron_right.svg"
|
||||
},
|
||||
"collapsed_folder": {
|
||||
"icon": "icons/file_icons/folder.svg"
|
||||
},
|
||||
"c": {
|
||||
"icon": "icons/file_icons/c.svg"
|
||||
},
|
||||
"cpp": {
|
||||
"icon": "icons/file_icons/cpp.svg"
|
||||
},
|
||||
"css": {
|
||||
"icon": "icons/file_icons/css.svg"
|
||||
},
|
||||
"coffeescript": {
|
||||
"icon": "icons/file_icons/coffeescript.svg"
|
||||
},
|
||||
"dart": {
|
||||
"icon": "icons/file_icons/dart.svg"
|
||||
},
|
||||
@@ -247,18 +251,18 @@
|
||||
"fsharp": {
|
||||
"icon": "icons/file_icons/fsharp.svg"
|
||||
},
|
||||
"haskell": {
|
||||
"icon": "icons/file_icons/haskell.svg"
|
||||
},
|
||||
"heroku": {
|
||||
"icon": "icons/file_icons/heroku.svg"
|
||||
},
|
||||
"go": {
|
||||
"icon": "icons/file_icons/go.svg"
|
||||
},
|
||||
"graphql": {
|
||||
"icon": "icons/file_icons/graphql.svg"
|
||||
},
|
||||
"haskell": {
|
||||
"icon": "icons/file_icons/haskell.svg"
|
||||
},
|
||||
"heroku": {
|
||||
"icon": "icons/file_icons/heroku.svg"
|
||||
},
|
||||
"image": {
|
||||
"icon": "icons/file_icons/image.svg"
|
||||
},
|
||||
@@ -274,21 +278,18 @@
|
||||
"lock": {
|
||||
"icon": "icons/file_icons/lock.svg"
|
||||
},
|
||||
"bun": {
|
||||
"icon": "icons/file_icons/bun.svg"
|
||||
},
|
||||
"log": {
|
||||
"icon": "icons/file_icons/info.svg"
|
||||
},
|
||||
"lua": {
|
||||
"icon": "icons/file_icons/lua.svg"
|
||||
},
|
||||
"ocaml": {
|
||||
"icon": "icons/file_icons/ocaml.svg"
|
||||
},
|
||||
"nim": {
|
||||
"icon": "icons/file_icons/nim.svg"
|
||||
},
|
||||
"ocaml": {
|
||||
"icon": "icons/file_icons/ocaml.svg"
|
||||
},
|
||||
"phoenix": {
|
||||
"icon": "icons/file_icons/phoenix.svg"
|
||||
},
|
||||
@@ -316,36 +317,36 @@
|
||||
"rust": {
|
||||
"icon": "icons/file_icons/rust.svg"
|
||||
},
|
||||
"scala": {
|
||||
"icon": "icons/file_icons/scala.svg"
|
||||
},
|
||||
"settings": {
|
||||
"icon": "icons/file_icons/settings.svg"
|
||||
},
|
||||
"storage": {
|
||||
"icon": "icons/file_icons/database.svg"
|
||||
},
|
||||
"scala": {
|
||||
"icon": "icons/file_icons/scala.svg"
|
||||
},
|
||||
"swift": {
|
||||
"icon": "icons/file_icons/swift.svg"
|
||||
},
|
||||
"tcl": {
|
||||
"icon": "icons/file_icons/tcl.svg"
|
||||
},
|
||||
"template": {
|
||||
"icon": "icons/file_icons/html.svg"
|
||||
},
|
||||
"terraform": {
|
||||
"icon": "icons/file_icons/terraform.svg"
|
||||
},
|
||||
"terminal": {
|
||||
"icon": "icons/file_icons/terminal.svg"
|
||||
},
|
||||
"terraform": {
|
||||
"icon": "icons/file_icons/terraform.svg"
|
||||
},
|
||||
"toml": {
|
||||
"icon": "icons/file_icons/toml.svg"
|
||||
},
|
||||
"typescript": {
|
||||
"icon": "icons/file_icons/typescript.svg"
|
||||
},
|
||||
"tcl": {
|
||||
"icon": "icons/file_icons/tcl.svg"
|
||||
},
|
||||
"vcs": {
|
||||
"icon": "icons/file_icons/git.svg"
|
||||
},
|
||||
|
||||
1
assets/icons/list_tree.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-list-tree"><path d="M21 12h-8"/><path d="M21 6H8"/><path d="M21 18h-8"/><path d="M3 6v4c0 1.1.9 2 2 2h3"/><path d="M3 10v6c0 1.1.9 2 2 2h3"/></svg>
|
||||
|
After Width: | Height: | Size: 349 B |
1
assets/icons/rotate_ccw.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rotate-ccw"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
|
||||
|
After Width: | Height: | Size: 302 B |
1
assets/icons/rotate_cw.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rotate-cw"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>
|
||||
|
After Width: | Height: | Size: 303 B |
1
assets/icons/search_selection.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-text-quote"><path d="M17 6H3"/><path d="M21 12H8"/><path d="M21 18H8"/><path d="M3 12v6"/></svg>
|
||||
|
After Width: | Height: | Size: 299 B |
1
assets/icons/sparkle.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-sparkle"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/></svg>
|
||||
|
After Width: | Height: | Size: 481 B |
3
assets/icons/sparkle_filled.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.937 15.5C9.84772 15.1539 9.66734 14.8381 9.41462 14.5854C9.1619 14.3327 8.84607 14.1523 8.5 14.063L2.365 12.481C2.26033 12.4513 2.16821 12.3883 2.10261 12.3014C2.03702 12.2146 2.00153 12.1088 2.00153 12C2.00153 11.8912 2.03702 11.7854 2.10261 11.6986C2.16821 11.6118 2.26033 11.5487 2.365 11.519L8.5 9.93601C8.84595 9.84681 9.16169 9.66658 9.4144 9.41404C9.66711 9.16151 9.84757 8.84589 9.937 8.50001L11.519 2.36501C11.5484 2.25992 11.6114 2.16735 11.6983 2.1014C11.7853 2.03545 11.8914 1.99976 12.0005 1.99976C12.1096 1.99976 12.2157 2.03545 12.3027 2.1014C12.3896 2.16735 12.4526 2.25992 12.482 2.36501L14.063 8.50001C14.1523 8.84608 14.3327 9.1619 14.5854 9.41462C14.8381 9.66734 15.1539 9.84773 15.5 9.93701L21.635 11.518C21.7405 11.5471 21.8335 11.61 21.8998 11.6971C21.9661 11.7841 22.0021 11.8906 22.0021 12C22.0021 12.1094 21.9661 12.2159 21.8998 12.3029C21.8335 12.39 21.7405 12.4529 21.635 12.482L15.5 14.063C15.1539 14.1523 14.8381 14.3327 14.5854 14.5854C14.3327 14.8381 14.1523 15.1539 14.063 15.5L12.481 21.635C12.4516 21.7401 12.3886 21.8327 12.3017 21.8986C12.2147 21.9646 12.1086 22.0003 11.9995 22.0003C11.8904 22.0003 11.7843 21.9646 11.6973 21.8986C11.6104 21.8327 11.5474 21.7401 11.518 21.635L9.937 15.5Z" fill="black" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
1
assets/icons/star.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.97942 1.25171L6.9585 1.30199L5.58662 4.60039C5.54342 4.70426 5.44573 4.77523 5.3336 4.78422L1.7727 5.0697L1.71841 5.07405L1.38687 5.10063L1.08608 5.12475C0.820085 5.14607 0.712228 5.47802 0.914889 5.65162L1.14406 5.84793L1.39666 6.06431L1.43802 6.09974L4.15105 8.42374C4.23648 8.49692 4.2738 8.61176 4.24769 8.72118L3.41882 12.196L3.40618 12.249L3.32901 12.5725L3.25899 12.866C3.19708 13.1256 3.47945 13.3308 3.70718 13.1917L3.9647 13.0344L4.24854 12.861L4.29502 12.8326L7.34365 10.9705C7.43965 10.9119 7.5604 10.9119 7.6564 10.9705L10.705 12.8326L10.7515 12.861L11.0354 13.0344L11.2929 13.1917C11.5206 13.3308 11.803 13.1256 11.7411 12.866L11.671 12.5725L11.5939 12.249L11.5812 12.196L10.7524 8.72118C10.7263 8.61176 10.7636 8.49692 10.849 8.42374L13.562 6.09974L13.6034 6.06431L13.856 5.84793L14.0852 5.65162C14.2878 5.47802 14.18 5.14607 13.914 5.12475L13.6132 5.10063L13.2816 5.07405L13.2274 5.0697L9.66645 4.78422C9.55432 4.77523 9.45663 4.70426 9.41343 4.60039L8.04155 1.30199L8.02064 1.25171L7.89291 0.944609L7.77702 0.665992C7.67454 0.419604 7.32551 0.419604 7.22303 0.665992L7.10715 0.944609L6.97942 1.25171ZM7.50003 2.60397L6.50994 4.98442C6.32273 5.43453 5.89944 5.74207 5.41351 5.78103L2.84361 5.98705L4.8016 7.66428C5.17183 7.98142 5.33351 8.47903 5.2204 8.95321L4.62221 11.461L6.8224 10.1171C7.23842 9.86302 7.76164 9.86302 8.17766 10.1171L10.3778 11.461L9.77965 8.95321C9.66654 8.47903 9.82822 7.98142 10.1984 7.66428L12.1564 5.98705L9.58654 5.78103C9.10061 5.74207 8.67732 5.43453 8.49011 4.98442L7.50003 2.60397Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
1
assets/icons/star_filled.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.22303 0.665992C7.32551 0.419604 7.67454 0.419604 7.77702 0.665992L9.41343 4.60039C9.45663 4.70426 9.55432 4.77523 9.66645 4.78422L13.914 5.12475C14.18 5.14607 14.2878 5.47802 14.0852 5.65162L10.849 8.42374C10.7636 8.49692 10.7263 8.61176 10.7524 8.72118L11.7411 12.866C11.803 13.1256 11.5206 13.3308 11.2929 13.1917L7.6564 10.9705C7.5604 10.9119 7.43965 10.9119 7.34365 10.9705L3.70718 13.1917C3.47945 13.3308 3.19708 13.1256 3.25899 12.866L4.24769 8.72118C4.2738 8.61176 4.23648 8.49692 4.15105 8.42374L0.914889 5.65162C0.712228 5.47802 0.820086 5.14607 1.08608 5.12475L5.3336 4.78422C5.44573 4.77523 5.54342 4.70426 5.58662 4.60039L7.22303 0.665992Z" fill="currentColor"></path></svg>
|
||||
|
After Width: | Height: | Size: 794 B |
3
assets/icons/stop.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.88889 1H2.11111C1.49746 1 1 1.49746 1 2.11111V9.88889C1 10.5025 1.49746 11 2.11111 11H9.88889C10.5025 11 11 10.5025 11 9.88889V2.11111C11 1.49746 10.5025 1 9.88889 1Z" stroke="#C56757" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 369 B |
1
assets/icons/text-cursor.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-text-cursor"><path d="M17 22h-1a4 4 0 0 1-4-4V6a4 4 0 0 1 4-4h1"/><path d="M7 22h1a4 4 0 0 0 4-4v-1"/><path d="M7 2h1a4 4 0 0 1 4 4v1"/></svg>
|
||||
|
After Width: | Height: | Size: 345 B |
5
assets/icons/zed_assistant_filled.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 1.75L5.88467 5.14092C5.82759 5.31446 5.73055 5.47218 5.60136 5.60136C5.47218 5.73055 5.31446 5.82759 5.14092 5.88467L1.75 7L5.14092 8.11533C5.31446 8.17241 5.47218 8.26945 5.60136 8.39864C5.73055 8.52782 5.82759 8.68554 5.88467 8.85908L7 12.25L8.11533 8.85908C8.17241 8.68554 8.26945 8.52782 8.39864 8.39864C8.52782 8.26945 8.68554 8.17241 8.85908 8.11533L12.25 7L8.85908 5.88467C8.68554 5.82759 8.52782 5.73055 8.39864 5.60136C8.26945 5.47218 8.17241 5.31446 8.11533 5.14092L7 1.75Z" fill="black" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2.91667 1.75V4.08333M1.75 2.91667H4.08333" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11.0833 9.91667V12.25M9.91667 11.0833H12.25" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1017 B |
@@ -25,7 +25,8 @@
|
||||
],
|
||||
"ctrl-shift-down": "editor::AddSelectionBelow",
|
||||
"ctrl-shift-up": "editor::AddSelectionAbove",
|
||||
"cmd-shift-backspace": "editor::DeleteToBeginningOfLine"
|
||||
"cmd-shift-backspace": "editor::DeleteToBeginningOfLine",
|
||||
"ctrl-shift-m": "markdown::OpenPreviewToTheSide"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
[
|
||||
// todo(linux): Review the editor bindings
|
||||
// Standard Linux bindings
|
||||
{
|
||||
"bindings": {
|
||||
@@ -28,7 +27,6 @@
|
||||
"ctrl-0": "zed::ResetBufferFontSize",
|
||||
"ctrl-,": "zed::OpenSettings",
|
||||
"ctrl-q": "zed::Quit",
|
||||
"alt-f9": "zed::Hide",
|
||||
"f11": "zed::ToggleFullScreen"
|
||||
}
|
||||
},
|
||||
@@ -44,40 +42,30 @@
|
||||
"shift-tab": "editor::TabPrev",
|
||||
"ctrl-k": "editor::CutToEndOfLine",
|
||||
"ctrl-t": "editor::Transpose",
|
||||
// "ctrl-backspace": "editor::DeleteToBeginningOfLine",
|
||||
// "ctrl-delete": "editor::DeleteToEndOfLine",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
// "ctrl-w": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd",
|
||||
// "alt-h": "editor::DeleteToPreviousWordStart",
|
||||
// "alt-d": "editor::DeleteToNextWordEnd",
|
||||
"ctrl-x": "editor::Cut",
|
||||
"ctrl-c": "editor::Copy",
|
||||
"ctrl-insert": "editor::Copy",
|
||||
"ctrl-v": "editor::Paste",
|
||||
"ctrl-c": "editor::Copy",
|
||||
"shift-insert": "editor::Paste",
|
||||
"ctrl-v": "editor::Paste",
|
||||
"ctrl-y": "editor::Redo",
|
||||
"ctrl-z": "editor::Undo",
|
||||
"ctrl-shift-z": "editor::Redo",
|
||||
"up": "editor::MoveUp",
|
||||
// "ctrl-up": "editor::MoveToStartOfParagraph", todo(linux) Should be "scroll down by 1 line"
|
||||
"ctrl-up": "editor::LineUp",
|
||||
"ctrl-down": "editor::LineDown",
|
||||
"pageup": "editor::PageUp",
|
||||
// "shift-pageup": "editor::MovePageUp", todo(linux) should be 'select page up'
|
||||
"shift-pageup": "editor::SelectPageUp",
|
||||
"home": "editor::MoveToBeginningOfLine",
|
||||
"down": "editor::MoveDown",
|
||||
// "ctrl-down": "editor::MoveToEndOfParagraph", todo(linux) should be "scroll up by 1 line"
|
||||
"pagedown": "editor::PageDown",
|
||||
// "shift-pagedown": "editor::MovePageDown", todo(linux) should be 'select page down'
|
||||
"shift-pagedown": "editor::SelectPageDown",
|
||||
"end": "editor::MoveToEndOfLine",
|
||||
"left": "editor::MoveLeft",
|
||||
"right": "editor::MoveRight",
|
||||
"ctrl-left": "editor::MoveToPreviousWordStart",
|
||||
// "alt-b": "editor::MoveToPreviousWordStart",
|
||||
"ctrl-right": "editor::MoveToNextWordEnd",
|
||||
// "alt-f": "editor::MoveToNextWordEnd",
|
||||
// "cmd-left": "editor::MoveToBeginningOfLine",
|
||||
// "ctrl-a": "editor::MoveToBeginningOfLine",
|
||||
// "cmd-right": "editor::MoveToEndOfLine",
|
||||
// "ctrl-e": "editor::MoveToEndOfLine",
|
||||
"ctrl-home": "editor::MoveToBeginning",
|
||||
"ctrl-end": "editor::MoveToEnd",
|
||||
"shift-up": "editor::SelectUp",
|
||||
@@ -88,8 +76,6 @@
|
||||
"ctrl-shift-right": "editor::SelectToNextWordEnd",
|
||||
"ctrl-shift-up": "editor::AddSelectionAbove",
|
||||
"ctrl-shift-down": "editor::AddSelectionBelow",
|
||||
// "ctrl-shift-up": "editor::SelectToStartOfParagraph",
|
||||
// "ctrl-shift-down": "editor::SelectToEndOfParagraph",
|
||||
"ctrl-shift-home": "editor::SelectToBeginning",
|
||||
"ctrl-shift-end": "editor::SelectToEnd",
|
||||
"ctrl-a": "editor::SelectAll",
|
||||
@@ -201,19 +187,15 @@
|
||||
"context": "AssistantPanel",
|
||||
"bindings": {
|
||||
"ctrl-g": "search::SelectNextMatch",
|
||||
"ctrl-shift-g": "search::SelectPrevMatch"
|
||||
"ctrl-shift-g": "search::SelectPrevMatch",
|
||||
"alt-m": "assistant::ToggleModelSelector"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ConversationEditor > Editor",
|
||||
"context": "PromptLibrary",
|
||||
"bindings": {
|
||||
"ctrl-enter": "assistant::Assist",
|
||||
"ctrl-s": "workspace::Save",
|
||||
"ctrl->": "assistant::QuoteSelection",
|
||||
"shift-enter": "assistant::Split",
|
||||
"ctrl-r": "assistant::CycleMessageRole",
|
||||
"enter": "assistant::ConfirmCommand",
|
||||
"alt-enter": "editor::Newline"
|
||||
"ctrl-n": "prompt_library::NewPrompt",
|
||||
"ctrl-shift-s": "prompt_library::ToggleDefaultPrompt"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -225,7 +207,8 @@
|
||||
"shift-enter": "search::SelectPrevMatch",
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
"ctrl-f": "search::FocusSearch",
|
||||
"ctrl-h": "search::ToggleReplace"
|
||||
"ctrl-h": "search::ToggleReplace",
|
||||
"ctrl-l": "search::ToggleSelection"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -289,6 +272,7 @@
|
||||
"ctrl-alt-g": "search::SelectNextMatch",
|
||||
"ctrl-alt-shift-g": "search::SelectPrevMatch",
|
||||
"ctrl-alt-shift-h": "search::ToggleReplace",
|
||||
"ctrl-alt-shift-l": "search::ToggleSelection",
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
"alt-c": "search::ToggleCaseSensitive",
|
||||
"alt-w": "search::ToggleWholeWord",
|
||||
@@ -298,6 +282,13 @@
|
||||
"ctrl-alt-shift-x": "search::ToggleRegex"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Terminal",
|
||||
"bindings": {
|
||||
"ctrl-w": ["terminal::SendKeystroke", "ctrl-w"],
|
||||
"ctrl-e": ["terminal::SendKeystroke", "ctrl-e"]
|
||||
}
|
||||
},
|
||||
// Bindings from VS Code
|
||||
{
|
||||
"context": "Editor",
|
||||
@@ -422,7 +413,7 @@
|
||||
"alt-8": ["workspace::ActivatePane", 7],
|
||||
"alt-9": ["workspace::ActivatePane", 8],
|
||||
"ctrl-alt-b": "workspace::ToggleLeftDock",
|
||||
"ctrl-b": "workspace::ToggleRightDock",
|
||||
"ctrl-b": "workspace::ToggleLeftDock",
|
||||
"ctrl-j": "workspace::ToggleBottomDock",
|
||||
"ctrl-alt-y": "workspace::CloseAllDocks",
|
||||
"ctrl-shift-f": "pane::DeploySearch",
|
||||
@@ -442,6 +433,7 @@
|
||||
"ctrl-shift-p": "command_palette::Toggle",
|
||||
"ctrl-shift-m": "diagnostics::Deploy",
|
||||
"ctrl-shift-e": "project_panel::ToggleFocus",
|
||||
"ctrl-shift-b": "outline_panel::ToggleFocus",
|
||||
"ctrl-?": "assistant::ToggleFocus",
|
||||
"ctrl-alt-s": "workspace::SaveAll",
|
||||
"ctrl-k m": "language_selector::Toggle",
|
||||
@@ -547,12 +539,37 @@
|
||||
"ctrl-enter": "assistant::InlineAssist"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ContextEditor > Editor",
|
||||
"bindings": {
|
||||
"ctrl-enter": "assistant::Assist",
|
||||
"ctrl-s": "workspace::Save",
|
||||
"ctrl->": "assistant::QuoteSelection",
|
||||
"shift-enter": "assistant::Split",
|
||||
"ctrl-r": "assistant::CycleMessageRole",
|
||||
"enter": "assistant::ConfirmCommand",
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectSearchBar && !in_replace",
|
||||
"bindings": {
|
||||
"ctrl-enter": "project_search::SearchInNew"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "OutlinePanel",
|
||||
"bindings": {
|
||||
"left": "outline_panel::CollapseSelectedEntry",
|
||||
"right": "outline_panel::ExpandSelectedEntry",
|
||||
"ctrl-alt-c": "outline_panel::CopyPath",
|
||||
"alt-ctrl-shift-c": "outline_panel::CopyRelativePath",
|
||||
"alt-ctrl-r": "outline_panel::RevealInFinder",
|
||||
"space": "outline_panel::Open",
|
||||
"shift-down": "menu::SelectNext",
|
||||
"shift-up": "menu::SelectPrev"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel",
|
||||
"bindings": {
|
||||
@@ -574,7 +591,10 @@
|
||||
"ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"alt-ctrl-r": "project_panel::RevealInFinder",
|
||||
"alt-shift-f": "project_panel::NewSearchInDirectory"
|
||||
"alt-shift-f": "project_panel::NewSearchInDirectory",
|
||||
"shift-down": "menu::SelectNext",
|
||||
"shift-up": "menu::SelectPrev",
|
||||
"escape": "menu::Cancel"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -635,12 +655,7 @@
|
||||
"pagedown": ["terminal::SendKeystroke", "pagedown"],
|
||||
"escape": ["terminal::SendKeystroke", "escape"],
|
||||
"enter": ["terminal::SendKeystroke", "enter"],
|
||||
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],
|
||||
|
||||
// Some nice conveniences
|
||||
"ctrl-backspace": ["terminal::SendText", "\u0015"],
|
||||
"ctrl-right": ["terminal::SendText", "\u0005"],
|
||||
"ctrl-left": ["terminal::SendText", "\u0001"]
|
||||
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -176,6 +176,12 @@
|
||||
"replace_enabled": true
|
||||
}
|
||||
],
|
||||
"cmd-alt-l": [
|
||||
"buffer_search::Deploy",
|
||||
{
|
||||
"selection_search_enabled": true
|
||||
}
|
||||
],
|
||||
"cmd-e": [
|
||||
"buffer_search::Deploy",
|
||||
{
|
||||
@@ -214,14 +220,15 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AssistantPanel", // Used in the assistant crate, which we're replacing
|
||||
"context": "AssistantPanel",
|
||||
"bindings": {
|
||||
"cmd-g": "search::SelectNextMatch",
|
||||
"cmd-shift-g": "search::SelectPrevMatch"
|
||||
"cmd-shift-g": "search::SelectPrevMatch",
|
||||
"alt-m": "assistant::ToggleModelSelector"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ConversationEditor > Editor",
|
||||
"context": "ContextEditor > Editor",
|
||||
"bindings": {
|
||||
"cmd-enter": "assistant::Assist",
|
||||
"cmd-s": "workspace::Save",
|
||||
@@ -232,6 +239,14 @@
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "PromptLibrary",
|
||||
"bindings": {
|
||||
"cmd-n": "prompt_library::NewPrompt",
|
||||
"cmd-shift-s": "prompt_library::ToggleDefaultPrompt",
|
||||
"cmd-w": "workspace::CloseWindow"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar",
|
||||
"bindings": {
|
||||
@@ -241,7 +256,8 @@
|
||||
"shift-enter": "search::SelectPrevMatch",
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
"cmd-f": "search::FocusSearch",
|
||||
"cmd-alt-f": "search::ToggleReplace"
|
||||
"cmd-alt-f": "search::ToggleReplace",
|
||||
"cmd-alt-l": "search::ToggleSelection"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -307,6 +323,7 @@
|
||||
"cmd-g": "search::SelectNextMatch",
|
||||
"cmd-shift-g": "search::SelectPrevMatch",
|
||||
"cmd-shift-h": "search::ToggleReplace",
|
||||
"cmd-alt-l": "search::ToggleSelection",
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
"alt-cmd-c": "search::ToggleCaseSensitive",
|
||||
"alt-cmd-w": "search::ToggleWholeWord",
|
||||
@@ -458,6 +475,7 @@
|
||||
"cmd-shift-p": "command_palette::Toggle",
|
||||
"cmd-shift-m": "diagnostics::Deploy",
|
||||
"cmd-shift-e": "project_panel::ToggleFocus",
|
||||
"cmd-shift-b": "outline_panel::ToggleFocus",
|
||||
"cmd-?": "assistant::ToggleFocus",
|
||||
"cmd-alt-s": "workspace::SaveAll",
|
||||
"cmd-k m": "language_selector::Toggle",
|
||||
@@ -567,6 +585,19 @@
|
||||
"cmd-enter": "project_search::SearchInNew"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "OutlinePanel",
|
||||
"bindings": {
|
||||
"left": "outline_panel::CollapseSelectedEntry",
|
||||
"right": "outline_panel::ExpandSelectedEntry",
|
||||
"cmd-alt-c": "outline_panel::CopyPath",
|
||||
"alt-cmd-shift-c": "outline_panel::CopyRelativePath",
|
||||
"alt-cmd-r": "outline_panel::RevealInFinder",
|
||||
"space": "outline_panel::Open",
|
||||
"shift-down": "menu::SelectNext",
|
||||
"shift-up": "menu::SelectPrev"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel",
|
||||
"bindings": {
|
||||
@@ -638,7 +669,7 @@
|
||||
{
|
||||
"context": "Picker",
|
||||
"bindings": {
|
||||
"alt-e": "picker::UseSelectedQuery",
|
||||
"f2": "picker::UseSelectedQuery",
|
||||
"alt-enter": ["picker::ConfirmInput", { "secondary": false }],
|
||||
"cmd-alt-enter": ["picker::ConfirmInput", { "secondary": true }]
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"right": "vim::Right",
|
||||
"space": "vim::Space",
|
||||
"$": "vim::EndOfLine",
|
||||
"end": "vim::EndOfLine",
|
||||
"^": "vim::FirstNonWhitespace",
|
||||
"_": "vim::StartOfLineDownward",
|
||||
"g _": "vim::EndOfLineDownward",
|
||||
@@ -80,6 +81,7 @@
|
||||
"g shift-e": ["vim::PreviousWordEnd", { "ignorePunctuation": true }],
|
||||
|
||||
"/": "vim::Search",
|
||||
"g /": "pane::DeploySearch",
|
||||
"?": [
|
||||
"vim::Search",
|
||||
{
|
||||
@@ -139,7 +141,8 @@
|
||||
"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
|
||||
"0": "vim::StartOfLine",
|
||||
"home": "vim::StartOfLine",
|
||||
"ctrl-f": "vim::PageDown",
|
||||
"pagedown": "vim::PageDown",
|
||||
"ctrl-b": "vim::PageUp",
|
||||
@@ -245,9 +248,10 @@
|
||||
"displayLines": true
|
||||
}
|
||||
],
|
||||
"g v": "vim::RestoreVisualSelection",
|
||||
"g ]": "editor::GoToDiagnostic",
|
||||
"g [": "editor::GoToPrevDiagnostic",
|
||||
"g i": ["workspace::SendKeystrokes", "` ^ i"],
|
||||
"g i": "vim::InsertAtPrevious",
|
||||
"g ,": "vim::ChangeListNewer",
|
||||
"g ;": "vim::ChangeListOlder",
|
||||
"shift-h": "vim::WindowTop",
|
||||
@@ -379,8 +383,12 @@
|
||||
"r": ["vim::PushOperator", "Replace"],
|
||||
"s": "vim::Substitute",
|
||||
"shift-s": "vim::SubstituteLine",
|
||||
"> >": "vim::Indent",
|
||||
"< <": "vim::Outdent",
|
||||
">": ["vim::PushOperator", "Indent"],
|
||||
"<": ["vim::PushOperator", "Outdent"],
|
||||
"g u": ["vim::PushOperator", "Lowercase"],
|
||||
"g shift-u": ["vim::PushOperator", "Uppercase"],
|
||||
"g ~": ["vim::PushOperator", "OppositeCase"],
|
||||
"\"": ["vim::PushOperator", "Register"],
|
||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||
"ctrl-pageup": "pane::ActivatePrevItem",
|
||||
// tree-sitter related commands
|
||||
@@ -395,13 +403,14 @@
|
||||
{
|
||||
"context": "Editor && vim_mode == visual && vim_operator == none && !VimWaiting",
|
||||
"bindings": {
|
||||
"\"": ["vim::PushOperator", "Register"],
|
||||
// tree-sitter related commands
|
||||
"[ x": "editor::SelectLargerSyntaxNode",
|
||||
"] x": "editor::SelectSmallerSyntaxNode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && VimCount",
|
||||
"context": "Editor && VimCount && vim_mode != insert",
|
||||
"bindings": {
|
||||
"0": ["vim::Number", 0]
|
||||
}
|
||||
@@ -430,6 +439,27 @@
|
||||
"d": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == gu",
|
||||
"bindings": {
|
||||
"g u": "vim::CurrentLine",
|
||||
"u": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == gU",
|
||||
"bindings": {
|
||||
"g shift-u": "vim::CurrentLine",
|
||||
"shift-u": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == g~",
|
||||
"bindings": {
|
||||
"g ~": "vim::CurrentLine",
|
||||
"~": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == normal && vim_operator == d",
|
||||
"bindings": {
|
||||
@@ -459,6 +489,18 @@
|
||||
"s": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == >",
|
||||
"bindings": {
|
||||
">": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == <",
|
||||
"bindings": {
|
||||
"<": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && VimObject",
|
||||
"bindings": {
|
||||
@@ -568,15 +610,15 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == normal",
|
||||
"context": "Editor && vim_mode == normal && !VimWaiting",
|
||||
"bindings": {
|
||||
"g c c": "editor::ToggleComments"
|
||||
"g c c": "vim::ToggleComments"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == visual",
|
||||
"bindings": {
|
||||
"g c": "editor::ToggleComments"
|
||||
"g c": "vim::ToggleComments"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -594,8 +636,7 @@
|
||||
"ctrl-u": "editor::DeleteToBeginningOfLine",
|
||||
"ctrl-t": "vim::Indent",
|
||||
"ctrl-d": "vim::Outdent",
|
||||
"ctrl-r \"": "editor::Paste",
|
||||
"ctrl-r +": "editor::Paste"
|
||||
"ctrl-r": ["vim::PushOperator", "Register"]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -604,11 +645,13 @@
|
||||
"escape": "vim::NormalBefore",
|
||||
"ctrl-c": "vim::NormalBefore",
|
||||
"ctrl-[": "vim::NormalBefore",
|
||||
"tab": "vim::Tab",
|
||||
"enter": "vim::Enter",
|
||||
"backspace": "vim::UndoReplace"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && VimWaiting",
|
||||
"context": "Editor && vim_mode != replace && VimWaiting",
|
||||
"bindings": {
|
||||
"tab": "vim::Tab",
|
||||
"enter": "vim::Enter",
|
||||
@@ -616,6 +659,13 @@
|
||||
"ctrl-[": ["vim::SwitchMode", "Normal"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == insert && VimWaiting",
|
||||
"bindings": {
|
||||
"escape": "vim::NormalBefore",
|
||||
"ctrl-[": "vim::NormalBefore"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar && !in_replace",
|
||||
"bindings": {
|
||||
|
||||
@@ -119,10 +119,10 @@
|
||||
// The debounce delay before re-querying the language server for completion
|
||||
// documentation when not included in original completion list.
|
||||
"completion_documentation_secondary_query_debounce": 300,
|
||||
// Whether to show wrap guides in the editor. Setting this to true will
|
||||
// show a guide at the 'preferred_line_length' value if 'soft_wrap' is set to
|
||||
// 'preferred_line_length', and will show any additional guides as specified
|
||||
// by the 'wrap_guides' setting.
|
||||
// Whether to show wrap guides (vertical rulers) in the editor.
|
||||
// Setting this to true will show a guide at the 'preferred_line_length' value
|
||||
// if softwrap is set to 'preferred_line_length', and will show any
|
||||
// additional guides as specified by the 'wrap_guides' setting.
|
||||
"show_wrap_guides": true,
|
||||
// Character counts at which to show wrap guides in the editor.
|
||||
"wrap_guides": [],
|
||||
@@ -146,15 +146,19 @@
|
||||
// opening parenthesis, bracket, brace, single or double quote characters.
|
||||
// For example, when you type (, Zed will add a closing ) at the correct position.
|
||||
"use_autoclose": true,
|
||||
// Whether to automatically surround selected text when typing opening parenthesis,
|
||||
// bracket, brace, single or double quote characters.
|
||||
// For example, when you select text and type (, Zed will surround the text with ().
|
||||
"use_auto_surround": true,
|
||||
// Controls how the editor handles the autoclosed characters.
|
||||
// When set to `false`(default), skipping over and auto-removing of the closing characters
|
||||
// happen only for auto-inserted characters.
|
||||
// Otherwise(when `true`), the closing characters are always skipped over and auto-removed
|
||||
// no matter how they were inserted.
|
||||
"always_treat_brackets_as_autoclosed": false,
|
||||
// Controls whether copilot provides suggestion immediately
|
||||
// or waits for a `copilot::Toggle`
|
||||
"show_copilot_suggestions": true,
|
||||
// Controls whether inline completions are shown immediately (true)
|
||||
// or manually by triggering `editor::ShowInlineCompletion` (false).
|
||||
"show_inline_completions": true,
|
||||
// Whether to show tabs and spaces in the editor.
|
||||
// This setting can take three values:
|
||||
//
|
||||
@@ -164,6 +168,12 @@
|
||||
// "none"
|
||||
// 3. Draw all invisible symbols:
|
||||
// "all"
|
||||
// 4. Draw whitespaces at boundaries only:
|
||||
// "boundaries"
|
||||
// For a whitespace to be on a boundary, any of the following conditions need to be met:
|
||||
// - It is a tab
|
||||
// - It is adjacent to an edge (start or end)
|
||||
// - It is adjacent to a whitespace (left or right)
|
||||
"show_whitespaces": "selection",
|
||||
// Settings related to calls in Zed
|
||||
"calls": {
|
||||
@@ -177,7 +187,9 @@
|
||||
// Whether to show breadcrumbs.
|
||||
"breadcrumbs": true,
|
||||
// Whether to show quick action buttons.
|
||||
"quick_actions": true
|
||||
"quick_actions": true,
|
||||
// Whether to show the Selections menu in the editor toolbar
|
||||
"selections_menu": true
|
||||
},
|
||||
// Scrollbar related settings
|
||||
"scrollbar": {
|
||||
@@ -219,6 +231,8 @@
|
||||
"line_numbers": true,
|
||||
// Whether to show code action buttons in the gutter.
|
||||
"code_actions": true,
|
||||
// Whether to show runnables buttons in the gutter.
|
||||
"runnables": true,
|
||||
// Whether to show fold buttons in the gutter.
|
||||
"folds": true
|
||||
},
|
||||
@@ -227,6 +241,8 @@
|
||||
"enabled": true,
|
||||
/// The width of the indent guides in pixels, between 1 and 10.
|
||||
"line_width": 1,
|
||||
/// The width of the active indent guide in pixels, between 1 and 10.
|
||||
"active_line_width": 1,
|
||||
/// Determines how indent guides are colored.
|
||||
/// This setting can take the following three values:
|
||||
///
|
||||
@@ -241,6 +257,8 @@
|
||||
/// 2. "indent_aware"
|
||||
"background_coloring": "disabled"
|
||||
},
|
||||
// Whether the editor will scroll beyond the last line.
|
||||
"scroll_beyond_last_line": "one_page",
|
||||
// The number of lines to keep above/below the cursor when scrolling.
|
||||
"vertical_scroll_margin": 3,
|
||||
// Scroll sensitivity multiplier. This multiplier is applied
|
||||
@@ -294,7 +312,37 @@
|
||||
"auto_reveal_entries": true,
|
||||
/// Whether to fold directories automatically
|
||||
/// when a directory has only one directory inside.
|
||||
"auto_fold_dirs": false
|
||||
"auto_fold_dirs": false,
|
||||
/// Scrollbar-related settings
|
||||
"scrollbar": {
|
||||
/// When to show the scrollbar in the project panel.
|
||||
///
|
||||
/// Default: always
|
||||
"show": "always"
|
||||
}
|
||||
},
|
||||
"outline_panel": {
|
||||
// Whether to show the outline panel button in the status bar
|
||||
"button": true,
|
||||
// Default width of the outline panel.
|
||||
"default_width": 300,
|
||||
// Where to dock the outline panel. Can be 'left' or 'right'.
|
||||
"dock": "left",
|
||||
// Whether to show file icons in the outline panel.
|
||||
"file_icons": true,
|
||||
// Whether to show folder icons or chevrons for directories in the outline panel.
|
||||
"folder_icons": true,
|
||||
// Whether to show the git status in the outline panel.
|
||||
"git_status": true,
|
||||
// Amount of indentation for nested items.
|
||||
"indent_size": 20,
|
||||
// Whether to reveal it in the outline panel automatically,
|
||||
// when a corresponding outline entry becomes active.
|
||||
// Gitignored entries are never auto revealed.
|
||||
"auto_reveal_entries": true,
|
||||
/// Whether to fold directories automatically
|
||||
/// when a directory has only one directory inside.
|
||||
"auto_fold_dirs": true
|
||||
},
|
||||
"collaboration_panel": {
|
||||
// Whether to show the collaboration panel button in the status bar.
|
||||
@@ -355,6 +403,9 @@
|
||||
"show_call_status_icon": true,
|
||||
// Whether to use language servers to provide code intelligence.
|
||||
"enable_language_server": true,
|
||||
// Whether to perform linked edits of associated ranges, if the language server supports it.
|
||||
// For example, when editing opening <html> tag, the contents of the closing </html> tag will be edited as well.
|
||||
"linked_edits": true,
|
||||
// The list of language servers to use (or disable) for all languages.
|
||||
//
|
||||
// This is typically customized on a per-language basis.
|
||||
@@ -426,16 +477,16 @@
|
||||
// or falling back to formatting via language server:
|
||||
// "formatter": "auto"
|
||||
"formatter": "auto",
|
||||
// How to soft-wrap long lines of text. This setting can take
|
||||
// three values:
|
||||
// How to soft-wrap long lines of text.
|
||||
// Possible values:
|
||||
//
|
||||
// 1. Do not soft wrap.
|
||||
// "soft_wrap": "none",
|
||||
// 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:
|
||||
// 3. Soft wrap lines that overflow the editor.
|
||||
// "soft_wrap": "editor_width",
|
||||
// 4. Soft wrap lines at the preferred line length
|
||||
// 4. Soft wrap lines at the preferred line length.
|
||||
// "soft_wrap": "preferred_line_length",
|
||||
"soft_wrap": "prefer_line",
|
||||
// The column at which to soft-wrap lines, for buffers where soft-wrap
|
||||
@@ -453,7 +504,8 @@
|
||||
// Send anonymized usage data like what languages you're using Zed with.
|
||||
"metrics": true
|
||||
},
|
||||
// Automatically update Zed
|
||||
// Automatically update Zed. This setting may be ignored on Linux if
|
||||
// installed through a package manager.
|
||||
"auto_update": true,
|
||||
// Diagnostics configuration.
|
||||
"diagnostics": {
|
||||
@@ -490,9 +542,8 @@
|
||||
// "delay_ms": 600
|
||||
}
|
||||
},
|
||||
"copilot": {
|
||||
// The set of glob patterns for which copilot should be disabled
|
||||
// in any matching file.
|
||||
"inline_completions": {
|
||||
// A list of globs representing files that inline completions should be disabled for.
|
||||
"disabled_globs": [".env"]
|
||||
},
|
||||
// Settings specific to journaling
|
||||
@@ -672,9 +723,6 @@
|
||||
"Elixir": {
|
||||
"language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
|
||||
},
|
||||
"Gleam": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"Go": {
|
||||
"code_actions_on_format": {
|
||||
"source.organizeImports": true
|
||||
@@ -700,6 +748,7 @@
|
||||
}
|
||||
},
|
||||
"JavaScript": {
|
||||
"language_servers": ["!typescript-language-server", "vtsls", "..."],
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
@@ -709,9 +758,6 @@
|
||||
"allowed": true
|
||||
}
|
||||
},
|
||||
"Make": {
|
||||
"hard_tabs": true
|
||||
},
|
||||
"Markdown": {
|
||||
"format_on_save": "off",
|
||||
"prettier": {
|
||||
@@ -724,9 +770,6 @@
|
||||
"plugins": ["@prettier/plugin-php"]
|
||||
}
|
||||
},
|
||||
"Prisma": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"Ruby": {
|
||||
"language_servers": ["solargraph", "!ruby-lsp", "..."]
|
||||
},
|
||||
@@ -748,6 +791,7 @@
|
||||
}
|
||||
},
|
||||
"TSX": {
|
||||
"language_servers": ["!typescript-language-server", "vtsls", "..."],
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
@@ -758,6 +802,7 @@
|
||||
}
|
||||
},
|
||||
"TypeScript": {
|
||||
"language_servers": ["!typescript-language-server", "vtsls", "..."],
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
|
||||
@@ -3,22 +3,22 @@ use editor::Editor;
|
||||
use extension::ExtensionStore;
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
actions, svg, AppContext, CursorStyle, EventEmitter, InteractiveElement as _, Model,
|
||||
ParentElement as _, Render, SharedString, StatefulInteractiveElement, Styled, View,
|
||||
ViewContext, VisualContext as _,
|
||||
actions, anchored, deferred, percentage, Animation, AnimationExt as _, AppContext, CursorStyle,
|
||||
DismissEvent, EventEmitter, InteractiveElement as _, Model, ParentElement as _, Render,
|
||||
SharedString, StatefulInteractiveElement, Styled, Transformation, View, ViewContext,
|
||||
VisualContext as _,
|
||||
};
|
||||
use language::{
|
||||
LanguageRegistry, LanguageServerBinaryStatus, LanguageServerId, LanguageServerName,
|
||||
};
|
||||
use language::{LanguageRegistry, LanguageServerBinaryStatus, LanguageServerName};
|
||||
use project::{LanguageServerProgress, Project};
|
||||
use smallvec::SmallVec;
|
||||
use std::{cmp::Reverse, fmt::Write, sync::Arc};
|
||||
use ui::prelude::*;
|
||||
use std::{cmp::Reverse, fmt::Write, sync::Arc, time::Duration};
|
||||
use ui::{prelude::*, ContextMenu};
|
||||
use workspace::{item::ItemHandle, StatusItemView, Workspace};
|
||||
|
||||
actions!(activity_indicator, [ShowErrorMessage]);
|
||||
|
||||
const DOWNLOAD_ICON: &str = "icons/download.svg";
|
||||
const WARNING_ICON: &str = "icons/warning.svg";
|
||||
|
||||
pub enum Event {
|
||||
ShowError { lsp_name: Arc<str>, error: String },
|
||||
}
|
||||
@@ -27,6 +27,7 @@ pub struct ActivityIndicator {
|
||||
statuses: Vec<LspStatus>,
|
||||
project: Model<Project>,
|
||||
auto_updater: Option<Model<AutoUpdater>>,
|
||||
context_menu: Option<View<ContextMenu>>,
|
||||
}
|
||||
|
||||
struct LspStatus {
|
||||
@@ -35,14 +36,14 @@ struct LspStatus {
|
||||
}
|
||||
|
||||
struct PendingWork<'a> {
|
||||
language_server_name: &'a str,
|
||||
language_server_id: LanguageServerId,
|
||||
progress_token: &'a str,
|
||||
progress: &'a LanguageServerProgress,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Content {
|
||||
icon: Option<&'static str>,
|
||||
icon: Option<gpui::AnyElement>,
|
||||
message: String,
|
||||
on_click: Option<Arc<dyn Fn(&mut ActivityIndicator, &mut ViewContext<ActivityIndicator>)>>,
|
||||
}
|
||||
@@ -78,6 +79,7 @@ impl ActivityIndicator {
|
||||
statuses: Default::default(),
|
||||
project: project.clone(),
|
||||
auto_updater,
|
||||
context_menu: None,
|
||||
}
|
||||
});
|
||||
|
||||
@@ -151,7 +153,7 @@ impl ActivityIndicator {
|
||||
.read(cx)
|
||||
.language_server_statuses()
|
||||
.rev()
|
||||
.filter_map(|status| {
|
||||
.filter_map(|(server_id, status)| {
|
||||
if status.pending_work.is_empty() {
|
||||
None
|
||||
} else {
|
||||
@@ -159,7 +161,7 @@ impl ActivityIndicator {
|
||||
.pending_work
|
||||
.iter()
|
||||
.map(|(token, progress)| PendingWork {
|
||||
language_server_name: status.name.as_str(),
|
||||
language_server_id: server_id,
|
||||
progress_token: token.as_str(),
|
||||
progress,
|
||||
})
|
||||
@@ -175,33 +177,44 @@ impl ActivityIndicator {
|
||||
// Show any language server has pending activity.
|
||||
let mut pending_work = self.pending_language_server_work(cx);
|
||||
if let Some(PendingWork {
|
||||
language_server_name,
|
||||
progress_token,
|
||||
progress,
|
||||
..
|
||||
}) = pending_work.next()
|
||||
{
|
||||
let mut message = language_server_name.to_string();
|
||||
|
||||
message.push_str(": ");
|
||||
if let Some(progress_message) = progress.message.as_ref() {
|
||||
message.push_str(progress_message);
|
||||
} else {
|
||||
message.push_str(progress_token);
|
||||
}
|
||||
let mut message = progress
|
||||
.title
|
||||
.as_deref()
|
||||
.unwrap_or(progress_token)
|
||||
.to_string();
|
||||
|
||||
if let Some(percentage) = progress.percentage {
|
||||
write!(&mut message, " ({}%)", percentage).unwrap();
|
||||
}
|
||||
|
||||
if let Some(progress_message) = progress.message.as_ref() {
|
||||
message.push_str(": ");
|
||||
message.push_str(progress_message);
|
||||
}
|
||||
|
||||
let additional_work_count = pending_work.count();
|
||||
if additional_work_count > 0 {
|
||||
write!(&mut message, " + {} more", additional_work_count).unwrap();
|
||||
}
|
||||
|
||||
return Content {
|
||||
icon: None,
|
||||
icon: Some(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
message,
|
||||
on_click: None,
|
||||
on_click: Some(Arc::new(Self::toggle_language_server_work_context_menu)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -222,7 +235,11 @@ impl ActivityIndicator {
|
||||
|
||||
if !downloading.is_empty() {
|
||||
return Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
icon: Some(
|
||||
Icon::new(IconName::Download)
|
||||
.size(IconSize::Small)
|
||||
.into_any_element(),
|
||||
),
|
||||
message: format!("Downloading {}...", downloading.join(", "),),
|
||||
on_click: None,
|
||||
};
|
||||
@@ -230,7 +247,11 @@ impl ActivityIndicator {
|
||||
|
||||
if !checking_for_update.is_empty() {
|
||||
return Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
icon: Some(
|
||||
Icon::new(IconName::Download)
|
||||
.size(IconSize::Small)
|
||||
.into_any_element(),
|
||||
),
|
||||
message: format!(
|
||||
"Checking for updates to {}...",
|
||||
checking_for_update.join(", "),
|
||||
@@ -241,7 +262,11 @@ impl ActivityIndicator {
|
||||
|
||||
if !failed.is_empty() {
|
||||
return Content {
|
||||
icon: Some(WARNING_ICON),
|
||||
icon: Some(
|
||||
Icon::new(IconName::ExclamationTriangle)
|
||||
.size(IconSize::Small)
|
||||
.into_any_element(),
|
||||
),
|
||||
message: format!(
|
||||
"Failed to download {}. Click to show error.",
|
||||
failed.join(", "),
|
||||
@@ -255,7 +280,11 @@ impl ActivityIndicator {
|
||||
// Show any formatting failure
|
||||
if let Some(failure) = self.project.read(cx).last_formatting_failure() {
|
||||
return Content {
|
||||
icon: Some(WARNING_ICON),
|
||||
icon: Some(
|
||||
Icon::new(IconName::ExclamationTriangle)
|
||||
.size(IconSize::Small)
|
||||
.into_any_element(),
|
||||
),
|
||||
message: format!("Formatting failed: {}. Click to see logs.", failure),
|
||||
on_click: Some(Arc::new(|_, cx| {
|
||||
cx.dispatch_action(Box::new(workspace::OpenLog));
|
||||
@@ -267,17 +296,29 @@ impl ActivityIndicator {
|
||||
if let Some(updater) = &self.auto_updater {
|
||||
return match &updater.read(cx).status() {
|
||||
AutoUpdateStatus::Checking => Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
icon: Some(
|
||||
Icon::new(IconName::Download)
|
||||
.size(IconSize::Small)
|
||||
.into_any_element(),
|
||||
),
|
||||
message: "Checking for Zed updates…".to_string(),
|
||||
on_click: None,
|
||||
},
|
||||
AutoUpdateStatus::Downloading => Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
icon: Some(
|
||||
Icon::new(IconName::Download)
|
||||
.size(IconSize::Small)
|
||||
.into_any_element(),
|
||||
),
|
||||
message: "Downloading Zed update…".to_string(),
|
||||
on_click: None,
|
||||
},
|
||||
AutoUpdateStatus::Installing => Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
icon: Some(
|
||||
Icon::new(IconName::Download)
|
||||
.size(IconSize::Small)
|
||||
.into_any_element(),
|
||||
),
|
||||
message: "Installing Zed update…".to_string(),
|
||||
on_click: None,
|
||||
},
|
||||
@@ -285,14 +326,18 @@ impl ActivityIndicator {
|
||||
icon: None,
|
||||
message: "Click to restart and update Zed".to_string(),
|
||||
on_click: Some(Arc::new({
|
||||
let restart = workspace::Restart {
|
||||
let reload = workspace::Reload {
|
||||
binary_path: Some(binary_path.clone()),
|
||||
};
|
||||
move |_, cx| workspace::restart(&restart, cx)
|
||||
move |_, cx| workspace::reload(&reload, cx)
|
||||
})),
|
||||
},
|
||||
AutoUpdateStatus::Errored => Content {
|
||||
icon: Some(WARNING_ICON),
|
||||
icon: Some(
|
||||
Icon::new(IconName::ExclamationTriangle)
|
||||
.size(IconSize::Small)
|
||||
.into_any_element(),
|
||||
),
|
||||
message: "Auto update failed".to_string(),
|
||||
on_click: Some(Arc::new(|this, cx| {
|
||||
this.dismiss_error_message(&Default::default(), cx)
|
||||
@@ -307,7 +352,11 @@ impl ActivityIndicator {
|
||||
{
|
||||
if let Some(extension_id) = extension_store.outstanding_operations().keys().next() {
|
||||
return Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
icon: Some(
|
||||
Icon::new(IconName::Download)
|
||||
.size(IconSize::Small)
|
||||
.into_any_element(),
|
||||
),
|
||||
message: format!("Updating {extension_id} extension…"),
|
||||
on_click: None,
|
||||
};
|
||||
@@ -316,6 +365,75 @@ impl ActivityIndicator {
|
||||
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn toggle_language_server_work_context_menu(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if self.context_menu.take().is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.build_lsp_work_context_menu(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn build_lsp_work_context_menu(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let mut has_work = false;
|
||||
let this = cx.view().downgrade();
|
||||
let context_menu = ContextMenu::build(cx, |mut menu, cx| {
|
||||
for work in self.pending_language_server_work(cx) {
|
||||
has_work = true;
|
||||
|
||||
let this = this.clone();
|
||||
let title = SharedString::from(
|
||||
work.progress
|
||||
.title
|
||||
.as_deref()
|
||||
.unwrap_or(work.progress_token)
|
||||
.to_string(),
|
||||
);
|
||||
if work.progress.is_cancellable {
|
||||
let language_server_id = work.language_server_id;
|
||||
let token = work.progress_token.to_string();
|
||||
menu = menu.custom_entry(
|
||||
move |_| {
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(Label::new(title.clone()))
|
||||
.child(Icon::new(IconName::XCircle))
|
||||
.into_any_element()
|
||||
},
|
||||
move |cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.project.update(cx, |project, cx| {
|
||||
project.cancel_language_server_work(
|
||||
language_server_id,
|
||||
Some(token.clone()),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
this.context_menu.take();
|
||||
})
|
||||
.ok();
|
||||
},
|
||||
);
|
||||
} else {
|
||||
menu = menu.label(title.clone());
|
||||
}
|
||||
}
|
||||
menu
|
||||
});
|
||||
|
||||
if has_work {
|
||||
cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
|
||||
this.context_menu.take();
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
cx.focus_view(&context_menu);
|
||||
self.context_menu = Some(context_menu);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for ActivityIndicator {}
|
||||
@@ -338,8 +456,17 @@ impl Render for ActivityIndicator {
|
||||
}
|
||||
|
||||
result
|
||||
.children(content.icon.map(|icon| svg().path(icon)))
|
||||
.gap_2()
|
||||
.children(content.icon)
|
||||
.child(Label::new(SharedString::from(content.message)).size(LabelSize::Small))
|
||||
.children(self.context_menu.as_ref().map(|menu| {
|
||||
deferred(
|
||||
anchored()
|
||||
.anchor(gpui::AnchorCorner::BottomLeft)
|
||||
.child(menu.clone()),
|
||||
)
|
||||
.with_priority(1)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ isahc.workspace = true
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
strum.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tokio.workspace = true
|
||||
|
||||
@@ -4,13 +4,16 @@ use http::{AsyncBody, HttpClient, Method, Request as HttpRequest};
|
||||
use isahc::config::Configurable;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{convert::TryFrom, time::Duration};
|
||||
use strum::EnumIter;
|
||||
|
||||
pub const ANTHROPIC_API_URL: &'static str = "https://api.anthropic.com";
|
||||
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
|
||||
pub enum Model {
|
||||
#[default]
|
||||
#[serde(alias = "claude-3-5-sonnet", rename = "claude-3-5-sonnet-20240620")]
|
||||
Claude3_5Sonnet,
|
||||
#[serde(alias = "claude-3-opus", rename = "claude-3-opus-20240229")]
|
||||
Claude3Opus,
|
||||
#[serde(alias = "claude-3-sonnet", rename = "claude-3-sonnet-20240229")]
|
||||
@@ -21,7 +24,9 @@ pub enum Model {
|
||||
|
||||
impl Model {
|
||||
pub fn from_id(id: &str) -> Result<Self> {
|
||||
if id.starts_with("claude-3-opus") {
|
||||
if id.starts_with("claude-3-5-sonnet") {
|
||||
Ok(Self::Claude3_5Sonnet)
|
||||
} else if id.starts_with("claude-3-opus") {
|
||||
Ok(Self::Claude3Opus)
|
||||
} else if id.starts_with("claude-3-sonnet") {
|
||||
Ok(Self::Claude3Sonnet)
|
||||
@@ -34,6 +39,7 @@ impl Model {
|
||||
|
||||
pub fn id(&self) -> &'static str {
|
||||
match self {
|
||||
Model::Claude3_5Sonnet => "claude-3-5-sonnet-20240620",
|
||||
Model::Claude3Opus => "claude-3-opus-20240229",
|
||||
Model::Claude3Sonnet => "claude-3-sonnet-20240229",
|
||||
Model::Claude3Haiku => "claude-3-opus-20240307",
|
||||
@@ -42,6 +48,7 @@ impl Model {
|
||||
|
||||
pub fn display_name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
|
||||
Self::Claude3Opus => "Claude 3 Opus",
|
||||
Self::Claude3Sonnet => "Claude 3 Sonnet",
|
||||
Self::Claude3Haiku => "Claude 3 Haiku",
|
||||
|
||||
@@ -16,40 +16,47 @@ doctest = false
|
||||
anyhow.workspace = true
|
||||
anthropic = { workspace = true, features = ["schemars"] }
|
||||
assistant_slash_command.workspace = true
|
||||
async-watch.workspace = true
|
||||
cargo_toml.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
file_icons.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
heed.workspace = true
|
||||
html_to_markdown.workspace = true
|
||||
http.workspace = true
|
||||
indoc.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
menu.workspace = true
|
||||
multi_buffer.workspace = true
|
||||
ollama = { workspace = true, features = ["schemars"] }
|
||||
open_ai = { workspace = true, features = ["schemars"] }
|
||||
ordered-float.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
project.workspace = true
|
||||
regex.workspace = true
|
||||
rope.workspace = true
|
||||
rustdoc_to_markdown.workspace = true
|
||||
rustdoc.workspace = true
|
||||
schemars.workspace = true
|
||||
search.workspace = true
|
||||
semantic_index.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
similar.workspace = true
|
||||
smol.workspace = true
|
||||
strsim = "0.11"
|
||||
strum.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
terminal_view.workspace = true
|
||||
theme.workspace = true
|
||||
tiktoken-rs.workspace = true
|
||||
toml.workspace = true
|
||||
@@ -58,7 +65,6 @@ util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace.workspace = true
|
||||
picker.workspace = true
|
||||
gray_matter = "0.2.7"
|
||||
|
||||
[dev-dependencies]
|
||||
ctor.workspace = true
|
||||
|
||||
@@ -1,29 +1,39 @@
|
||||
pub mod assistant_panel;
|
||||
pub mod assistant_settings;
|
||||
mod codegen;
|
||||
mod completion_provider;
|
||||
mod context_store;
|
||||
mod inline_assistant;
|
||||
mod model_selector;
|
||||
mod prompt_library;
|
||||
mod prompts;
|
||||
mod saved_conversation;
|
||||
mod search;
|
||||
mod slash_command;
|
||||
mod streaming_diff;
|
||||
|
||||
pub use assistant_panel::AssistantPanel;
|
||||
|
||||
use assistant_settings::{AnthropicModel, AssistantSettings, OpenAiModel, ZedDotDevModel};
|
||||
use assistant_settings::{AnthropicModel, AssistantSettings, CloudModel, OllamaModel, OpenAiModel};
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use client::{proto, Client};
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
pub(crate) use completion_provider::*;
|
||||
pub(crate) use context_store::*;
|
||||
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
|
||||
pub(crate) use saved_conversation::*;
|
||||
pub(crate) use inline_assistant::*;
|
||||
pub(crate) use model_selector::*;
|
||||
use rustdoc::RustdocStore;
|
||||
use semantic_index::{CloudEmbeddingProvider, SemanticIndex};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use slash_command::{
|
||||
active_command, default_command, diagnostics_command, fetch_command, file_command, now_command,
|
||||
project_command, prompt_command, rustdoc_command, search_command, tabs_command, term_command,
|
||||
};
|
||||
use std::{
|
||||
fmt::{self, Display},
|
||||
sync::Arc,
|
||||
};
|
||||
use util::paths::EMBEDDINGS_DIR;
|
||||
pub(crate) use streaming_diff::*;
|
||||
|
||||
actions!(
|
||||
assistant,
|
||||
@@ -38,7 +48,8 @@ actions!(
|
||||
InsertActivePrompt,
|
||||
ToggleHistory,
|
||||
ApplyEdit,
|
||||
ConfirmCommand
|
||||
ConfirmCommand,
|
||||
ToggleModelSelector
|
||||
]
|
||||
);
|
||||
|
||||
@@ -77,14 +88,15 @@ impl Display for Role {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub enum LanguageModel {
|
||||
ZedDotDev(ZedDotDevModel),
|
||||
Cloud(CloudModel),
|
||||
OpenAi(OpenAiModel),
|
||||
Anthropic(AnthropicModel),
|
||||
Ollama(OllamaModel),
|
||||
}
|
||||
|
||||
impl Default for LanguageModel {
|
||||
fn default() -> Self {
|
||||
LanguageModel::ZedDotDev(ZedDotDevModel::default())
|
||||
LanguageModel::Cloud(CloudModel::default())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +105,8 @@ impl LanguageModel {
|
||||
match self {
|
||||
LanguageModel::OpenAi(model) => format!("openai/{}", model.id()),
|
||||
LanguageModel::Anthropic(model) => format!("anthropic/{}", model.id()),
|
||||
LanguageModel::ZedDotDev(model) => format!("zed.dev/{}", model.id()),
|
||||
LanguageModel::Cloud(model) => format!("zed.dev/{}", model.id()),
|
||||
LanguageModel::Ollama(model) => format!("ollama/{}", model.id()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +114,8 @@ impl LanguageModel {
|
||||
match self {
|
||||
LanguageModel::OpenAi(model) => model.display_name().into(),
|
||||
LanguageModel::Anthropic(model) => model.display_name().into(),
|
||||
LanguageModel::ZedDotDev(model) => model.display_name().into(),
|
||||
LanguageModel::Cloud(model) => model.display_name().into(),
|
||||
LanguageModel::Ollama(model) => model.display_name().into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +123,8 @@ impl LanguageModel {
|
||||
match self {
|
||||
LanguageModel::OpenAi(model) => model.max_token_count(),
|
||||
LanguageModel::Anthropic(model) => model.max_token_count(),
|
||||
LanguageModel::ZedDotDev(model) => model.max_token_count(),
|
||||
LanguageModel::Cloud(model) => model.max_token_count(),
|
||||
LanguageModel::Ollama(model) => model.max_token_count(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +132,8 @@ impl LanguageModel {
|
||||
match self {
|
||||
LanguageModel::OpenAi(model) => model.id(),
|
||||
LanguageModel::Anthropic(model) => model.id(),
|
||||
LanguageModel::ZedDotDev(model) => model.id(),
|
||||
LanguageModel::Cloud(model) => model.id(),
|
||||
LanguageModel::Ollama(model) => model.id(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,6 +178,24 @@ impl LanguageModelRequest {
|
||||
tools: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Before we send the request to the server, we can perform fixups on it appropriate to the model.
|
||||
pub fn preprocess(&mut self) {
|
||||
match &self.model {
|
||||
LanguageModel::OpenAi(_) => {}
|
||||
LanguageModel::Anthropic(_) => {}
|
||||
LanguageModel::Ollama(_) => {}
|
||||
LanguageModel::Cloud(model) => match model {
|
||||
CloudModel::Claude3Opus
|
||||
| CloudModel::Claude3Sonnet
|
||||
| CloudModel::Claude3Haiku
|
||||
| CloudModel::Claude3_5Sonnet => {
|
||||
preprocess_anthropic_request(self);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
@@ -239,7 +273,7 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
async move {
|
||||
let embedding_provider = CloudEmbeddingProvider::new(client.clone());
|
||||
let semantic_index = SemanticIndex::new(
|
||||
EMBEDDINGS_DIR.join("semantic-index-db.0.mdb"),
|
||||
paths::embeddings_dir().join("semantic-index-db.0.mdb"),
|
||||
Arc::new(embedding_provider),
|
||||
&mut cx,
|
||||
)
|
||||
@@ -248,9 +282,14 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
completion_provider::init(client, cx);
|
||||
|
||||
prompt_library::init(cx);
|
||||
completion_provider::init(client.clone(), cx);
|
||||
assistant_slash_command::init(cx);
|
||||
register_slash_commands(cx);
|
||||
assistant_panel::init(cx);
|
||||
inline_assistant::init(client.telemetry().clone(), cx);
|
||||
RustdocStore::init_global(cx);
|
||||
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_namespace(Assistant::NAMESPACE);
|
||||
@@ -263,13 +302,28 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
cx.observe_global::<SettingsStore>(|cx| {
|
||||
Assistant::update_global(cx, |assistant, cx| {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
|
||||
assistant.set_enabled(settings.enabled, cx);
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn register_slash_commands(cx: &mut AppContext) {
|
||||
let slash_command_registry = SlashCommandRegistry::global(cx);
|
||||
slash_command_registry.register_command(file_command::FileSlashCommand, true);
|
||||
slash_command_registry.register_command(active_command::ActiveSlashCommand, true);
|
||||
slash_command_registry.register_command(tabs_command::TabsSlashCommand, true);
|
||||
slash_command_registry.register_command(project_command::ProjectSlashCommand, true);
|
||||
slash_command_registry.register_command(search_command::SearchSlashCommand, true);
|
||||
slash_command_registry.register_command(prompt_command::PromptSlashCommand, true);
|
||||
slash_command_registry.register_command(default_command::DefaultSlashCommand, true);
|
||||
slash_command_registry.register_command(term_command::TermSlashCommand, true);
|
||||
slash_command_registry.register_command(now_command::NowSlashCommand, true);
|
||||
slash_command_registry.register_command(diagnostics_command::DiagnosticsCommand, true);
|
||||
slash_command_registry.register_command(rustdoc_command::RustdocSlashCommand, false);
|
||||
slash_command_registry.register_command(fetch_command::FetchSlashCommand, false);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::fmt;
|
||||
|
||||
pub use anthropic::Model as AnthropicModel;
|
||||
use gpui::Pixels;
|
||||
pub use ollama::Model as OllamaModel;
|
||||
pub use open_ai::Model as OpenAiModel;
|
||||
use schemars::{
|
||||
schema::{InstanceType, Metadata, Schema, SchemaObject},
|
||||
@@ -12,21 +13,25 @@ use serde::{
|
||||
Deserialize, Deserializer, Serialize, Serializer,
|
||||
};
|
||||
use settings::{Settings, SettingsSources};
|
||||
use strum::{EnumIter, IntoEnumIterator};
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub enum ZedDotDevModel {
|
||||
use crate::{preprocess_anthropic_request, LanguageModel, LanguageModelRequest};
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, EnumIter)]
|
||||
pub enum CloudModel {
|
||||
Gpt3Point5Turbo,
|
||||
Gpt4,
|
||||
Gpt4Turbo,
|
||||
#[default]
|
||||
Gpt4Omni,
|
||||
Claude3_5Sonnet,
|
||||
Claude3Opus,
|
||||
Claude3Sonnet,
|
||||
Claude3Haiku,
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
impl Serialize for ZedDotDevModel {
|
||||
impl Serialize for CloudModel {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
@@ -35,7 +40,7 @@ impl Serialize for ZedDotDevModel {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ZedDotDevModel {
|
||||
impl<'de> Deserialize<'de> for CloudModel {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
@@ -43,7 +48,7 @@ impl<'de> Deserialize<'de> for ZedDotDevModel {
|
||||
struct ZedDotDevModelVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for ZedDotDevModelVisitor {
|
||||
type Value = ZedDotDevModel;
|
||||
type Value = CloudModel;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a string for a ZedDotDevModel variant or a custom model")
|
||||
@@ -53,13 +58,10 @@ impl<'de> Deserialize<'de> for ZedDotDevModel {
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
match value {
|
||||
"gpt-3.5-turbo" => Ok(ZedDotDevModel::Gpt3Point5Turbo),
|
||||
"gpt-4" => Ok(ZedDotDevModel::Gpt4),
|
||||
"gpt-4-turbo-preview" => Ok(ZedDotDevModel::Gpt4Turbo),
|
||||
"gpt-4o" => Ok(ZedDotDevModel::Gpt4Omni),
|
||||
_ => Ok(ZedDotDevModel::Custom(value.to_owned())),
|
||||
}
|
||||
let model = CloudModel::iter()
|
||||
.find(|model| model.id() == value)
|
||||
.unwrap_or_else(|| CloudModel::Custom(value.to_string()));
|
||||
Ok(model)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,30 +69,29 @@ impl<'de> Deserialize<'de> for ZedDotDevModel {
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for ZedDotDevModel {
|
||||
impl JsonSchema for CloudModel {
|
||||
fn schema_name() -> String {
|
||||
"ZedDotDevModel".to_owned()
|
||||
}
|
||||
|
||||
fn json_schema(_generator: &mut schemars::gen::SchemaGenerator) -> Schema {
|
||||
let variants = vec![
|
||||
"gpt-3.5-turbo".to_owned(),
|
||||
"gpt-4".to_owned(),
|
||||
"gpt-4-turbo-preview".to_owned(),
|
||||
"gpt-4o".to_owned(),
|
||||
];
|
||||
let variants = CloudModel::iter()
|
||||
.filter_map(|model| {
|
||||
let id = model.id();
|
||||
if id.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(id.to_string())
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
Schema::Object(SchemaObject {
|
||||
instance_type: Some(InstanceType::String.into()),
|
||||
enum_values: Some(variants.into_iter().map(|s| s.into()).collect()),
|
||||
enum_values: Some(variants.iter().map(|s| s.clone().into()).collect()),
|
||||
metadata: Some(Box::new(Metadata {
|
||||
title: Some("ZedDotDevModel".to_owned()),
|
||||
default: Some(serde_json::json!("gpt-4-turbo-preview")),
|
||||
examples: vec![
|
||||
serde_json::json!("gpt-3.5-turbo"),
|
||||
serde_json::json!("gpt-4"),
|
||||
serde_json::json!("gpt-4-turbo-preview"),
|
||||
serde_json::json!("custom-model-name"),
|
||||
],
|
||||
default: Some(CloudModel::default().id().into()),
|
||||
examples: variants.into_iter().map(Into::into).collect(),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
@@ -98,13 +99,14 @@ impl JsonSchema for ZedDotDevModel {
|
||||
}
|
||||
}
|
||||
|
||||
impl ZedDotDevModel {
|
||||
impl CloudModel {
|
||||
pub fn id(&self) -> &str {
|
||||
match self {
|
||||
Self::Gpt3Point5Turbo => "gpt-3.5-turbo",
|
||||
Self::Gpt4 => "gpt-4",
|
||||
Self::Gpt4Turbo => "gpt-4-turbo-preview",
|
||||
Self::Gpt4Omni => "gpt-4o",
|
||||
Self::Claude3_5Sonnet => "claude-3-5-sonnet",
|
||||
Self::Claude3Opus => "claude-3-opus",
|
||||
Self::Claude3Sonnet => "claude-3-sonnet",
|
||||
Self::Claude3Haiku => "claude-3-haiku",
|
||||
@@ -118,6 +120,7 @@ impl ZedDotDevModel {
|
||||
Self::Gpt4 => "GPT 4",
|
||||
Self::Gpt4Turbo => "GPT 4 Turbo",
|
||||
Self::Gpt4Omni => "GPT 4 Omni",
|
||||
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
|
||||
Self::Claude3Opus => "Claude 3 Opus",
|
||||
Self::Claude3Sonnet => "Claude 3 Sonnet",
|
||||
Self::Claude3Haiku => "Claude 3 Haiku",
|
||||
@@ -130,10 +133,22 @@ impl ZedDotDevModel {
|
||||
Self::Gpt3Point5Turbo => 2048,
|
||||
Self::Gpt4 => 4096,
|
||||
Self::Gpt4Turbo | Self::Gpt4Omni => 128000,
|
||||
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3Haiku => 200000,
|
||||
Self::Claude3_5Sonnet
|
||||
| Self::Claude3Opus
|
||||
| Self::Claude3Sonnet
|
||||
| Self::Claude3Haiku => 200000,
|
||||
Self::Custom(_) => 4096, // TODO: Make this configurable
|
||||
}
|
||||
}
|
||||
|
||||
pub fn preprocess_request(&self, request: &mut LanguageModelRequest) {
|
||||
match self {
|
||||
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3Haiku => {
|
||||
preprocess_anthropic_request(request)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -145,51 +160,64 @@ pub enum AssistantDockPosition {
|
||||
Bottom,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[serde(tag = "name", rename_all = "snake_case")]
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum AssistantProvider {
|
||||
#[serde(rename = "zed.dev")]
|
||||
ZedDotDev {
|
||||
#[serde(default)]
|
||||
default_model: ZedDotDevModel,
|
||||
model: CloudModel,
|
||||
},
|
||||
#[serde(rename = "openai")]
|
||||
OpenAi {
|
||||
#[serde(default)]
|
||||
default_model: OpenAiModel,
|
||||
#[serde(default = "open_ai_url")]
|
||||
model: OpenAiModel,
|
||||
api_url: String,
|
||||
#[serde(default)]
|
||||
low_speed_timeout_in_seconds: Option<u64>,
|
||||
},
|
||||
#[serde(rename = "anthropic")]
|
||||
Anthropic {
|
||||
#[serde(default)]
|
||||
default_model: AnthropicModel,
|
||||
#[serde(default = "anthropic_api_url")]
|
||||
model: AnthropicModel,
|
||||
api_url: String,
|
||||
low_speed_timeout_in_seconds: Option<u64>,
|
||||
},
|
||||
Ollama {
|
||||
model: OllamaModel,
|
||||
api_url: String,
|
||||
#[serde(default)]
|
||||
low_speed_timeout_in_seconds: Option<u64>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for AssistantProvider {
|
||||
fn default() -> Self {
|
||||
Self::ZedDotDev {
|
||||
default_model: ZedDotDevModel::default(),
|
||||
Self::OpenAi {
|
||||
model: OpenAiModel::default(),
|
||||
api_url: open_ai::OPEN_AI_API_URL.into(),
|
||||
low_speed_timeout_in_seconds: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn open_ai_url() -> String {
|
||||
open_ai::OPEN_AI_API_URL.to_string()
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[serde(tag = "name", rename_all = "snake_case")]
|
||||
pub enum AssistantProviderContent {
|
||||
#[serde(rename = "zed.dev")]
|
||||
ZedDotDev { default_model: Option<CloudModel> },
|
||||
#[serde(rename = "openai")]
|
||||
OpenAi {
|
||||
default_model: Option<OpenAiModel>,
|
||||
api_url: Option<String>,
|
||||
low_speed_timeout_in_seconds: Option<u64>,
|
||||
},
|
||||
#[serde(rename = "anthropic")]
|
||||
Anthropic {
|
||||
default_model: Option<AnthropicModel>,
|
||||
api_url: Option<String>,
|
||||
low_speed_timeout_in_seconds: Option<u64>,
|
||||
},
|
||||
#[serde(rename = "ollama")]
|
||||
Ollama {
|
||||
default_model: Option<OllamaModel>,
|
||||
api_url: Option<String>,
|
||||
low_speed_timeout_in_seconds: Option<u64>,
|
||||
},
|
||||
}
|
||||
|
||||
fn anthropic_api_url() -> String {
|
||||
anthropic::ANTHROPIC_API_URL.to_string()
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Deserialize, Serialize)]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct AssistantSettings {
|
||||
pub enabled: bool,
|
||||
pub button: bool,
|
||||
@@ -240,16 +268,16 @@ impl AssistantSettingsContent {
|
||||
default_width: settings.default_width,
|
||||
default_height: settings.default_height,
|
||||
provider: if let Some(open_ai_api_url) = settings.openai_api_url.as_ref() {
|
||||
Some(AssistantProvider::OpenAi {
|
||||
default_model: settings.default_open_ai_model.clone().unwrap_or_default(),
|
||||
api_url: open_ai_api_url.clone(),
|
||||
Some(AssistantProviderContent::OpenAi {
|
||||
default_model: settings.default_open_ai_model.clone(),
|
||||
api_url: Some(open_ai_api_url.clone()),
|
||||
low_speed_timeout_in_seconds: None,
|
||||
})
|
||||
} else {
|
||||
settings.default_open_ai_model.clone().map(|open_ai_model| {
|
||||
AssistantProvider::OpenAi {
|
||||
default_model: open_ai_model,
|
||||
api_url: open_ai_url(),
|
||||
AssistantProviderContent::OpenAi {
|
||||
default_model: Some(open_ai_model),
|
||||
api_url: None,
|
||||
low_speed_timeout_in_seconds: None,
|
||||
}
|
||||
})
|
||||
@@ -270,6 +298,71 @@ impl AssistantSettingsContent {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_model(&mut self, new_model: LanguageModel) {
|
||||
match self {
|
||||
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||
VersionedAssistantSettingsContent::V1(settings) => match &mut settings.provider {
|
||||
Some(AssistantProviderContent::ZedDotDev {
|
||||
default_model: model,
|
||||
}) => {
|
||||
if let LanguageModel::Cloud(new_model) = new_model {
|
||||
*model = Some(new_model);
|
||||
}
|
||||
}
|
||||
Some(AssistantProviderContent::OpenAi {
|
||||
default_model: model,
|
||||
..
|
||||
}) => {
|
||||
if let LanguageModel::OpenAi(new_model) = new_model {
|
||||
*model = Some(new_model);
|
||||
}
|
||||
}
|
||||
Some(AssistantProviderContent::Anthropic {
|
||||
default_model: model,
|
||||
..
|
||||
}) => {
|
||||
if let LanguageModel::Anthropic(new_model) = new_model {
|
||||
*model = Some(new_model);
|
||||
}
|
||||
}
|
||||
provider => match new_model {
|
||||
LanguageModel::Cloud(model) => {
|
||||
*provider = Some(AssistantProviderContent::ZedDotDev {
|
||||
default_model: Some(model),
|
||||
})
|
||||
}
|
||||
LanguageModel::OpenAi(model) => {
|
||||
*provider = Some(AssistantProviderContent::OpenAi {
|
||||
default_model: Some(model),
|
||||
api_url: None,
|
||||
low_speed_timeout_in_seconds: None,
|
||||
})
|
||||
}
|
||||
LanguageModel::Anthropic(model) => {
|
||||
*provider = Some(AssistantProviderContent::Anthropic {
|
||||
default_model: Some(model),
|
||||
api_url: None,
|
||||
low_speed_timeout_in_seconds: None,
|
||||
})
|
||||
}
|
||||
LanguageModel::Ollama(model) => {
|
||||
*provider = Some(AssistantProviderContent::Ollama {
|
||||
default_model: Some(model),
|
||||
api_url: None,
|
||||
low_speed_timeout_in_seconds: None,
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
AssistantSettingsContent::Legacy(settings) => {
|
||||
if let LanguageModel::OpenAi(model) = new_model {
|
||||
settings.default_open_ai_model = Some(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
@@ -318,7 +411,7 @@ pub struct AssistantSettingsContentV1 {
|
||||
///
|
||||
/// This can either be the internal `zed.dev` service or an external `openai` service,
|
||||
/// each with their respective default models and configurations.
|
||||
provider: Option<AssistantProvider>,
|
||||
provider: Option<AssistantProviderContent>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
@@ -376,31 +469,112 @@ impl Settings for AssistantSettings {
|
||||
if let Some(provider) = value.provider.clone() {
|
||||
match (&mut settings.provider, provider) {
|
||||
(
|
||||
AssistantProvider::ZedDotDev { default_model },
|
||||
AssistantProvider::ZedDotDev {
|
||||
default_model: default_model_override,
|
||||
AssistantProvider::ZedDotDev { model },
|
||||
AssistantProviderContent::ZedDotDev {
|
||||
default_model: model_override,
|
||||
},
|
||||
) => {
|
||||
*default_model = default_model_override;
|
||||
merge(model, model_override);
|
||||
}
|
||||
(
|
||||
AssistantProvider::OpenAi {
|
||||
default_model,
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
},
|
||||
AssistantProvider::OpenAi {
|
||||
default_model: default_model_override,
|
||||
AssistantProviderContent::OpenAi {
|
||||
default_model: model_override,
|
||||
api_url: api_url_override,
|
||||
low_speed_timeout_in_seconds: low_speed_timeout_in_seconds_override,
|
||||
},
|
||||
) => {
|
||||
*default_model = default_model_override;
|
||||
*api_url = api_url_override;
|
||||
*low_speed_timeout_in_seconds = low_speed_timeout_in_seconds_override;
|
||||
merge(model, model_override);
|
||||
merge(api_url, api_url_override);
|
||||
if let Some(low_speed_timeout_in_seconds_override) =
|
||||
low_speed_timeout_in_seconds_override
|
||||
{
|
||||
*low_speed_timeout_in_seconds =
|
||||
Some(low_speed_timeout_in_seconds_override);
|
||||
}
|
||||
}
|
||||
(merged, provider_override) => {
|
||||
*merged = provider_override;
|
||||
(
|
||||
AssistantProvider::Ollama {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
},
|
||||
AssistantProviderContent::Ollama {
|
||||
default_model: model_override,
|
||||
api_url: api_url_override,
|
||||
low_speed_timeout_in_seconds: low_speed_timeout_in_seconds_override,
|
||||
},
|
||||
) => {
|
||||
merge(model, model_override);
|
||||
merge(api_url, api_url_override);
|
||||
if let Some(low_speed_timeout_in_seconds_override) =
|
||||
low_speed_timeout_in_seconds_override
|
||||
{
|
||||
*low_speed_timeout_in_seconds =
|
||||
Some(low_speed_timeout_in_seconds_override);
|
||||
}
|
||||
}
|
||||
(
|
||||
AssistantProvider::Anthropic {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
},
|
||||
AssistantProviderContent::Anthropic {
|
||||
default_model: model_override,
|
||||
api_url: api_url_override,
|
||||
low_speed_timeout_in_seconds: low_speed_timeout_in_seconds_override,
|
||||
},
|
||||
) => {
|
||||
merge(model, model_override);
|
||||
merge(api_url, api_url_override);
|
||||
if let Some(low_speed_timeout_in_seconds_override) =
|
||||
low_speed_timeout_in_seconds_override
|
||||
{
|
||||
*low_speed_timeout_in_seconds =
|
||||
Some(low_speed_timeout_in_seconds_override);
|
||||
}
|
||||
}
|
||||
(provider, provider_override) => {
|
||||
*provider = match provider_override {
|
||||
AssistantProviderContent::ZedDotDev {
|
||||
default_model: model,
|
||||
} => AssistantProvider::ZedDotDev {
|
||||
model: model.unwrap_or_default(),
|
||||
},
|
||||
AssistantProviderContent::OpenAi {
|
||||
default_model: model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
} => AssistantProvider::OpenAi {
|
||||
model: model.unwrap_or_default(),
|
||||
api_url: api_url.unwrap_or_else(|| open_ai::OPEN_AI_API_URL.into()),
|
||||
low_speed_timeout_in_seconds,
|
||||
},
|
||||
AssistantProviderContent::Anthropic {
|
||||
default_model: model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
} => AssistantProvider::Anthropic {
|
||||
model: model.unwrap_or_default(),
|
||||
api_url: api_url
|
||||
.unwrap_or_else(|| anthropic::ANTHROPIC_API_URL.into()),
|
||||
low_speed_timeout_in_seconds,
|
||||
},
|
||||
AssistantProviderContent::Ollama {
|
||||
default_model: model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
} => AssistantProvider::Ollama {
|
||||
model: model.unwrap_or_default(),
|
||||
api_url: api_url.unwrap_or_else(|| ollama::OLLAMA_API_URL.into()),
|
||||
low_speed_timeout_in_seconds,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -410,7 +584,7 @@ impl Settings for AssistantSettings {
|
||||
}
|
||||
}
|
||||
|
||||
fn merge<T: Copy>(target: &mut T, value: Option<T>) {
|
||||
fn merge<T>(target: &mut T, value: Option<T>) {
|
||||
if let Some(value) = value {
|
||||
*target = value;
|
||||
}
|
||||
@@ -433,8 +607,8 @@ mod tests {
|
||||
assert_eq!(
|
||||
AssistantSettings::get_global(cx).provider,
|
||||
AssistantProvider::OpenAi {
|
||||
default_model: OpenAiModel::FourOmni,
|
||||
api_url: open_ai_url(),
|
||||
model: OpenAiModel::FourOmni,
|
||||
api_url: open_ai::OPEN_AI_API_URL.into(),
|
||||
low_speed_timeout_in_seconds: None,
|
||||
}
|
||||
);
|
||||
@@ -455,7 +629,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
AssistantSettings::get_global(cx).provider,
|
||||
AssistantProvider::OpenAi {
|
||||
default_model: OpenAiModel::FourOmni,
|
||||
model: OpenAiModel::FourOmni,
|
||||
api_url: "test-url".into(),
|
||||
low_speed_timeout_in_seconds: None,
|
||||
}
|
||||
@@ -475,8 +649,8 @@ mod tests {
|
||||
assert_eq!(
|
||||
AssistantSettings::get_global(cx).provider,
|
||||
AssistantProvider::OpenAi {
|
||||
default_model: OpenAiModel::Four,
|
||||
api_url: open_ai_url(),
|
||||
model: OpenAiModel::Four,
|
||||
api_url: open_ai::OPEN_AI_API_URL.into(),
|
||||
low_speed_timeout_in_seconds: None,
|
||||
}
|
||||
);
|
||||
@@ -501,7 +675,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
AssistantSettings::get_global(cx).provider,
|
||||
AssistantProvider::ZedDotDev {
|
||||
default_model: ZedDotDevModel::Custom("custom".into())
|
||||
model: CloudModel::Custom("custom".into())
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,696 +0,0 @@
|
||||
use crate::{
|
||||
streaming_diff::{Hunk, StreamingDiff},
|
||||
CompletionProvider, LanguageModelRequest,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use client::telemetry::Telemetry;
|
||||
use editor::{Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
|
||||
use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
|
||||
use gpui::{EventEmitter, Model, ModelContext, Task};
|
||||
use language::{Rope, TransactionId};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use std::{cmp, future, ops::Range, sync::Arc, time::Instant};
|
||||
|
||||
pub enum Event {
|
||||
Finished,
|
||||
Undone,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum CodegenKind {
|
||||
Transform { range: Range<Anchor> },
|
||||
Generate { position: Anchor },
|
||||
}
|
||||
|
||||
pub struct Codegen {
|
||||
buffer: Model<MultiBuffer>,
|
||||
snapshot: MultiBufferSnapshot,
|
||||
kind: CodegenKind,
|
||||
last_equal_ranges: Vec<Range<Anchor>>,
|
||||
transaction_id: Option<TransactionId>,
|
||||
error: Option<anyhow::Error>,
|
||||
generation: Task<()>,
|
||||
idle: bool,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
_subscription: gpui::Subscription,
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for Codegen {}
|
||||
|
||||
impl Codegen {
|
||||
pub fn new(
|
||||
buffer: Model<MultiBuffer>,
|
||||
kind: CodegenKind,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let snapshot = buffer.read(cx).snapshot(cx);
|
||||
Self {
|
||||
buffer: buffer.clone(),
|
||||
snapshot,
|
||||
kind,
|
||||
last_equal_ranges: Default::default(),
|
||||
transaction_id: Default::default(),
|
||||
error: Default::default(),
|
||||
idle: true,
|
||||
generation: Task::ready(()),
|
||||
telemetry,
|
||||
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_buffer_event(
|
||||
&mut self,
|
||||
_buffer: Model<MultiBuffer>,
|
||||
event: &multi_buffer::Event,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if let multi_buffer::Event::TransactionUndone { transaction_id } = event {
|
||||
if self.transaction_id == Some(*transaction_id) {
|
||||
self.transaction_id = None;
|
||||
self.generation = Task::ready(());
|
||||
cx.emit(Event::Undone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn range(&self) -> Range<Anchor> {
|
||||
match &self.kind {
|
||||
CodegenKind::Transform { range } => range.clone(),
|
||||
CodegenKind::Generate { position } => position.bias_left(&self.snapshot)..*position,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> &CodegenKind {
|
||||
&self.kind
|
||||
}
|
||||
|
||||
pub fn last_equal_ranges(&self) -> &[Range<Anchor>] {
|
||||
&self.last_equal_ranges
|
||||
}
|
||||
|
||||
pub fn idle(&self) -> bool {
|
||||
self.idle
|
||||
}
|
||||
|
||||
pub fn error(&self) -> Option<&anyhow::Error> {
|
||||
self.error.as_ref()
|
||||
}
|
||||
|
||||
pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut ModelContext<Self>) {
|
||||
let range = self.range();
|
||||
let snapshot = self.snapshot.clone();
|
||||
let selected_text = snapshot
|
||||
.text_for_range(range.start..range.end)
|
||||
.collect::<Rope>();
|
||||
|
||||
let selection_start = range.start.to_point(&snapshot);
|
||||
let suggested_line_indent = snapshot
|
||||
.suggested_indents(selection_start.row..selection_start.row + 1, cx)
|
||||
.into_values()
|
||||
.next()
|
||||
.unwrap_or_else(|| snapshot.indent_size_for_line(MultiBufferRow(selection_start.row)));
|
||||
|
||||
let model_telemetry_id = prompt.model.telemetry_id();
|
||||
let response = CompletionProvider::global(cx).complete(prompt);
|
||||
let telemetry = self.telemetry.clone();
|
||||
self.generation = cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
let generate = async {
|
||||
let mut edit_start = range.start.to_offset(&snapshot);
|
||||
|
||||
let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
|
||||
let diff = cx.background_executor().spawn(async move {
|
||||
let mut response_latency = None;
|
||||
let request_start = Instant::now();
|
||||
let diff = async {
|
||||
let chunks = strip_invalid_spans_from_codeblock(response.await?);
|
||||
futures::pin_mut!(chunks);
|
||||
let mut diff = StreamingDiff::new(selected_text.to_string());
|
||||
|
||||
let mut new_text = String::new();
|
||||
let mut base_indent = None;
|
||||
let mut line_indent = None;
|
||||
let mut first_line = true;
|
||||
|
||||
while let Some(chunk) = chunks.next().await {
|
||||
if response_latency.is_none() {
|
||||
response_latency = Some(request_start.elapsed());
|
||||
}
|
||||
let chunk = chunk?;
|
||||
|
||||
let mut lines = chunk.split('\n').peekable();
|
||||
while let Some(line) = lines.next() {
|
||||
new_text.push_str(line);
|
||||
if line_indent.is_none() {
|
||||
if let Some(non_whitespace_ch_ix) =
|
||||
new_text.find(|ch: char| !ch.is_whitespace())
|
||||
{
|
||||
line_indent = Some(non_whitespace_ch_ix);
|
||||
base_indent = base_indent.or(line_indent);
|
||||
|
||||
let line_indent = line_indent.unwrap();
|
||||
let base_indent = base_indent.unwrap();
|
||||
let indent_delta =
|
||||
line_indent as i32 - base_indent as i32;
|
||||
let mut corrected_indent_len = cmp::max(
|
||||
0,
|
||||
suggested_line_indent.len as i32 + indent_delta,
|
||||
)
|
||||
as usize;
|
||||
if first_line {
|
||||
corrected_indent_len = corrected_indent_len
|
||||
.saturating_sub(
|
||||
selection_start.column as usize,
|
||||
);
|
||||
}
|
||||
|
||||
let indent_char = suggested_line_indent.char();
|
||||
let mut indent_buffer = [0; 4];
|
||||
let indent_str =
|
||||
indent_char.encode_utf8(&mut indent_buffer);
|
||||
new_text.replace_range(
|
||||
..line_indent,
|
||||
&indent_str.repeat(corrected_indent_len),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if line_indent.is_some() {
|
||||
hunks_tx.send(diff.push_new(&new_text)).await?;
|
||||
new_text.clear();
|
||||
}
|
||||
|
||||
if lines.peek().is_some() {
|
||||
hunks_tx.send(diff.push_new("\n")).await?;
|
||||
line_indent = None;
|
||||
first_line = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
hunks_tx.send(diff.push_new(&new_text)).await?;
|
||||
hunks_tx.send(diff.finish()).await?;
|
||||
|
||||
anyhow::Ok(())
|
||||
};
|
||||
|
||||
let error_message = diff.await.err().map(|error| error.to_string());
|
||||
if let Some(telemetry) = telemetry {
|
||||
telemetry.report_assistant_event(
|
||||
None,
|
||||
telemetry_events::AssistantKind::Inline,
|
||||
model_telemetry_id,
|
||||
response_latency,
|
||||
error_message,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
while let Some(hunks) = hunks_rx.next().await {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.last_equal_ranges.clear();
|
||||
|
||||
let transaction = this.buffer.update(cx, |buffer, cx| {
|
||||
// Avoid grouping assistant edits with user edits.
|
||||
buffer.finalize_last_transaction(cx);
|
||||
|
||||
buffer.start_transaction(cx);
|
||||
buffer.edit(
|
||||
hunks.into_iter().filter_map(|hunk| match hunk {
|
||||
Hunk::Insert { text } => {
|
||||
let edit_start = snapshot.anchor_after(edit_start);
|
||||
Some((edit_start..edit_start, text))
|
||||
}
|
||||
Hunk::Remove { len } => {
|
||||
let edit_end = edit_start + len;
|
||||
let edit_range = snapshot.anchor_after(edit_start)
|
||||
..snapshot.anchor_before(edit_end);
|
||||
edit_start = edit_end;
|
||||
Some((edit_range, String::new()))
|
||||
}
|
||||
Hunk::Keep { len } => {
|
||||
let edit_end = edit_start + len;
|
||||
let edit_range = snapshot.anchor_after(edit_start)
|
||||
..snapshot.anchor_before(edit_end);
|
||||
edit_start = edit_end;
|
||||
this.last_equal_ranges.push(edit_range);
|
||||
None
|
||||
}
|
||||
}),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
|
||||
buffer.end_transaction(cx)
|
||||
});
|
||||
|
||||
if let Some(transaction) = transaction {
|
||||
if let Some(first_transaction) = this.transaction_id {
|
||||
// Group all assistant edits into the first transaction.
|
||||
this.buffer.update(cx, |buffer, cx| {
|
||||
buffer.merge_transactions(
|
||||
transaction,
|
||||
first_transaction,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
} else {
|
||||
this.transaction_id = Some(transaction);
|
||||
this.buffer.update(cx, |buffer, cx| {
|
||||
buffer.finalize_last_transaction(cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
|
||||
diff.await;
|
||||
|
||||
anyhow::Ok(())
|
||||
};
|
||||
|
||||
let result = generate.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.last_equal_ranges.clear();
|
||||
this.idle = true;
|
||||
if let Err(error) = result {
|
||||
this.error = Some(error);
|
||||
}
|
||||
cx.emit(Event::Finished);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
self.error.take();
|
||||
self.idle = false;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn undo(&mut self, cx: &mut ModelContext<Self>) {
|
||||
if let Some(transaction_id) = self.transaction_id {
|
||||
self.buffer
|
||||
.update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn strip_invalid_spans_from_codeblock(
|
||||
stream: impl Stream<Item = Result<String>>,
|
||||
) -> impl Stream<Item = Result<String>> {
|
||||
let mut first_line = true;
|
||||
let mut buffer = String::new();
|
||||
let mut starts_with_markdown_codeblock = false;
|
||||
let mut includes_start_or_end_span = false;
|
||||
stream.filter_map(move |chunk| {
|
||||
let chunk = match chunk {
|
||||
Ok(chunk) => chunk,
|
||||
Err(err) => return future::ready(Some(Err(err))),
|
||||
};
|
||||
buffer.push_str(&chunk);
|
||||
|
||||
if buffer.len() > "<|S|".len() && buffer.starts_with("<|S|") {
|
||||
includes_start_or_end_span = true;
|
||||
|
||||
buffer = buffer
|
||||
.strip_prefix("<|S|>")
|
||||
.or_else(|| buffer.strip_prefix("<|S|"))
|
||||
.unwrap_or(&buffer)
|
||||
.to_string();
|
||||
} else if buffer.ends_with("|E|>") {
|
||||
includes_start_or_end_span = true;
|
||||
} else if buffer.starts_with("<|")
|
||||
|| buffer.starts_with("<|S")
|
||||
|| buffer.starts_with("<|S|")
|
||||
|| buffer.ends_with('|')
|
||||
|| buffer.ends_with("|E")
|
||||
|| buffer.ends_with("|E|")
|
||||
{
|
||||
return future::ready(None);
|
||||
}
|
||||
|
||||
if first_line {
|
||||
if buffer.is_empty() || buffer == "`" || buffer == "``" {
|
||||
return future::ready(None);
|
||||
} else if buffer.starts_with("```") {
|
||||
starts_with_markdown_codeblock = true;
|
||||
if let Some(newline_ix) = buffer.find('\n') {
|
||||
buffer.replace_range(..newline_ix + 1, "");
|
||||
first_line = false;
|
||||
} else {
|
||||
return future::ready(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut text = buffer.to_string();
|
||||
if starts_with_markdown_codeblock {
|
||||
text = text
|
||||
.strip_suffix("\n```\n")
|
||||
.or_else(|| text.strip_suffix("\n```"))
|
||||
.or_else(|| text.strip_suffix("\n``"))
|
||||
.or_else(|| text.strip_suffix("\n`"))
|
||||
.or_else(|| text.strip_suffix('\n'))
|
||||
.unwrap_or(&text)
|
||||
.to_string();
|
||||
}
|
||||
|
||||
if includes_start_or_end_span {
|
||||
text = text
|
||||
.strip_suffix("|E|>")
|
||||
.or_else(|| text.strip_suffix("E|>"))
|
||||
.or_else(|| text.strip_prefix("|>"))
|
||||
.or_else(|| text.strip_prefix('>'))
|
||||
.unwrap_or(&text)
|
||||
.to_string();
|
||||
};
|
||||
|
||||
if text.contains('\n') {
|
||||
first_line = false;
|
||||
}
|
||||
|
||||
let remainder = buffer.split_off(text.len());
|
||||
let result = if buffer.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Ok(buffer.clone()))
|
||||
};
|
||||
|
||||
buffer = remainder;
|
||||
future::ready(result)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::FakeCompletionProvider;
|
||||
|
||||
use super::*;
|
||||
use futures::stream::{self};
|
||||
use gpui::{Context, TestAppContext};
|
||||
use indoc::indoc;
|
||||
use language::{
|
||||
language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, LanguageMatcher,
|
||||
Point,
|
||||
};
|
||||
use rand::prelude::*;
|
||||
use serde::Serialize;
|
||||
use settings::SettingsStore;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct DummyCompletionRequest {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_transform_autoindent(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
let provider = FakeCompletionProvider::default();
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
cx.set_global(CompletionProvider::Fake(provider.clone()));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
let text = indoc! {"
|
||||
fn main() {
|
||||
let x = 0;
|
||||
for _ in 0..10 {
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
"};
|
||||
let buffer =
|
||||
cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let range = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5))
|
||||
});
|
||||
let codegen = cx.new_model(|cx| {
|
||||
Codegen::new(buffer.clone(), CodegenKind::Transform { range }, None, cx)
|
||||
});
|
||||
|
||||
let request = LanguageModelRequest::default();
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
|
||||
|
||||
let mut new_text = concat!(
|
||||
" let mut x = 0;\n",
|
||||
" while x < 10 {\n",
|
||||
" x += 1;\n",
|
||||
" }",
|
||||
);
|
||||
while !new_text.is_empty() {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
provider.send_completion(chunk.into());
|
||||
new_text = suffix;
|
||||
cx.background_executor.run_until_parked();
|
||||
}
|
||||
provider.finish_completion();
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
|
||||
indoc! {"
|
||||
fn main() {
|
||||
let mut x = 0;
|
||||
while x < 10 {
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
"}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_autoindent_when_generating_past_indentation(
|
||||
cx: &mut TestAppContext,
|
||||
mut rng: StdRng,
|
||||
) {
|
||||
let provider = FakeCompletionProvider::default();
|
||||
cx.set_global(CompletionProvider::Fake(provider.clone()));
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
let text = indoc! {"
|
||||
fn main() {
|
||||
le
|
||||
}
|
||||
"};
|
||||
let buffer =
|
||||
cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let position = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(1, 6))
|
||||
});
|
||||
let codegen = cx.new_model(|cx| {
|
||||
Codegen::new(buffer.clone(), CodegenKind::Generate { position }, None, cx)
|
||||
});
|
||||
|
||||
let request = LanguageModelRequest::default();
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
|
||||
|
||||
let mut new_text = concat!(
|
||||
"t mut x = 0;\n",
|
||||
"while x < 10 {\n",
|
||||
" x += 1;\n",
|
||||
"}", //
|
||||
);
|
||||
while !new_text.is_empty() {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
provider.send_completion(chunk.into());
|
||||
new_text = suffix;
|
||||
cx.background_executor.run_until_parked();
|
||||
}
|
||||
provider.finish_completion();
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
|
||||
indoc! {"
|
||||
fn main() {
|
||||
let mut x = 0;
|
||||
while x < 10 {
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
"}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_autoindent_when_generating_before_indentation(
|
||||
cx: &mut TestAppContext,
|
||||
mut rng: StdRng,
|
||||
) {
|
||||
let provider = FakeCompletionProvider::default();
|
||||
cx.set_global(CompletionProvider::Fake(provider.clone()));
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
let text = concat!(
|
||||
"fn main() {\n",
|
||||
" \n",
|
||||
"}\n" //
|
||||
);
|
||||
let buffer =
|
||||
cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let position = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(1, 2))
|
||||
});
|
||||
let codegen = cx.new_model(|cx| {
|
||||
Codegen::new(buffer.clone(), CodegenKind::Generate { position }, None, cx)
|
||||
});
|
||||
|
||||
let request = LanguageModelRequest::default();
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
|
||||
|
||||
let mut new_text = concat!(
|
||||
"let mut x = 0;\n",
|
||||
"while x < 10 {\n",
|
||||
" x += 1;\n",
|
||||
"}", //
|
||||
);
|
||||
while !new_text.is_empty() {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
provider.send_completion(chunk.into());
|
||||
new_text = suffix;
|
||||
cx.background_executor.run_until_parked();
|
||||
}
|
||||
provider.finish_completion();
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
|
||||
indoc! {"
|
||||
fn main() {
|
||||
let mut x = 0;
|
||||
while x < 10 {
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
"}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_strip_invalid_spans_from_codeblock() {
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("Lorem ipsum dolor", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor\n```\n", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks(
|
||||
"```html\n```js\nLorem ipsum dolor\n```\n```",
|
||||
2
|
||||
))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"```js\nLorem ipsum dolor\n```"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("``\nLorem ipsum dolor\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"``\nLorem ipsum dolor\n```"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("<|S|Lorem ipsum|E|>", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("<|S|>Lorem ipsum", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\n<|S|>Lorem ipsum\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\n<|S|Lorem ipsum|E|>\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum"
|
||||
);
|
||||
fn chunks(text: &str, size: usize) -> impl Stream<Item = Result<String>> {
|
||||
stream::iter(
|
||||
text.chars()
|
||||
.collect::<Vec<_>>()
|
||||
.chunks(size)
|
||||
.map(|chunk| Ok(chunk.iter().collect::<String>()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn rust_lang() -> Language {
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
)
|
||||
.with_indents_query(
|
||||
r#"
|
||||
(call_expression) @indent
|
||||
(field_expression) @indent
|
||||
(_ "(" ")" @end) @indent
|
||||
(_ "{" "}" @end) @indent
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
mod anthropic;
|
||||
mod cloud;
|
||||
#[cfg(test)]
|
||||
mod fake;
|
||||
mod ollama;
|
||||
mod open_ai;
|
||||
mod zed;
|
||||
|
||||
pub use anthropic::*;
|
||||
pub use cloud::*;
|
||||
#[cfg(test)]
|
||||
pub use fake::*;
|
||||
pub use ollama::*;
|
||||
pub use open_ai::*;
|
||||
pub use zed::*;
|
||||
|
||||
use crate::{
|
||||
assistant_settings::{AssistantProvider, AssistantSettings},
|
||||
@@ -25,36 +27,43 @@ use std::time::Duration;
|
||||
pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
let mut settings_version = 0;
|
||||
let provider = match &AssistantSettings::get_global(cx).provider {
|
||||
AssistantProvider::ZedDotDev { default_model } => {
|
||||
CompletionProvider::ZedDotDev(ZedDotDevCompletionProvider::new(
|
||||
default_model.clone(),
|
||||
client.clone(),
|
||||
settings_version,
|
||||
cx,
|
||||
))
|
||||
}
|
||||
AssistantProvider::ZedDotDev { model } => CompletionProvider::Cloud(
|
||||
CloudCompletionProvider::new(model.clone(), client.clone(), settings_version, cx),
|
||||
),
|
||||
AssistantProvider::OpenAi {
|
||||
default_model,
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
} => CompletionProvider::OpenAi(OpenAiCompletionProvider::new(
|
||||
default_model.clone(),
|
||||
model.clone(),
|
||||
api_url.clone(),
|
||||
client.http_client(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
settings_version,
|
||||
)),
|
||||
AssistantProvider::Anthropic {
|
||||
default_model,
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
} => CompletionProvider::Anthropic(AnthropicCompletionProvider::new(
|
||||
default_model.clone(),
|
||||
model.clone(),
|
||||
api_url.clone(),
|
||||
client.http_client(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
settings_version,
|
||||
)),
|
||||
AssistantProvider::Ollama {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
} => CompletionProvider::Ollama(OllamaCompletionProvider::new(
|
||||
model.clone(),
|
||||
api_url.clone(),
|
||||
client.http_client(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
settings_version,
|
||||
cx,
|
||||
)),
|
||||
};
|
||||
cx.set_global(provider);
|
||||
|
||||
@@ -65,13 +74,13 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
(
|
||||
CompletionProvider::OpenAi(provider),
|
||||
AssistantProvider::OpenAi {
|
||||
default_model,
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
},
|
||||
) => {
|
||||
provider.update(
|
||||
default_model.clone(),
|
||||
model.clone(),
|
||||
api_url.clone(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
settings_version,
|
||||
@@ -80,27 +89,42 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
(
|
||||
CompletionProvider::Anthropic(provider),
|
||||
AssistantProvider::Anthropic {
|
||||
default_model,
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
},
|
||||
) => {
|
||||
provider.update(
|
||||
default_model.clone(),
|
||||
model.clone(),
|
||||
api_url.clone(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
settings_version,
|
||||
);
|
||||
}
|
||||
|
||||
(
|
||||
CompletionProvider::ZedDotDev(provider),
|
||||
AssistantProvider::ZedDotDev { default_model },
|
||||
CompletionProvider::Ollama(provider),
|
||||
AssistantProvider::Ollama {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
},
|
||||
) => {
|
||||
provider.update(default_model.clone(), settings_version);
|
||||
provider.update(
|
||||
model.clone(),
|
||||
api_url.clone(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
settings_version,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
(_, AssistantProvider::ZedDotDev { default_model }) => {
|
||||
*provider = CompletionProvider::ZedDotDev(ZedDotDevCompletionProvider::new(
|
||||
default_model.clone(),
|
||||
|
||||
(CompletionProvider::Cloud(provider), AssistantProvider::ZedDotDev { model }) => {
|
||||
provider.update(model.clone(), settings_version);
|
||||
}
|
||||
(_, AssistantProvider::ZedDotDev { model }) => {
|
||||
*provider = CompletionProvider::Cloud(CloudCompletionProvider::new(
|
||||
model.clone(),
|
||||
client.clone(),
|
||||
settings_version,
|
||||
cx,
|
||||
@@ -109,13 +133,13 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
(
|
||||
_,
|
||||
AssistantProvider::OpenAi {
|
||||
default_model,
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
},
|
||||
) => {
|
||||
*provider = CompletionProvider::OpenAi(OpenAiCompletionProvider::new(
|
||||
default_model.clone(),
|
||||
model.clone(),
|
||||
api_url.clone(),
|
||||
client.http_client(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
@@ -125,19 +149,36 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
(
|
||||
_,
|
||||
AssistantProvider::Anthropic {
|
||||
default_model,
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
},
|
||||
) => {
|
||||
*provider = CompletionProvider::Anthropic(AnthropicCompletionProvider::new(
|
||||
default_model.clone(),
|
||||
model.clone(),
|
||||
api_url.clone(),
|
||||
client.http_client(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
settings_version,
|
||||
));
|
||||
}
|
||||
(
|
||||
_,
|
||||
AssistantProvider::Ollama {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
},
|
||||
) => {
|
||||
*provider = CompletionProvider::Ollama(OllamaCompletionProvider::new(
|
||||
model.clone(),
|
||||
api_url.clone(),
|
||||
client.http_client(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
settings_version,
|
||||
cx,
|
||||
));
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -147,9 +188,10 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
pub enum CompletionProvider {
|
||||
OpenAi(OpenAiCompletionProvider),
|
||||
Anthropic(AnthropicCompletionProvider),
|
||||
ZedDotDev(ZedDotDevCompletionProvider),
|
||||
Cloud(CloudCompletionProvider),
|
||||
#[cfg(test)]
|
||||
Fake(FakeCompletionProvider),
|
||||
Ollama(OllamaCompletionProvider),
|
||||
}
|
||||
|
||||
impl gpui::Global for CompletionProvider {}
|
||||
@@ -159,11 +201,35 @@ impl CompletionProvider {
|
||||
cx.global::<Self>()
|
||||
}
|
||||
|
||||
pub fn available_models(&self) -> Vec<LanguageModel> {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider
|
||||
.available_models()
|
||||
.map(LanguageModel::OpenAi)
|
||||
.collect(),
|
||||
CompletionProvider::Anthropic(provider) => provider
|
||||
.available_models()
|
||||
.map(LanguageModel::Anthropic)
|
||||
.collect(),
|
||||
CompletionProvider::Cloud(provider) => provider
|
||||
.available_models()
|
||||
.map(LanguageModel::Cloud)
|
||||
.collect(),
|
||||
CompletionProvider::Ollama(provider) => provider
|
||||
.available_models()
|
||||
.map(|model| LanguageModel::Ollama(model.clone()))
|
||||
.collect(),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn settings_version(&self) -> usize {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider.settings_version(),
|
||||
CompletionProvider::Anthropic(provider) => provider.settings_version(),
|
||||
CompletionProvider::ZedDotDev(provider) => provider.settings_version(),
|
||||
CompletionProvider::Cloud(provider) => provider.settings_version(),
|
||||
CompletionProvider::Ollama(provider) => provider.settings_version(),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => unimplemented!(),
|
||||
}
|
||||
@@ -173,7 +239,8 @@ impl CompletionProvider {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider.is_authenticated(),
|
||||
CompletionProvider::Anthropic(provider) => provider.is_authenticated(),
|
||||
CompletionProvider::ZedDotDev(provider) => provider.is_authenticated(),
|
||||
CompletionProvider::Cloud(provider) => provider.is_authenticated(),
|
||||
CompletionProvider::Ollama(provider) => provider.is_authenticated(),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => true,
|
||||
}
|
||||
@@ -183,7 +250,8 @@ impl CompletionProvider {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider.authenticate(cx),
|
||||
CompletionProvider::Anthropic(provider) => provider.authenticate(cx),
|
||||
CompletionProvider::ZedDotDev(provider) => provider.authenticate(cx),
|
||||
CompletionProvider::Cloud(provider) => provider.authenticate(cx),
|
||||
CompletionProvider::Ollama(provider) => provider.authenticate(cx),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => Task::ready(Ok(())),
|
||||
}
|
||||
@@ -193,7 +261,8 @@ impl CompletionProvider {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider.authentication_prompt(cx),
|
||||
CompletionProvider::Anthropic(provider) => provider.authentication_prompt(cx),
|
||||
CompletionProvider::ZedDotDev(provider) => provider.authentication_prompt(cx),
|
||||
CompletionProvider::Cloud(provider) => provider.authentication_prompt(cx),
|
||||
CompletionProvider::Ollama(provider) => provider.authentication_prompt(cx),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => unimplemented!(),
|
||||
}
|
||||
@@ -203,23 +272,21 @@ impl CompletionProvider {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider.reset_credentials(cx),
|
||||
CompletionProvider::Anthropic(provider) => provider.reset_credentials(cx),
|
||||
CompletionProvider::ZedDotDev(_) => Task::ready(Ok(())),
|
||||
CompletionProvider::Cloud(_) => Task::ready(Ok(())),
|
||||
CompletionProvider::Ollama(provider) => provider.reset_credentials(cx),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => Task::ready(Ok(())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_model(&self) -> LanguageModel {
|
||||
pub fn model(&self) -> LanguageModel {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => LanguageModel::OpenAi(provider.default_model()),
|
||||
CompletionProvider::Anthropic(provider) => {
|
||||
LanguageModel::Anthropic(provider.default_model())
|
||||
}
|
||||
CompletionProvider::ZedDotDev(provider) => {
|
||||
LanguageModel::ZedDotDev(provider.default_model())
|
||||
}
|
||||
CompletionProvider::OpenAi(provider) => LanguageModel::OpenAi(provider.model()),
|
||||
CompletionProvider::Anthropic(provider) => LanguageModel::Anthropic(provider.model()),
|
||||
CompletionProvider::Cloud(provider) => LanguageModel::Cloud(provider.model()),
|
||||
CompletionProvider::Ollama(provider) => LanguageModel::Ollama(provider.model()),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => unimplemented!(),
|
||||
CompletionProvider::Fake(_) => LanguageModel::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,7 +298,8 @@ impl CompletionProvider {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider.count_tokens(request, cx),
|
||||
CompletionProvider::Anthropic(provider) => provider.count_tokens(request, cx),
|
||||
CompletionProvider::ZedDotDev(provider) => provider.count_tokens(request, cx),
|
||||
CompletionProvider::Cloud(provider) => provider.count_tokens(request, cx),
|
||||
CompletionProvider::Ollama(provider) => provider.count_tokens(request, cx),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => futures::FutureExt::boxed(futures::future::ready(Ok(0))),
|
||||
}
|
||||
@@ -244,7 +312,8 @@ impl CompletionProvider {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider.complete(request),
|
||||
CompletionProvider::Anthropic(provider) => provider.complete(request),
|
||||
CompletionProvider::ZedDotDev(provider) => provider.complete(request),
|
||||
CompletionProvider::Cloud(provider) => provider.complete(request),
|
||||
CompletionProvider::Ollama(provider) => provider.complete(request),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(provider) => provider.complete(),
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::count_open_ai_tokens;
|
||||
use crate::{
|
||||
assistant_settings::AnthropicModel, CompletionProvider, LanguageModel, LanguageModelRequest,
|
||||
Role,
|
||||
};
|
||||
use anthropic::{stream_completion, Request, RequestMessage, Role as AnthropicRole};
|
||||
use crate::{count_open_ai_tokens, LanguageModelRequestMessage};
|
||||
use anthropic::{stream_completion, Request, RequestMessage};
|
||||
use anyhow::{anyhow, Result};
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
|
||||
@@ -12,6 +12,7 @@ use http::HttpClient;
|
||||
use settings::Settings;
|
||||
use std::time::Duration;
|
||||
use std::{env, sync::Arc};
|
||||
use strum::IntoEnumIterator;
|
||||
use theme::ThemeSettings;
|
||||
use ui::prelude::*;
|
||||
use util::ResultExt;
|
||||
@@ -19,7 +20,7 @@ use util::ResultExt;
|
||||
pub struct AnthropicCompletionProvider {
|
||||
api_key: Option<String>,
|
||||
api_url: String,
|
||||
default_model: AnthropicModel,
|
||||
model: AnthropicModel,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
low_speed_timeout: Option<Duration>,
|
||||
settings_version: usize,
|
||||
@@ -27,7 +28,7 @@ pub struct AnthropicCompletionProvider {
|
||||
|
||||
impl AnthropicCompletionProvider {
|
||||
pub fn new(
|
||||
default_model: AnthropicModel,
|
||||
model: AnthropicModel,
|
||||
api_url: String,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
low_speed_timeout: Option<Duration>,
|
||||
@@ -36,7 +37,7 @@ impl AnthropicCompletionProvider {
|
||||
Self {
|
||||
api_key: None,
|
||||
api_url,
|
||||
default_model,
|
||||
model,
|
||||
http_client,
|
||||
low_speed_timeout,
|
||||
settings_version,
|
||||
@@ -45,17 +46,21 @@ impl AnthropicCompletionProvider {
|
||||
|
||||
pub fn update(
|
||||
&mut self,
|
||||
default_model: AnthropicModel,
|
||||
model: AnthropicModel,
|
||||
api_url: String,
|
||||
low_speed_timeout: Option<Duration>,
|
||||
settings_version: usize,
|
||||
) {
|
||||
self.default_model = default_model;
|
||||
self.model = model;
|
||||
self.api_url = api_url;
|
||||
self.low_speed_timeout = low_speed_timeout;
|
||||
self.settings_version = settings_version;
|
||||
}
|
||||
|
||||
pub fn available_models(&self) -> impl Iterator<Item = AnthropicModel> {
|
||||
AnthropicModel::iter()
|
||||
}
|
||||
|
||||
pub fn settings_version(&self) -> usize {
|
||||
self.settings_version
|
||||
}
|
||||
@@ -105,8 +110,8 @@ impl AnthropicCompletionProvider {
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn default_model(&self) -> AnthropicModel {
|
||||
self.default_model.clone()
|
||||
pub fn model(&self) -> AnthropicModel {
|
||||
self.model.clone()
|
||||
}
|
||||
|
||||
pub fn count_tokens(
|
||||
@@ -162,53 +167,37 @@ impl AnthropicCompletionProvider {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn to_anthropic_request(&self, request: LanguageModelRequest) -> Request {
|
||||
fn to_anthropic_request(&self, mut request: LanguageModelRequest) -> Request {
|
||||
preprocess_anthropic_request(&mut request);
|
||||
|
||||
let model = match request.model {
|
||||
LanguageModel::Anthropic(model) => model,
|
||||
_ => self.default_model(),
|
||||
_ => self.model(),
|
||||
};
|
||||
|
||||
let mut system_message = String::new();
|
||||
|
||||
let mut messages: Vec<RequestMessage> = Vec::new();
|
||||
for message in request.messages {
|
||||
if message.content.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
match message.role {
|
||||
Role::User | Role::Assistant => {
|
||||
let role = match message.role {
|
||||
Role::User => AnthropicRole::User,
|
||||
Role::Assistant => AnthropicRole::Assistant,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
if let Some(last_message) = messages.last_mut() {
|
||||
if last_message.role == role {
|
||||
last_message.content.push_str("\n\n");
|
||||
last_message.content.push_str(&message.content);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
messages.push(RequestMessage {
|
||||
role,
|
||||
content: message.content,
|
||||
});
|
||||
}
|
||||
Role::System => {
|
||||
if !system_message.is_empty() {
|
||||
system_message.push_str("\n\n");
|
||||
}
|
||||
system_message.push_str(&message.content);
|
||||
}
|
||||
}
|
||||
if request
|
||||
.messages
|
||||
.first()
|
||||
.map_or(false, |message| message.role == Role::System)
|
||||
{
|
||||
system_message = request.messages.remove(0).content;
|
||||
}
|
||||
|
||||
Request {
|
||||
model,
|
||||
messages,
|
||||
messages: request
|
||||
.messages
|
||||
.iter()
|
||||
.map(|msg| RequestMessage {
|
||||
role: match msg.role {
|
||||
Role::User => anthropic::Role::User,
|
||||
Role::Assistant => anthropic::Role::Assistant,
|
||||
Role::System => unreachable!("filtered out by preprocess_request"),
|
||||
},
|
||||
content: msg.content.clone(),
|
||||
})
|
||||
.collect(),
|
||||
stream: true,
|
||||
system: system_message,
|
||||
max_tokens: 4092,
|
||||
@@ -216,6 +205,49 @@ impl AnthropicCompletionProvider {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn preprocess_anthropic_request(request: &mut LanguageModelRequest) {
|
||||
let mut new_messages: Vec<LanguageModelRequestMessage> = Vec::new();
|
||||
let mut system_message = String::new();
|
||||
|
||||
for message in request.messages.drain(..) {
|
||||
if message.content.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
match message.role {
|
||||
Role::User | Role::Assistant => {
|
||||
if let Some(last_message) = new_messages.last_mut() {
|
||||
if last_message.role == message.role {
|
||||
last_message.content.push_str("\n\n");
|
||||
last_message.content.push_str(&message.content);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
new_messages.push(message);
|
||||
}
|
||||
Role::System => {
|
||||
if !system_message.is_empty() {
|
||||
system_message.push_str("\n\n");
|
||||
}
|
||||
system_message.push_str(&message.content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !system_message.is_empty() {
|
||||
request.messages.insert(
|
||||
0,
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::System,
|
||||
content: system_message,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
request.messages = new_messages;
|
||||
}
|
||||
|
||||
struct AuthenticationPrompt {
|
||||
api_key: View<Editor>,
|
||||
api_url: String,
|
||||
@@ -317,7 +349,7 @@ impl Render for AuthenticationPrompt {
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Label::new("Click on").size(LabelSize::Small))
|
||||
.child(Icon::new(IconName::Ai).size(IconSize::XSmall))
|
||||
.child(Icon::new(IconName::ZedAssistant).size(IconSize::XSmall))
|
||||
.child(
|
||||
Label::new("in the status bar to close this panel.").size(LabelSize::Small),
|
||||
),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
assistant_settings::ZedDotDevModel, count_open_ai_tokens, CompletionProvider, LanguageModel,
|
||||
assistant_settings::CloudModel, count_open_ai_tokens, CompletionProvider, LanguageModel,
|
||||
LanguageModelRequest,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
@@ -7,19 +7,20 @@ use client::{proto, Client};
|
||||
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt, TryFutureExt};
|
||||
use gpui::{AnyView, AppContext, Task};
|
||||
use std::{future, sync::Arc};
|
||||
use strum::IntoEnumIterator;
|
||||
use ui::prelude::*;
|
||||
|
||||
pub struct ZedDotDevCompletionProvider {
|
||||
pub struct CloudCompletionProvider {
|
||||
client: Arc<Client>,
|
||||
default_model: ZedDotDevModel,
|
||||
model: CloudModel,
|
||||
settings_version: usize,
|
||||
status: client::Status,
|
||||
_maintain_client_status: Task<()>,
|
||||
}
|
||||
|
||||
impl ZedDotDevCompletionProvider {
|
||||
impl CloudCompletionProvider {
|
||||
pub fn new(
|
||||
default_model: ZedDotDevModel,
|
||||
model: CloudModel,
|
||||
client: Arc<Client>,
|
||||
settings_version: usize,
|
||||
cx: &mut AppContext,
|
||||
@@ -29,7 +30,7 @@ impl ZedDotDevCompletionProvider {
|
||||
let maintain_client_status = cx.spawn(|mut cx| async move {
|
||||
while let Some(status) = status_rx.next().await {
|
||||
let _ = cx.update_global::<CompletionProvider, _>(|provider, _cx| {
|
||||
if let CompletionProvider::ZedDotDev(provider) = provider {
|
||||
if let CompletionProvider::Cloud(provider) = provider {
|
||||
provider.status = status;
|
||||
} else {
|
||||
unreachable!()
|
||||
@@ -39,24 +40,39 @@ impl ZedDotDevCompletionProvider {
|
||||
});
|
||||
Self {
|
||||
client,
|
||||
default_model,
|
||||
model,
|
||||
settings_version,
|
||||
status,
|
||||
_maintain_client_status: maintain_client_status,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, default_model: ZedDotDevModel, settings_version: usize) {
|
||||
self.default_model = default_model;
|
||||
pub fn update(&mut self, model: CloudModel, settings_version: usize) {
|
||||
self.model = model;
|
||||
self.settings_version = settings_version;
|
||||
}
|
||||
|
||||
pub fn available_models(&self) -> impl Iterator<Item = CloudModel> {
|
||||
let mut custom_model = if let CloudModel::Custom(custom_model) = self.model.clone() {
|
||||
Some(custom_model)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
CloudModel::iter().filter_map(move |model| {
|
||||
if let CloudModel::Custom(_) = model {
|
||||
Some(CloudModel::Custom(custom_model.take()?))
|
||||
} else {
|
||||
Some(model)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn settings_version(&self) -> usize {
|
||||
self.settings_version
|
||||
}
|
||||
|
||||
pub fn default_model(&self) -> ZedDotDevModel {
|
||||
self.default_model.clone()
|
||||
pub fn model(&self) -> CloudModel {
|
||||
self.model.clone()
|
||||
}
|
||||
|
||||
pub fn is_authenticated(&self) -> bool {
|
||||
@@ -78,21 +94,22 @@ impl ZedDotDevCompletionProvider {
|
||||
cx: &AppContext,
|
||||
) -> BoxFuture<'static, Result<usize>> {
|
||||
match request.model {
|
||||
LanguageModel::ZedDotDev(ZedDotDevModel::Gpt4)
|
||||
| LanguageModel::ZedDotDev(ZedDotDevModel::Gpt4Turbo)
|
||||
| LanguageModel::ZedDotDev(ZedDotDevModel::Gpt4Omni)
|
||||
| LanguageModel::ZedDotDev(ZedDotDevModel::Gpt3Point5Turbo) => {
|
||||
LanguageModel::Cloud(CloudModel::Gpt4)
|
||||
| LanguageModel::Cloud(CloudModel::Gpt4Turbo)
|
||||
| LanguageModel::Cloud(CloudModel::Gpt4Omni)
|
||||
| LanguageModel::Cloud(CloudModel::Gpt3Point5Turbo) => {
|
||||
count_open_ai_tokens(request, cx.background_executor())
|
||||
}
|
||||
LanguageModel::ZedDotDev(
|
||||
ZedDotDevModel::Claude3Opus
|
||||
| ZedDotDevModel::Claude3Sonnet
|
||||
| ZedDotDevModel::Claude3Haiku,
|
||||
LanguageModel::Cloud(
|
||||
CloudModel::Claude3_5Sonnet
|
||||
| CloudModel::Claude3Opus
|
||||
| CloudModel::Claude3Sonnet
|
||||
| CloudModel::Claude3Haiku,
|
||||
) => {
|
||||
// Can't find a tokenizer for Claude 3, so for now just use the same as OpenAI's as an approximation.
|
||||
count_open_ai_tokens(request, cx.background_executor())
|
||||
}
|
||||
LanguageModel::ZedDotDev(ZedDotDevModel::Custom(model)) => {
|
||||
LanguageModel::Cloud(CloudModel::Custom(model)) => {
|
||||
let request = self.client.request(proto::CountTokensWithLanguageModel {
|
||||
model,
|
||||
messages: request
|
||||
@@ -113,8 +130,10 @@ impl ZedDotDevCompletionProvider {
|
||||
|
||||
pub fn complete(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
mut request: LanguageModelRequest,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
|
||||
request.preprocess();
|
||||
|
||||
let request = proto::CompleteWithLanguageModel {
|
||||
model: request.model.id().to_string(),
|
||||
messages: request
|
||||
348
crates/assistant/src/completion_provider/ollama.rs
Normal file
@@ -0,0 +1,348 @@
|
||||
use crate::{
|
||||
assistant_settings::OllamaModel, CompletionProvider, LanguageModel, LanguageModelRequest, Role,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use futures::StreamExt as _;
|
||||
use futures::{future::BoxFuture, stream::BoxStream, FutureExt};
|
||||
use gpui::{AnyView, AppContext, Task};
|
||||
use http::HttpClient;
|
||||
use ollama::{
|
||||
get_models, preload_model, stream_chat_completion, ChatMessage, ChatOptions, ChatRequest,
|
||||
Role as OllamaRole,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use ui::{prelude::*, ButtonLike, ElevationIndex};
|
||||
|
||||
const OLLAMA_DOWNLOAD_URL: &str = "https://ollama.com/download";
|
||||
const OLLAMA_LIBRARY_URL: &str = "https://ollama.com/library";
|
||||
|
||||
pub struct OllamaCompletionProvider {
|
||||
api_url: String,
|
||||
model: OllamaModel,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
low_speed_timeout: Option<Duration>,
|
||||
settings_version: usize,
|
||||
available_models: Vec<OllamaModel>,
|
||||
}
|
||||
|
||||
impl OllamaCompletionProvider {
|
||||
pub fn new(
|
||||
model: OllamaModel,
|
||||
api_url: String,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
low_speed_timeout: Option<Duration>,
|
||||
settings_version: usize,
|
||||
cx: &AppContext,
|
||||
) -> Self {
|
||||
cx.spawn({
|
||||
let api_url = api_url.clone();
|
||||
let client = http_client.clone();
|
||||
let model = model.name.clone();
|
||||
|
||||
|_| async move {
|
||||
if model.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
preload_model(client.as_ref(), &api_url, &model).await
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
Self {
|
||||
api_url,
|
||||
model,
|
||||
http_client,
|
||||
low_speed_timeout,
|
||||
settings_version,
|
||||
available_models: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
&mut self,
|
||||
model: OllamaModel,
|
||||
api_url: String,
|
||||
low_speed_timeout: Option<Duration>,
|
||||
settings_version: usize,
|
||||
cx: &AppContext,
|
||||
) {
|
||||
cx.spawn({
|
||||
let api_url = api_url.clone();
|
||||
let client = self.http_client.clone();
|
||||
let model = model.name.clone();
|
||||
|
||||
|_| async move { preload_model(client.as_ref(), &api_url, &model).await }
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
if model.name.is_empty() {
|
||||
self.select_first_available_model()
|
||||
} else {
|
||||
self.model = model;
|
||||
}
|
||||
|
||||
self.api_url = api_url;
|
||||
self.low_speed_timeout = low_speed_timeout;
|
||||
self.settings_version = settings_version;
|
||||
}
|
||||
|
||||
pub fn available_models(&self) -> impl Iterator<Item = &OllamaModel> {
|
||||
self.available_models.iter()
|
||||
}
|
||||
|
||||
pub fn select_first_available_model(&mut self) {
|
||||
if let Some(model) = self.available_models.first() {
|
||||
self.model = model.clone();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn settings_version(&self) -> usize {
|
||||
self.settings_version
|
||||
}
|
||||
|
||||
pub fn is_authenticated(&self) -> bool {
|
||||
!self.available_models.is_empty()
|
||||
}
|
||||
|
||||
pub fn authenticate(&self, cx: &AppContext) -> Task<Result<()>> {
|
||||
if self.is_authenticated() {
|
||||
Task::ready(Ok(()))
|
||||
} else {
|
||||
self.fetch_models(cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_credentials(&self, cx: &AppContext) -> Task<Result<()>> {
|
||||
self.fetch_models(cx)
|
||||
}
|
||||
|
||||
pub fn fetch_models(&self, cx: &AppContext) -> Task<Result<()>> {
|
||||
let http_client = self.http_client.clone();
|
||||
let api_url = self.api_url.clone();
|
||||
|
||||
// As a proxy for the server being "authenticated", we'll check if its up by fetching the models
|
||||
cx.spawn(|mut cx| async move {
|
||||
let models = get_models(http_client.as_ref(), &api_url, None).await?;
|
||||
|
||||
let mut models: Vec<OllamaModel> = models
|
||||
.into_iter()
|
||||
// Since there is no metadata from the Ollama API
|
||||
// indicating which models are embedding models,
|
||||
// simply filter out models with "-embed" in their name
|
||||
.filter(|model| !model.name.contains("-embed"))
|
||||
.map(|model| OllamaModel::new(&model.name))
|
||||
.collect();
|
||||
|
||||
models.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
|
||||
cx.update_global::<CompletionProvider, _>(|provider, _cx| {
|
||||
if let CompletionProvider::Ollama(provider) = provider {
|
||||
provider.available_models = models;
|
||||
|
||||
if !provider.available_models.is_empty() && provider.model.name.is_empty() {
|
||||
provider.select_first_available_model()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
|
||||
let fetch_models = Box::new(move |cx: &mut WindowContext| {
|
||||
cx.update_global::<CompletionProvider, _>(|provider, cx| {
|
||||
if let CompletionProvider::Ollama(provider) = provider {
|
||||
provider.fetch_models(cx)
|
||||
} else {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
cx.new_view(|cx| DownloadOllamaMessage::new(fetch_models, cx))
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn model(&self) -> OllamaModel {
|
||||
self.model.clone()
|
||||
}
|
||||
|
||||
pub fn count_tokens(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
_cx: &AppContext,
|
||||
) -> BoxFuture<'static, Result<usize>> {
|
||||
// There is no endpoint for this _yet_ in Ollama
|
||||
// see: https://github.com/ollama/ollama/issues/1716 and https://github.com/ollama/ollama/issues/3582
|
||||
let token_count = request
|
||||
.messages
|
||||
.iter()
|
||||
.map(|msg| msg.content.chars().count())
|
||||
.sum::<usize>()
|
||||
/ 4;
|
||||
|
||||
async move { Ok(token_count) }.boxed()
|
||||
}
|
||||
|
||||
pub fn complete(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
|
||||
let request = self.to_ollama_request(request);
|
||||
|
||||
let http_client = self.http_client.clone();
|
||||
let api_url = self.api_url.clone();
|
||||
let low_speed_timeout = self.low_speed_timeout;
|
||||
async move {
|
||||
let request =
|
||||
stream_chat_completion(http_client.as_ref(), &api_url, request, low_speed_timeout);
|
||||
let response = request.await?;
|
||||
let stream = response
|
||||
.filter_map(|response| async move {
|
||||
match response {
|
||||
Ok(delta) => {
|
||||
let content = match delta.message {
|
||||
ChatMessage::User { content } => content,
|
||||
ChatMessage::Assistant { content } => content,
|
||||
ChatMessage::System { content } => content,
|
||||
};
|
||||
Some(Ok(content))
|
||||
}
|
||||
Err(error) => Some(Err(error)),
|
||||
}
|
||||
})
|
||||
.boxed();
|
||||
Ok(stream)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn to_ollama_request(&self, request: LanguageModelRequest) -> ChatRequest {
|
||||
let model = match request.model {
|
||||
LanguageModel::Ollama(model) => model,
|
||||
_ => self.model(),
|
||||
};
|
||||
|
||||
ChatRequest {
|
||||
model: model.name,
|
||||
messages: request
|
||||
.messages
|
||||
.into_iter()
|
||||
.map(|msg| match msg.role {
|
||||
Role::User => ChatMessage::User {
|
||||
content: msg.content,
|
||||
},
|
||||
Role::Assistant => ChatMessage::Assistant {
|
||||
content: msg.content,
|
||||
},
|
||||
Role::System => ChatMessage::System {
|
||||
content: msg.content,
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
keep_alive: model.keep_alive.unwrap_or_default(),
|
||||
stream: true,
|
||||
options: Some(ChatOptions {
|
||||
num_ctx: Some(model.max_tokens),
|
||||
stop: Some(request.stop),
|
||||
temperature: Some(request.temperature),
|
||||
..Default::default()
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Role> for ollama::Role {
|
||||
fn from(val: Role) -> Self {
|
||||
match val {
|
||||
Role::User => OllamaRole::User,
|
||||
Role::Assistant => OllamaRole::Assistant,
|
||||
Role::System => OllamaRole::System,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DownloadOllamaMessage {
|
||||
retry_connection: Box<dyn Fn(&mut WindowContext) -> Task<Result<()>>>,
|
||||
}
|
||||
|
||||
impl DownloadOllamaMessage {
|
||||
pub fn new(
|
||||
retry_connection: Box<dyn Fn(&mut WindowContext) -> Task<Result<()>>>,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
Self { retry_connection }
|
||||
}
|
||||
|
||||
fn render_download_button(&self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
ButtonLike::new("download_ollama_button")
|
||||
.style(ButtonStyle::Filled)
|
||||
.size(ButtonSize::Large)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.child(Label::new("Get Ollama"))
|
||||
.on_click(move |_, cx| cx.open_url(OLLAMA_DOWNLOAD_URL))
|
||||
}
|
||||
|
||||
fn render_retry_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
ButtonLike::new("retry_ollama_models")
|
||||
.style(ButtonStyle::Filled)
|
||||
.size(ButtonSize::Large)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.child(Label::new("Retry"))
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
let connected = (this.retry_connection)(cx);
|
||||
|
||||
cx.spawn(|_this, _cx| async move {
|
||||
connected.await?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx)
|
||||
}))
|
||||
}
|
||||
|
||||
fn render_next_steps(&self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.p_4()
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.child(
|
||||
Label::new("Once Ollama is on your machine, make sure to download a model or two.")
|
||||
.size(LabelSize::Large),
|
||||
)
|
||||
.child(
|
||||
h_flex().w_full().p_4().justify_center().gap_2().child(
|
||||
ButtonLike::new("view-models")
|
||||
.style(ButtonStyle::Filled)
|
||||
.size(ButtonSize::Large)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.child(Label::new("View Available Models"))
|
||||
.on_click(move |_, cx| cx.open_url(OLLAMA_LIBRARY_URL)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for DownloadOllamaMessage {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.p_4()
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.child(Label::new("To use Ollama models via the assistant, Ollama must be running on your machine with at least one model downloaded.").size(LabelSize::Large))
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.p_4()
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
self.render_download_button(cx)
|
||||
)
|
||||
.child(
|
||||
self.render_retry_button(cx)
|
||||
)
|
||||
)
|
||||
.child(self.render_next_steps(cx))
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::assistant_settings::ZedDotDevModel;
|
||||
use crate::assistant_settings::CloudModel;
|
||||
use crate::{
|
||||
assistant_settings::OpenAiModel, CompletionProvider, LanguageModel, LanguageModelRequest, Role,
|
||||
};
|
||||
@@ -11,6 +11,7 @@ use open_ai::{stream_completion, Request, RequestMessage, Role as OpenAiRole};
|
||||
use settings::Settings;
|
||||
use std::time::Duration;
|
||||
use std::{env, sync::Arc};
|
||||
use strum::IntoEnumIterator;
|
||||
use theme::ThemeSettings;
|
||||
use ui::prelude::*;
|
||||
use util::ResultExt;
|
||||
@@ -18,7 +19,7 @@ use util::ResultExt;
|
||||
pub struct OpenAiCompletionProvider {
|
||||
api_key: Option<String>,
|
||||
api_url: String,
|
||||
default_model: OpenAiModel,
|
||||
model: OpenAiModel,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
low_speed_timeout: Option<Duration>,
|
||||
settings_version: usize,
|
||||
@@ -26,7 +27,7 @@ pub struct OpenAiCompletionProvider {
|
||||
|
||||
impl OpenAiCompletionProvider {
|
||||
pub fn new(
|
||||
default_model: OpenAiModel,
|
||||
model: OpenAiModel,
|
||||
api_url: String,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
low_speed_timeout: Option<Duration>,
|
||||
@@ -35,7 +36,7 @@ impl OpenAiCompletionProvider {
|
||||
Self {
|
||||
api_key: None,
|
||||
api_url,
|
||||
default_model,
|
||||
model,
|
||||
http_client,
|
||||
low_speed_timeout,
|
||||
settings_version,
|
||||
@@ -44,17 +45,21 @@ impl OpenAiCompletionProvider {
|
||||
|
||||
pub fn update(
|
||||
&mut self,
|
||||
default_model: OpenAiModel,
|
||||
model: OpenAiModel,
|
||||
api_url: String,
|
||||
low_speed_timeout: Option<Duration>,
|
||||
settings_version: usize,
|
||||
) {
|
||||
self.default_model = default_model;
|
||||
self.model = model;
|
||||
self.api_url = api_url;
|
||||
self.low_speed_timeout = low_speed_timeout;
|
||||
self.settings_version = settings_version;
|
||||
}
|
||||
|
||||
pub fn available_models(&self) -> impl Iterator<Item = OpenAiModel> {
|
||||
OpenAiModel::iter()
|
||||
}
|
||||
|
||||
pub fn settings_version(&self) -> usize {
|
||||
self.settings_version
|
||||
}
|
||||
@@ -104,8 +109,8 @@ impl OpenAiCompletionProvider {
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn default_model(&self) -> OpenAiModel {
|
||||
self.default_model.clone()
|
||||
pub fn model(&self) -> OpenAiModel {
|
||||
self.model.clone()
|
||||
}
|
||||
|
||||
pub fn count_tokens(
|
||||
@@ -152,7 +157,7 @@ impl OpenAiCompletionProvider {
|
||||
fn to_open_ai_request(&self, request: LanguageModelRequest) -> Request {
|
||||
let model = match request.model {
|
||||
LanguageModel::OpenAi(model) => model,
|
||||
_ => self.default_model(),
|
||||
_ => self.model(),
|
||||
};
|
||||
|
||||
Request {
|
||||
@@ -205,9 +210,10 @@ pub fn count_open_ai_tokens(
|
||||
|
||||
match request.model {
|
||||
LanguageModel::Anthropic(_)
|
||||
| LanguageModel::ZedDotDev(ZedDotDevModel::Claude3Opus)
|
||||
| LanguageModel::ZedDotDev(ZedDotDevModel::Claude3Sonnet)
|
||||
| LanguageModel::ZedDotDev(ZedDotDevModel::Claude3Haiku) => {
|
||||
| LanguageModel::Cloud(CloudModel::Claude3_5Sonnet)
|
||||
| LanguageModel::Cloud(CloudModel::Claude3Opus)
|
||||
| LanguageModel::Cloud(CloudModel::Claude3Sonnet)
|
||||
| LanguageModel::Cloud(CloudModel::Claude3Haiku) => {
|
||||
// Tiktoken doesn't yet support these models, so we manually use the
|
||||
// same tokenizer as GPT-4.
|
||||
tiktoken_rs::num_tokens_from_messages("gpt-4", &messages)
|
||||
@@ -331,7 +337,7 @@ impl Render for AuthenticationPrompt {
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Label::new("Click on").size(LabelSize::Small))
|
||||
.child(Icon::new(IconName::Ai).size(IconSize::XSmall))
|
||||
.child(Icon::new(IconName::ZedAssistant).size(IconSize::XSmall))
|
||||
.child(
|
||||
Label::new("in the status bar to close this panel.").size(LabelSize::Small),
|
||||
),
|
||||
|
||||
225
crates/assistant/src/context_store.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
use crate::{assistant_settings::OpenAiModel, MessageId, MessageMetadata};
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::SlashCommandOutputSection;
|
||||
use collections::HashMap;
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{AppContext, Model, ModelContext, Task};
|
||||
use paths::contexts_dir;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc, time::Duration};
|
||||
use ui::Context;
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedMessage {
|
||||
pub id: MessageId,
|
||||
pub start: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedContext {
|
||||
pub id: Option<String>,
|
||||
pub zed: String,
|
||||
pub version: String,
|
||||
pub text: String,
|
||||
pub messages: Vec<SavedMessage>,
|
||||
pub message_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
pub summary: String,
|
||||
pub slash_command_output_sections: Vec<SlashCommandOutputSection<usize>>,
|
||||
}
|
||||
|
||||
impl SavedContext {
|
||||
pub const VERSION: &'static str = "0.3.0";
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedContextV0_2_0 {
|
||||
pub id: Option<String>,
|
||||
pub zed: String,
|
||||
pub version: String,
|
||||
pub text: String,
|
||||
pub messages: Vec<SavedMessage>,
|
||||
pub message_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
pub summary: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SavedContextV0_1_0 {
|
||||
id: Option<String>,
|
||||
zed: String,
|
||||
version: String,
|
||||
text: String,
|
||||
messages: Vec<SavedMessage>,
|
||||
message_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
summary: String,
|
||||
api_url: Option<String>,
|
||||
model: OpenAiModel,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SavedContextMetadata {
|
||||
pub title: String,
|
||||
pub path: PathBuf,
|
||||
pub mtime: chrono::DateTime<chrono::Local>,
|
||||
}
|
||||
|
||||
pub struct ContextStore {
|
||||
contexts_metadata: Vec<SavedContextMetadata>,
|
||||
fs: Arc<dyn Fs>,
|
||||
_watch_updates: Task<Option<()>>,
|
||||
}
|
||||
|
||||
impl ContextStore {
|
||||
pub fn new(fs: Arc<dyn Fs>, cx: &mut AppContext) -> Task<Result<Model<Self>>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
const CONTEXT_WATCH_DURATION: Duration = Duration::from_millis(100);
|
||||
let (mut events, _) = fs.watch(contexts_dir(), CONTEXT_WATCH_DURATION).await;
|
||||
|
||||
let this = cx.new_model(|cx: &mut ModelContext<Self>| Self {
|
||||
contexts_metadata: Vec::new(),
|
||||
fs,
|
||||
_watch_updates: cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
while events.next().await.is_some() {
|
||||
this.update(&mut cx, |this, cx| this.reload(cx))?
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
}),
|
||||
})?;
|
||||
this.update(&mut cx, |this, cx| this.reload(cx))?
|
||||
.await
|
||||
.log_err();
|
||||
Ok(this)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load(&self, path: PathBuf, cx: &AppContext) -> Task<Result<SavedContext>> {
|
||||
let fs = self.fs.clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
let saved_context = fs.load(&path).await?;
|
||||
let saved_context_json = serde_json::from_str::<serde_json::Value>(&saved_context)?;
|
||||
match saved_context_json
|
||||
.get("version")
|
||||
.ok_or_else(|| anyhow!("version not found"))?
|
||||
{
|
||||
serde_json::Value::String(version) => match version.as_str() {
|
||||
SavedContext::VERSION => {
|
||||
Ok(serde_json::from_value::<SavedContext>(saved_context_json)?)
|
||||
}
|
||||
"0.2.0" => {
|
||||
let saved_context =
|
||||
serde_json::from_value::<SavedContextV0_2_0>(saved_context_json)?;
|
||||
Ok(SavedContext {
|
||||
id: saved_context.id,
|
||||
zed: saved_context.zed,
|
||||
version: saved_context.version,
|
||||
text: saved_context.text,
|
||||
messages: saved_context.messages,
|
||||
message_metadata: saved_context.message_metadata,
|
||||
summary: saved_context.summary,
|
||||
slash_command_output_sections: Vec::new(),
|
||||
})
|
||||
}
|
||||
"0.1.0" => {
|
||||
let saved_context =
|
||||
serde_json::from_value::<SavedContextV0_1_0>(saved_context_json)?;
|
||||
Ok(SavedContext {
|
||||
id: saved_context.id,
|
||||
zed: saved_context.zed,
|
||||
version: saved_context.version,
|
||||
text: saved_context.text,
|
||||
messages: saved_context.messages,
|
||||
message_metadata: saved_context.message_metadata,
|
||||
summary: saved_context.summary,
|
||||
slash_command_output_sections: Vec::new(),
|
||||
})
|
||||
}
|
||||
_ => Err(anyhow!("unrecognized saved context version: {}", version)),
|
||||
},
|
||||
_ => Err(anyhow!("version not found on saved context")),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn search(&self, query: String, cx: &AppContext) -> Task<Vec<SavedContextMetadata>> {
|
||||
let metadata = self.contexts_metadata.clone();
|
||||
let executor = cx.background_executor().clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
if query.is_empty() {
|
||||
metadata
|
||||
} else {
|
||||
let candidates = metadata
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, metadata)| StringMatchCandidate::new(id, metadata.title.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
let matches = fuzzy::match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
false,
|
||||
100,
|
||||
&Default::default(),
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
|
||||
matches
|
||||
.into_iter()
|
||||
.map(|mat| metadata[mat.candidate_id].clone())
|
||||
.collect()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn reload(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
let fs = self.fs.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
fs.create_dir(contexts_dir()).await?;
|
||||
|
||||
let mut paths = fs.read_dir(contexts_dir()).await?;
|
||||
let mut contexts = Vec::<SavedContextMetadata>::new();
|
||||
while let Some(path) = paths.next().await {
|
||||
let path = path?;
|
||||
if path.extension() != Some(OsStr::new("json")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let pattern = r" - \d+.zed.json$";
|
||||
let re = Regex::new(pattern).unwrap();
|
||||
|
||||
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 contexts saved by the new assistant.
|
||||
if !re.is_match(file_name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(title) = re.replace(file_name, "").lines().next() {
|
||||
contexts.push(SavedContextMetadata {
|
||||
title: title.to_string(),
|
||||
path,
|
||||
mtime: metadata.mtime.into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
contexts.sort_unstable_by_key(|context| Reverse(context.mtime));
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.contexts_metadata = contexts;
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
2534
crates/assistant/src/inline_assistant.rs
Normal file
84
crates/assistant/src/model_selector.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{assistant_settings::AssistantSettings, CompletionProvider, ToggleModelSelector};
|
||||
use fs::Fs;
|
||||
use settings::update_settings_file;
|
||||
use ui::{prelude::*, ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ModelSelector {
|
||||
handle: PopoverMenuHandle<ContextMenu>,
|
||||
fs: Arc<dyn Fs>,
|
||||
}
|
||||
|
||||
impl ModelSelector {
|
||||
pub fn new(handle: PopoverMenuHandle<ContextMenu>, fs: Arc<dyn Fs>) -> Self {
|
||||
ModelSelector { handle, fs }
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ModelSelector {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
PopoverMenu::new("model-switcher")
|
||||
.with_handle(self.handle)
|
||||
.menu(move |cx| {
|
||||
ContextMenu::build(cx, |mut menu, cx| {
|
||||
for model in CompletionProvider::global(cx).available_models() {
|
||||
menu = menu.custom_entry(
|
||||
{
|
||||
let model = model.clone();
|
||||
move |_| Label::new(model.display_name()).into_any_element()
|
||||
},
|
||||
{
|
||||
let fs = self.fs.clone();
|
||||
let model = model.clone();
|
||||
move |cx| {
|
||||
let model = model.clone();
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings| settings.set_model(model),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
menu
|
||||
})
|
||||
.into()
|
||||
})
|
||||
.trigger(
|
||||
ButtonLike::new("active-model")
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
div()
|
||||
.overflow_x_hidden()
|
||||
.flex_grow()
|
||||
.whitespace_nowrap()
|
||||
.child(
|
||||
Label::new(
|
||||
CompletionProvider::global(cx).model().display_name(),
|
||||
)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
),
|
||||
)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::for_action("Change Model", &ToggleModelSelector, cx)
|
||||
}),
|
||||
)
|
||||
.anchor(gpui::AnchorCorner::BottomRight)
|
||||
}
|
||||
}
|
||||
1138
crates/assistant/src/prompt_library.rs
Normal file
@@ -1,7 +1,123 @@
|
||||
mod prompt;
|
||||
mod prompt_library;
|
||||
mod prompt_manager;
|
||||
use language::BufferSnapshot;
|
||||
use std::{fmt::Write, ops::Range};
|
||||
|
||||
pub use prompt::*;
|
||||
pub use prompt_library::*;
|
||||
pub use prompt_manager::*;
|
||||
pub fn generate_content_prompt(
|
||||
user_prompt: String,
|
||||
language_name: Option<&str>,
|
||||
buffer: BufferSnapshot,
|
||||
range: Range<usize>,
|
||||
project_name: Option<String>,
|
||||
) -> anyhow::Result<String> {
|
||||
let mut prompt = String::new();
|
||||
|
||||
let content_type = match language_name {
|
||||
None | Some("Markdown" | "Plain Text") => {
|
||||
writeln!(prompt, "You are an expert engineer.")?;
|
||||
"Text"
|
||||
}
|
||||
Some(language_name) => {
|
||||
writeln!(prompt, "You are an expert {language_name} engineer.")?;
|
||||
writeln!(
|
||||
prompt,
|
||||
"Your answer MUST always and only be valid {}.",
|
||||
language_name
|
||||
)?;
|
||||
"Code"
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(project_name) = project_name {
|
||||
writeln!(
|
||||
prompt,
|
||||
"You are currently working inside the '{project_name}' project in code editor Zed."
|
||||
)?;
|
||||
}
|
||||
|
||||
writeln!(
|
||||
prompt,
|
||||
"The user has the following file open in the editor:"
|
||||
)?;
|
||||
if range.is_empty() {
|
||||
write!(prompt, "```")?;
|
||||
if let Some(language_name) = language_name {
|
||||
write!(prompt, "{language_name}")?;
|
||||
}
|
||||
|
||||
for chunk in buffer.as_rope().chunks_in_range(0..range.start) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
prompt.push_str("<|CURSOR|>");
|
||||
for chunk in buffer.as_rope().chunks_in_range(range.start..buffer.len()) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
if !prompt.ends_with('\n') {
|
||||
prompt.push('\n');
|
||||
}
|
||||
writeln!(prompt, "```")?;
|
||||
prompt.push('\n');
|
||||
|
||||
writeln!(
|
||||
prompt,
|
||||
"Assume the cursor is located where the `<|CURSOR|>` span is."
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"{content_type} can't be replaced, so assume your answer will be inserted at the cursor.",
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Generate {content_type} based on the users prompt: {user_prompt}",
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
write!(prompt, "```")?;
|
||||
for chunk in buffer.as_rope().chunks() {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
if !prompt.ends_with('\n') {
|
||||
prompt.push('\n');
|
||||
}
|
||||
writeln!(prompt, "```")?;
|
||||
prompt.push('\n');
|
||||
|
||||
writeln!(
|
||||
prompt,
|
||||
"In particular, the following piece of text is selected:"
|
||||
)?;
|
||||
write!(prompt, "```")?;
|
||||
if let Some(language_name) = language_name {
|
||||
write!(prompt, "{language_name}")?;
|
||||
}
|
||||
prompt.push('\n');
|
||||
for chunk in buffer.text_for_range(range.clone()) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
if !prompt.ends_with('\n') {
|
||||
prompt.push('\n');
|
||||
}
|
||||
writeln!(prompt, "```")?;
|
||||
prompt.push('\n');
|
||||
|
||||
writeln!(
|
||||
prompt,
|
||||
"Modify the user's selected {content_type} based upon the users prompt: {user_prompt}"
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"You must reply with only the adjusted {content_type}, not the entire file."
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
writeln!(prompt, "Never make remarks about the output.").unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Do not return anything else, except the generated {content_type}."
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
Ok(prompt)
|
||||
}
|
||||
|
||||
@@ -1,360 +0,0 @@
|
||||
use fs::Fs;
|
||||
use language::BufferSnapshot;
|
||||
use std::{fmt::Write, ops::Range, path::PathBuf, sync::Arc};
|
||||
use ui::SharedString;
|
||||
use util::paths::PROMPTS_DIR;
|
||||
|
||||
use gray_matter::{engine::YAML, Matter};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::prompt_library::PromptId;
|
||||
|
||||
pub const PROMPT_DEFAULT_TITLE: &str = "Untitled Prompt";
|
||||
|
||||
fn standardize_value(value: String) -> String {
|
||||
value.replace(['\n', '\r', '"', '\''], "")
|
||||
}
|
||||
|
||||
fn slugify(input: String) -> String {
|
||||
let mut slug = String::new();
|
||||
for c in input.chars() {
|
||||
if c.is_alphanumeric() {
|
||||
slug.push(c.to_ascii_lowercase());
|
||||
} else if c.is_whitespace() {
|
||||
slug.push('-');
|
||||
} else {
|
||||
slug.push('_');
|
||||
}
|
||||
}
|
||||
slug
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct StaticPromptFrontmatter {
|
||||
title: String,
|
||||
version: String,
|
||||
author: String,
|
||||
#[serde(default)]
|
||||
languages: Vec<String>,
|
||||
#[serde(default)]
|
||||
dependencies: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for StaticPromptFrontmatter {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
title: PROMPT_DEFAULT_TITLE.to_string(),
|
||||
version: "1.0".to_string(),
|
||||
author: "You <you@email.com>".to_string(),
|
||||
languages: vec![],
|
||||
dependencies: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StaticPromptFrontmatter {
|
||||
/// Returns the frontmatter as a markdown frontmatter string
|
||||
pub fn frontmatter_string(&self) -> String {
|
||||
let mut frontmatter = format!(
|
||||
"---\ntitle: \"{}\"\nversion: \"{}\"\nauthor: \"{}\"\n",
|
||||
standardize_value(self.title.clone()),
|
||||
standardize_value(self.version.clone()),
|
||||
standardize_value(self.author.clone()),
|
||||
);
|
||||
|
||||
if !self.languages.is_empty() {
|
||||
let languages = self
|
||||
.languages
|
||||
.iter()
|
||||
.map(|l| standardize_value(l.clone()))
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ");
|
||||
writeln!(frontmatter, "languages: [{}]", languages).unwrap();
|
||||
}
|
||||
|
||||
if !self.dependencies.is_empty() {
|
||||
let dependencies = self
|
||||
.dependencies
|
||||
.iter()
|
||||
.map(|d| standardize_value(d.clone()))
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ");
|
||||
writeln!(frontmatter, "dependencies: [{}]", dependencies).unwrap();
|
||||
}
|
||||
|
||||
frontmatter.push_str("---\n");
|
||||
|
||||
frontmatter
|
||||
}
|
||||
}
|
||||
|
||||
/// A static prompt that can be loaded into the prompt library
|
||||
/// from Markdown with a frontmatter header
|
||||
///
|
||||
/// Examples:
|
||||
///
|
||||
/// ### Globally available prompt
|
||||
///
|
||||
/// ```markdown
|
||||
/// ---
|
||||
/// title: Foo
|
||||
/// version: 1.0
|
||||
/// author: Jane Kim <jane@kim.com
|
||||
/// languages: ["*"]
|
||||
/// dependencies: []
|
||||
/// ---
|
||||
///
|
||||
/// Foo and bar are terms used in programming to describe generic concepts.
|
||||
/// ```
|
||||
///
|
||||
/// ### Language-specific prompt
|
||||
///
|
||||
/// ```markdown
|
||||
/// ---
|
||||
/// title: UI with GPUI
|
||||
/// version: 1.0
|
||||
/// author: Nate Butler <iamnbutler@gmail.com>
|
||||
/// languages: ["rust"]
|
||||
/// dependencies: ["gpui"]
|
||||
/// ---
|
||||
///
|
||||
/// When building a UI with GPUI, ensure you...
|
||||
/// ```
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct StaticPrompt {
|
||||
#[serde(skip_deserializing)]
|
||||
id: PromptId,
|
||||
#[serde(skip)]
|
||||
metadata: StaticPromptFrontmatter,
|
||||
content: String,
|
||||
file_name: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl Default for StaticPrompt {
|
||||
fn default() -> Self {
|
||||
let metadata = StaticPromptFrontmatter::default();
|
||||
|
||||
let content = metadata.clone().frontmatter_string();
|
||||
|
||||
Self {
|
||||
id: PromptId::new(),
|
||||
metadata,
|
||||
content,
|
||||
file_name: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StaticPrompt {
|
||||
pub fn new(content: String, file_name: Option<String>) -> Self {
|
||||
let matter = Matter::<YAML>::new();
|
||||
let result = matter.parse(&content);
|
||||
let file_name = if let Some(file_name) = file_name {
|
||||
let shared_filename: SharedString = file_name.into();
|
||||
Some(shared_filename)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let metadata = result
|
||||
.data
|
||||
.map_or_else(
|
||||
|| Err(anyhow::anyhow!("Failed to parse frontmatter")),
|
||||
|data| {
|
||||
let front_matter: StaticPromptFrontmatter = data.deserialize()?;
|
||||
Ok(front_matter)
|
||||
},
|
||||
)
|
||||
.unwrap_or_else(|e| {
|
||||
if let Some(file_name) = &file_name {
|
||||
log::error!("Failed to parse frontmatter for {}: {}", file_name, e);
|
||||
} else {
|
||||
log::error!("Failed to parse frontmatter: {}", e);
|
||||
}
|
||||
StaticPromptFrontmatter::default()
|
||||
});
|
||||
|
||||
let id = if let Some(file_name) = &file_name {
|
||||
PromptId::from_str(file_name).unwrap_or_default()
|
||||
} else {
|
||||
PromptId::new()
|
||||
};
|
||||
|
||||
StaticPrompt {
|
||||
id,
|
||||
content,
|
||||
file_name,
|
||||
metadata,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, id: PromptId, content: String) {
|
||||
let mut updated_prompt =
|
||||
StaticPrompt::new(content, self.file_name.clone().map(|s| s.to_string()));
|
||||
updated_prompt.id = id;
|
||||
*self = updated_prompt;
|
||||
}
|
||||
}
|
||||
|
||||
impl StaticPrompt {
|
||||
/// Returns the prompt's id
|
||||
pub fn id(&self) -> &PromptId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn file_name(&self) -> Option<&SharedString> {
|
||||
self.file_name.as_ref()
|
||||
}
|
||||
|
||||
/// Sets the file name of the prompt
|
||||
pub fn new_file_name(&self) -> String {
|
||||
let in_name = format!(
|
||||
"{}_{}_{}",
|
||||
standardize_value(self.metadata.title.clone()),
|
||||
standardize_value(self.metadata.version.clone()),
|
||||
standardize_value(self.id.0.to_string())
|
||||
);
|
||||
let out_name = slugify(in_name);
|
||||
out_name
|
||||
}
|
||||
|
||||
/// Returns the prompt's content
|
||||
pub fn content(&self) -> &String {
|
||||
&self.content
|
||||
}
|
||||
|
||||
/// Returns the prompt's metadata
|
||||
pub fn _metadata(&self) -> &StaticPromptFrontmatter {
|
||||
&self.metadata
|
||||
}
|
||||
|
||||
/// Returns the prompt's title
|
||||
pub fn title(&self) -> SharedString {
|
||||
self.metadata.title.clone().into()
|
||||
}
|
||||
|
||||
pub fn body(&self) -> String {
|
||||
let matter = Matter::<YAML>::new();
|
||||
let result = matter.parse(self.content.as_str());
|
||||
result.content.clone()
|
||||
}
|
||||
|
||||
pub fn path(&self) -> Option<PathBuf> {
|
||||
if let Some(file_name) = self.file_name() {
|
||||
let path_str = format!("{}", file_name);
|
||||
Some(PROMPTS_DIR.join(path_str))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn save(&self, fs: Arc<dyn Fs>) -> anyhow::Result<()> {
|
||||
let file_name = self.file_name();
|
||||
let new_file_name = self.new_file_name();
|
||||
|
||||
let out_name = if let Some(file_name) = file_name {
|
||||
file_name.to_owned().to_string()
|
||||
} else {
|
||||
format!("{}.md", new_file_name)
|
||||
};
|
||||
let path = PROMPTS_DIR.join(&out_name);
|
||||
let json = self.content.clone();
|
||||
|
||||
fs.atomic_write(path, json).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_content_prompt(
|
||||
user_prompt: String,
|
||||
language_name: Option<&str>,
|
||||
buffer: BufferSnapshot,
|
||||
range: Range<usize>,
|
||||
project_name: Option<String>,
|
||||
) -> anyhow::Result<String> {
|
||||
let mut prompt = String::new();
|
||||
|
||||
let content_type = match language_name {
|
||||
None | Some("Markdown" | "Plain Text") => {
|
||||
writeln!(prompt, "You are an expert engineer.")?;
|
||||
"Text"
|
||||
}
|
||||
Some(language_name) => {
|
||||
writeln!(prompt, "You are an expert {language_name} engineer.")?;
|
||||
writeln!(
|
||||
prompt,
|
||||
"Your answer MUST always and only be valid {}.",
|
||||
language_name
|
||||
)?;
|
||||
"Code"
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(project_name) = project_name {
|
||||
writeln!(
|
||||
prompt,
|
||||
"You are currently working inside the '{project_name}' project in code editor Zed."
|
||||
)?;
|
||||
}
|
||||
|
||||
// Include file content.
|
||||
for chunk in buffer.text_for_range(0..range.start) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
|
||||
if range.is_empty() {
|
||||
prompt.push_str("<|START|>");
|
||||
} else {
|
||||
prompt.push_str("<|START|");
|
||||
}
|
||||
|
||||
for chunk in buffer.text_for_range(range.clone()) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
|
||||
if !range.is_empty() {
|
||||
prompt.push_str("|END|>");
|
||||
}
|
||||
|
||||
for chunk in buffer.text_for_range(range.end..buffer.len()) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
|
||||
prompt.push('\n');
|
||||
|
||||
if range.is_empty() {
|
||||
writeln!(
|
||||
prompt,
|
||||
"Assume the cursor is located where the `<|START|>` span is."
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"{content_type} can't be replaced, so assume your answer will be inserted at the cursor.",
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Generate {content_type} based on the users prompt: {user_prompt}",
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(prompt, "Modify the user's selected {content_type} based upon the users prompt: '{user_prompt}'").unwrap();
|
||||
writeln!(prompt, "You must reply with only the adjusted {content_type} (within the '<|START|' and '|END|>' spans) not the entire file.").unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Double check that you only return code and not the '<|START|' and '|END|'> spans"
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
writeln!(prompt, "Never make remarks about the output.").unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Do not return anything else, except the generated {content_type}."
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
Ok(prompt)
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
use anyhow::Context;
|
||||
use collections::HashMap;
|
||||
use fs::Fs;
|
||||
|
||||
use gray_matter::{engine::YAML, Matter};
|
||||
use parking_lot::RwLock;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smol::stream::StreamExt;
|
||||
use std::sync::Arc;
|
||||
use util::paths::PROMPTS_DIR;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::prompt::StaticPrompt;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
|
||||
pub struct PromptId(pub Uuid);
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum SortOrder {
|
||||
Alphabetical,
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
impl PromptId {
|
||||
pub fn new() -> Self {
|
||||
Self(Uuid::new_v4())
|
||||
}
|
||||
|
||||
pub fn from_str(id: &str) -> anyhow::Result<Self> {
|
||||
Ok(Self(Uuid::parse_str(id)?))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PromptId {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
pub struct PromptLibraryState {
|
||||
/// A set of prompts that all assistant contexts will start with
|
||||
default_prompt: Vec<PromptId>,
|
||||
/// All [Prompt]s loaded into the library
|
||||
prompts: HashMap<PromptId, StaticPrompt>,
|
||||
/// Prompts that have been changed but haven't been
|
||||
/// saved back to the file system
|
||||
dirty_prompts: Vec<PromptId>,
|
||||
version: usize,
|
||||
}
|
||||
|
||||
pub struct PromptLibrary {
|
||||
state: RwLock<PromptLibraryState>,
|
||||
}
|
||||
|
||||
impl Default for PromptLibrary {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl PromptLibrary {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
state: RwLock::new(PromptLibraryState::default()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_prompt(&self) -> StaticPrompt {
|
||||
StaticPrompt::default()
|
||||
}
|
||||
|
||||
pub fn add_prompt(&self, prompt: StaticPrompt) {
|
||||
let mut state = self.state.write();
|
||||
let id = *prompt.id();
|
||||
state.prompts.insert(id, prompt);
|
||||
state.version += 1;
|
||||
}
|
||||
|
||||
pub fn prompts(&self) -> HashMap<PromptId, StaticPrompt> {
|
||||
let state = self.state.read();
|
||||
state.prompts.clone()
|
||||
}
|
||||
|
||||
pub fn sorted_prompts(&self, sort_order: SortOrder) -> Vec<(PromptId, StaticPrompt)> {
|
||||
let state = self.state.read();
|
||||
|
||||
let mut prompts = state
|
||||
.prompts
|
||||
.iter()
|
||||
.map(|(id, prompt)| (*id, prompt.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
match sort_order {
|
||||
SortOrder::Alphabetical => prompts.sort_by(|(_, a), (_, b)| a.title().cmp(&b.title())),
|
||||
};
|
||||
|
||||
prompts
|
||||
}
|
||||
|
||||
pub fn prompt_by_id(&self, id: PromptId) -> Option<StaticPrompt> {
|
||||
let state = self.state.read();
|
||||
state.prompts.get(&id).cloned()
|
||||
}
|
||||
|
||||
pub fn first_prompt_id(&self) -> Option<PromptId> {
|
||||
let state = self.state.read();
|
||||
state.prompts.keys().next().cloned()
|
||||
}
|
||||
|
||||
pub fn is_dirty(&self, id: &PromptId) -> bool {
|
||||
let state = self.state.read();
|
||||
state.dirty_prompts.contains(&id)
|
||||
}
|
||||
|
||||
pub fn set_dirty(&self, id: PromptId, dirty: bool) {
|
||||
let mut state = self.state.write();
|
||||
if dirty {
|
||||
if !state.dirty_prompts.contains(&id) {
|
||||
state.dirty_prompts.push(id);
|
||||
}
|
||||
state.version += 1;
|
||||
} else {
|
||||
state.dirty_prompts.retain(|&i| i != id);
|
||||
state.version += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Load the state of the prompt library from the file system
|
||||
/// or create a new one if it doesn't exist
|
||||
pub async fn load_index(fs: Arc<dyn Fs>) -> anyhow::Result<Self> {
|
||||
let path = PROMPTS_DIR.join("index.json");
|
||||
|
||||
let state = if fs.is_file(&path).await {
|
||||
let json = fs.load(&path).await?;
|
||||
serde_json::from_str(&json)?
|
||||
} else {
|
||||
PromptLibraryState::default()
|
||||
};
|
||||
|
||||
let mut prompt_library = Self {
|
||||
state: RwLock::new(state),
|
||||
};
|
||||
|
||||
prompt_library.load_prompts(fs).await?;
|
||||
|
||||
Ok(prompt_library)
|
||||
}
|
||||
|
||||
/// Load all prompts from the file system
|
||||
/// adding them to the library if they don't already exist
|
||||
pub async fn load_prompts(&mut self, fs: Arc<dyn Fs>) -> anyhow::Result<()> {
|
||||
self.state.get_mut().prompts.clear();
|
||||
|
||||
let mut prompt_paths = fs.read_dir(&PROMPTS_DIR).await?;
|
||||
|
||||
while let Some(prompt_path) = prompt_paths.next().await {
|
||||
let prompt_path = prompt_path.with_context(|| "Failed to read prompt path")?;
|
||||
let file_name_lossy = if prompt_path.file_name().is_some() {
|
||||
Some(
|
||||
prompt_path
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if !fs.is_file(&prompt_path).await
|
||||
|| prompt_path.extension().and_then(|ext| ext.to_str()) != Some("md")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let json = fs
|
||||
.load(&prompt_path)
|
||||
.await
|
||||
.with_context(|| format!("Failed to load prompt {:?}", prompt_path))?;
|
||||
|
||||
// Check that the prompt is valid
|
||||
let matter = Matter::<YAML>::new();
|
||||
let result = matter.parse(&json);
|
||||
if result.data.is_none() {
|
||||
log::warn!("Invalid prompt: {:?}", prompt_path);
|
||||
continue;
|
||||
}
|
||||
|
||||
let static_prompt = StaticPrompt::new(json, file_name_lossy.clone());
|
||||
|
||||
let state = self.state.get_mut();
|
||||
|
||||
let id = Uuid::new_v4();
|
||||
state.prompts.insert(PromptId(id), static_prompt);
|
||||
state.version += 1;
|
||||
}
|
||||
|
||||
// Write any changes back to the file system
|
||||
self.save_index(fs.clone()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Save the current state of the prompt library to the
|
||||
/// file system as a JSON file
|
||||
pub async fn save_index(&self, fs: Arc<dyn Fs>) -> anyhow::Result<()> {
|
||||
fs.create_dir(&PROMPTS_DIR).await?;
|
||||
|
||||
let path = PROMPTS_DIR.join("index.json");
|
||||
|
||||
let json = {
|
||||
let state = self.state.read();
|
||||
serde_json::to_string(&*state)?
|
||||
};
|
||||
|
||||
fs.atomic_write(path, json).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn save_prompt(
|
||||
&self,
|
||||
prompt_id: PromptId,
|
||||
updated_content: Option<String>,
|
||||
fs: Arc<dyn Fs>,
|
||||
) -> anyhow::Result<()> {
|
||||
if let Some(updated_content) = updated_content {
|
||||
let mut state = self.state.write();
|
||||
if let Some(prompt) = state.prompts.get_mut(&prompt_id) {
|
||||
prompt.update(prompt_id, updated_content);
|
||||
state.version += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(prompt) = self.prompt_by_id(prompt_id) {
|
||||
prompt.save(fs).await?;
|
||||
self.set_dirty(prompt_id, false);
|
||||
} else {
|
||||
log::warn!("Failed to save prompt: {:?}", prompt_id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,512 +0,0 @@
|
||||
use collections::HashMap;
|
||||
use editor::{Editor, EditorEvent};
|
||||
use fs::Fs;
|
||||
use gpui::{prelude::FluentBuilder, *};
|
||||
use language::{language_settings, Buffer, LanguageRegistry};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use std::sync::Arc;
|
||||
use ui::{prelude::*, IconButtonShape, Indicator, ListItem, ListItemSpacing, Tooltip};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use workspace::ModalView;
|
||||
|
||||
use crate::prompts::{PromptId, PromptLibrary, SortOrder, StaticPrompt, PROMPT_DEFAULT_TITLE};
|
||||
|
||||
actions!(prompt_manager, [NewPrompt, SavePrompt]);
|
||||
|
||||
pub struct PromptManager {
|
||||
focus_handle: FocusHandle,
|
||||
prompt_library: Arc<PromptLibrary>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
#[allow(dead_code)]
|
||||
fs: Arc<dyn Fs>,
|
||||
picker: View<Picker<PromptManagerDelegate>>,
|
||||
prompt_editors: HashMap<PromptId, View<Editor>>,
|
||||
active_prompt_id: Option<PromptId>,
|
||||
last_new_prompt_id: Option<PromptId>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl PromptManager {
|
||||
pub fn new(
|
||||
prompt_library: Arc<PromptLibrary>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let prompt_manager = cx.view().downgrade();
|
||||
let picker = cx.new_view(|cx| {
|
||||
Picker::uniform_list(
|
||||
PromptManagerDelegate {
|
||||
prompt_manager,
|
||||
matching_prompts: vec![],
|
||||
matching_prompt_ids: vec![],
|
||||
prompt_library: prompt_library.clone(),
|
||||
selected_index: 0,
|
||||
_subscriptions: vec![],
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.max_height(rems(35.75))
|
||||
.modal(false)
|
||||
});
|
||||
|
||||
let focus_handle = picker.focus_handle(cx);
|
||||
|
||||
let subscriptions = vec![
|
||||
// cx.on_focus_in(&focus_handle, Self::focus_in),
|
||||
// cx.on_focus_out(&focus_handle, Self::focus_out),
|
||||
];
|
||||
|
||||
let mut manager = Self {
|
||||
focus_handle,
|
||||
prompt_library,
|
||||
language_registry,
|
||||
fs,
|
||||
picker,
|
||||
prompt_editors: HashMap::default(),
|
||||
active_prompt_id: None,
|
||||
last_new_prompt_id: None,
|
||||
_subscriptions: subscriptions,
|
||||
};
|
||||
|
||||
manager.active_prompt_id = manager.prompt_library.first_prompt_id();
|
||||
|
||||
manager
|
||||
}
|
||||
|
||||
fn dispatch_context(&self, cx: &ViewContext<Self>) -> KeyContext {
|
||||
let mut dispatch_context = KeyContext::new_with_defaults();
|
||||
dispatch_context.add("PromptManager");
|
||||
|
||||
let identifier = match self.active_editor() {
|
||||
Some(active_editor) if active_editor.focus_handle(cx).is_focused(cx) => "editing",
|
||||
_ => "not_editing",
|
||||
};
|
||||
|
||||
dispatch_context.add(identifier);
|
||||
dispatch_context
|
||||
}
|
||||
|
||||
pub fn new_prompt(&mut self, _: &NewPrompt, cx: &mut ViewContext<Self>) {
|
||||
// TODO: Why doesn't this prevent making a new prompt if you
|
||||
// move the picker selection/maybe unfocus the editor?
|
||||
|
||||
// Prevent making a new prompt if the last new prompt is still empty
|
||||
//
|
||||
// Instead, we'll focus the last new prompt
|
||||
if let Some(last_new_prompt_id) = self.last_new_prompt_id() {
|
||||
if let Some(last_new_prompt) = self.prompt_library.prompt_by_id(last_new_prompt_id) {
|
||||
let normalized_body = last_new_prompt
|
||||
.body()
|
||||
.trim()
|
||||
.replace(['\r', '\n'], "")
|
||||
.to_string();
|
||||
|
||||
if last_new_prompt.title() == PROMPT_DEFAULT_TITLE && normalized_body.is_empty() {
|
||||
self.set_editor_for_prompt(last_new_prompt_id, cx);
|
||||
self.focus_active_editor(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let prompt = self.prompt_library.new_prompt();
|
||||
self.set_last_new_prompt_id(Some(prompt.id().to_owned()));
|
||||
|
||||
self.prompt_library.add_prompt(prompt.clone());
|
||||
|
||||
let id = *prompt.id();
|
||||
self.picker.update(cx, |picker, _cx| {
|
||||
let prompts = self
|
||||
.prompt_library
|
||||
.sorted_prompts(SortOrder::Alphabetical)
|
||||
.clone()
|
||||
.into_iter();
|
||||
|
||||
picker.delegate.prompt_library = self.prompt_library.clone();
|
||||
picker.delegate.matching_prompts = prompts.clone().map(|(_, p)| Arc::new(p)).collect();
|
||||
picker.delegate.matching_prompt_ids = prompts.map(|(id, _)| id).collect();
|
||||
picker.delegate.selected_index = picker
|
||||
.delegate
|
||||
.matching_prompts
|
||||
.iter()
|
||||
.position(|p| p.id() == &id)
|
||||
.unwrap_or(0);
|
||||
});
|
||||
|
||||
self.active_prompt_id = Some(id);
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn save_prompt(
|
||||
&mut self,
|
||||
fs: Arc<dyn Fs>,
|
||||
prompt_id: PromptId,
|
||||
new_content: String,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Result<()> {
|
||||
let library = self.prompt_library.clone();
|
||||
if library.prompt_by_id(prompt_id).is_some() {
|
||||
cx.spawn(|_, _| async move {
|
||||
library
|
||||
.save_prompt(prompt_id, Some(new_content), fs)
|
||||
.log_err()
|
||||
.await;
|
||||
})
|
||||
.detach();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_active_prompt(&mut self, prompt_id: Option<PromptId>, cx: &mut ViewContext<Self>) {
|
||||
self.active_prompt_id = prompt_id;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn last_new_prompt_id(&self) -> Option<PromptId> {
|
||||
self.last_new_prompt_id
|
||||
}
|
||||
|
||||
pub fn set_last_new_prompt_id(&mut self, id: Option<PromptId>) {
|
||||
self.last_new_prompt_id = id;
|
||||
}
|
||||
|
||||
pub fn focus_active_editor(&self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(active_prompt_id) = self.active_prompt_id {
|
||||
if let Some(editor) = self.prompt_editors.get(&active_prompt_id) {
|
||||
let focus_handle = editor.focus_handle(cx);
|
||||
|
||||
cx.focus(&focus_handle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn active_editor(&self) -> Option<&View<Editor>> {
|
||||
self.active_prompt_id
|
||||
.and_then(|active_prompt_id| self.prompt_editors.get(&active_prompt_id))
|
||||
}
|
||||
|
||||
fn set_editor_for_prompt(
|
||||
&mut self,
|
||||
prompt_id: PromptId,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl IntoElement {
|
||||
let prompt_library = self.prompt_library.clone();
|
||||
|
||||
let editor_for_prompt = self.prompt_editors.entry(prompt_id).or_insert_with(|| {
|
||||
cx.new_view(|cx| {
|
||||
let text = if let Some(prompt) = prompt_library.prompt_by_id(prompt_id) {
|
||||
prompt.content().to_owned()
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
let buffer = cx.new_model(|cx| {
|
||||
let mut buffer = Buffer::local(text, cx);
|
||||
let markdown = self.language_registry.language_for_name("Markdown");
|
||||
cx.spawn(|buffer, mut cx| async move {
|
||||
if let Some(markdown) = markdown.await.log_err() {
|
||||
_ = buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.set_language(Some(markdown), cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
buffer.set_language_registry(self.language_registry.clone());
|
||||
buffer
|
||||
});
|
||||
let mut editor = Editor::for_buffer(buffer, None, cx);
|
||||
editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor
|
||||
})
|
||||
});
|
||||
|
||||
editor_for_prompt.clone()
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn render_prompt_list(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let picker = self.picker.clone();
|
||||
|
||||
v_flex()
|
||||
.id("prompt-list")
|
||||
.bg(cx.theme().colors().surface_background)
|
||||
.h_full()
|
||||
.w_1_3()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
.bg(cx.theme().colors().background)
|
||||
.p(Spacing::Small.rems(cx))
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.h(rems(1.75))
|
||||
.w_full()
|
||||
.flex_none()
|
||||
.justify_between()
|
||||
.child(Label::new("Prompt Library").size(LabelSize::Small))
|
||||
.child(
|
||||
IconButton::new("new-prompt", IconName::Plus)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| Tooltip::text("New Prompt", cx))
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(NewPrompt.boxed_clone());
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.h(rems(38.25))
|
||||
.flex_grow()
|
||||
.justify_start()
|
||||
.child(picker),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for PromptManager {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let active_prompt_id = self.active_prompt_id;
|
||||
let active_prompt = if let Some(active_prompt_id) = active_prompt_id {
|
||||
self.prompt_library.clone().prompt_by_id(active_prompt_id)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let active_editor = self.active_editor().map(|editor| editor.clone());
|
||||
let updated_content = if let Some(editor) = active_editor {
|
||||
Some(editor.read(cx).text(cx))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let can_save = active_prompt_id.is_some() && updated_content.is_some();
|
||||
let fs = self.fs.clone();
|
||||
|
||||
h_flex()
|
||||
.id("prompt-manager")
|
||||
.key_context(self.dispatch_context(cx))
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_action(cx.listener(Self::dismiss))
|
||||
.on_action(cx.listener(Self::new_prompt))
|
||||
.elevation_3(cx)
|
||||
.size_full()
|
||||
.flex_none()
|
||||
.w(rems(64.))
|
||||
.h(rems(40.))
|
||||
.overflow_hidden()
|
||||
.child(self.render_prompt_list(cx))
|
||||
.child(
|
||||
div().w_2_3().h_full().child(
|
||||
v_flex()
|
||||
.id("prompt-editor")
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.size_full()
|
||||
.flex_none()
|
||||
.min_w_64()
|
||||
.h_full()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
.bg(cx.theme().colors().background)
|
||||
.p(Spacing::Small.rems(cx))
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.h_7()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap(Spacing::XXLarge.rems(cx))
|
||||
.child(if can_save {
|
||||
IconButton::new("save", IconName::Save)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| Tooltip::text("Save Prompt", cx))
|
||||
.on_click(cx.listener(move |this, _event, cx| {
|
||||
if let Some(prompt_id) = active_prompt_id {
|
||||
this.save_prompt(
|
||||
fs.clone(),
|
||||
prompt_id,
|
||||
updated_content.clone().unwrap_or(
|
||||
"TODO: make unreachable"
|
||||
.to_string(),
|
||||
),
|
||||
cx,
|
||||
)
|
||||
.log_err();
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
IconButton::new("save", IconName::Save)
|
||||
.shape(IconButtonShape::Square)
|
||||
.disabled(true)
|
||||
})
|
||||
.when_some(active_prompt, |this, active_prompt| {
|
||||
let path = active_prompt.path();
|
||||
|
||||
this.child(
|
||||
IconButton::new("reveal", IconName::Reveal)
|
||||
.shape(IconButtonShape::Square)
|
||||
.disabled(path.is_none())
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::text("Reveal in Finder", cx)
|
||||
})
|
||||
.on_click(cx.listener(move |_, _event, cx| {
|
||||
if let Some(path) = path.clone() {
|
||||
cx.reveal_path(&path);
|
||||
}
|
||||
})),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("dismiss", IconName::Close)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| Tooltip::text("Close", cx))
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(menu::Cancel.boxed_clone());
|
||||
}),
|
||||
),
|
||||
)
|
||||
.when_some(active_prompt_id, |this, active_prompt_id| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.w_full()
|
||||
.py(Spacing::Large.rems(cx))
|
||||
.px(Spacing::XLarge.rems(cx))
|
||||
.child(self.set_editor_for_prompt(active_prompt_id, cx)),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for PromptManager {}
|
||||
impl EventEmitter<EditorEvent> for PromptManager {}
|
||||
|
||||
impl ModalView for PromptManager {}
|
||||
|
||||
impl FocusableView for PromptManager {
|
||||
fn focus_handle(&self, _cx: &AppContext) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PromptManagerDelegate {
|
||||
prompt_manager: WeakView<PromptManager>,
|
||||
matching_prompts: Vec<Arc<StaticPrompt>>,
|
||||
matching_prompt_ids: Vec<PromptId>,
|
||||
prompt_library: Arc<PromptLibrary>,
|
||||
selected_index: usize,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl PickerDelegate for PromptManagerDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
|
||||
"Find a prompt…".into()
|
||||
}
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matching_prompt_ids.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn selected_index_changed(
|
||||
&self,
|
||||
ix: usize,
|
||||
_cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Box<dyn Fn(&mut WindowContext) + 'static>> {
|
||||
let prompt_id = self.matching_prompt_ids.get(ix).copied()?;
|
||||
let prompt_manager = self.prompt_manager.upgrade()?;
|
||||
|
||||
Some(Box::new(move |cx| {
|
||||
prompt_manager.update(cx, |manager, cx| {
|
||||
manager.set_active_prompt(Some(prompt_id), cx);
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
let prompt_library = self.prompt_library.clone();
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
async {
|
||||
let prompts = prompt_library.sorted_prompts(SortOrder::Alphabetical);
|
||||
let matching_prompts = prompts
|
||||
.into_iter()
|
||||
.filter(|(_, prompt)| {
|
||||
prompt
|
||||
.content()
|
||||
.to_lowercase()
|
||||
.contains(&query.to_lowercase())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
picker.update(&mut cx, |picker, cx| {
|
||||
picker.delegate.matching_prompt_ids =
|
||||
matching_prompts.iter().map(|(id, _)| *id).collect();
|
||||
picker.delegate.matching_prompts = matching_prompts
|
||||
.into_iter()
|
||||
.map(|(_, prompt)| Arc::new(prompt))
|
||||
.collect();
|
||||
cx.notify();
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
.await;
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
let prompt_manager = self.prompt_manager.upgrade().unwrap();
|
||||
prompt_manager.update(cx, move |manager, cx| manager.focus_active_editor(cx));
|
||||
}
|
||||
|
||||
fn should_dismiss(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.prompt_manager
|
||||
.update(cx, |_, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let prompt = self.matching_prompts.get(ix)?;
|
||||
|
||||
let is_diry = self.prompt_library.is_dirty(prompt.id());
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.selected(selected)
|
||||
.child(Label::new(prompt.title()))
|
||||
.end_slot(div().when(is_diry, |this| this.child(Indicator::dot()))),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
use crate::{assistant_settings::OpenAiModel, MessageId, MessageMetadata};
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::HashMap;
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
cmp::Reverse,
|
||||
ffi::OsStr,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use util::paths::CONVERSATIONS_DIR;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedMessage {
|
||||
pub id: MessageId,
|
||||
pub start: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedConversation {
|
||||
pub id: Option<String>,
|
||||
pub zed: String,
|
||||
pub version: String,
|
||||
pub text: String,
|
||||
pub messages: Vec<SavedMessage>,
|
||||
pub message_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
pub summary: String,
|
||||
}
|
||||
|
||||
impl SavedConversation {
|
||||
pub const VERSION: &'static str = "0.2.0";
|
||||
|
||||
pub async fn load(path: &Path, fs: &dyn Fs) -> Result<Self> {
|
||||
let saved_conversation = fs.load(path).await?;
|
||||
let saved_conversation_json =
|
||||
serde_json::from_str::<serde_json::Value>(&saved_conversation)?;
|
||||
match saved_conversation_json
|
||||
.get("version")
|
||||
.ok_or_else(|| anyhow!("version not found"))?
|
||||
{
|
||||
serde_json::Value::String(version) => match version.as_str() {
|
||||
Self::VERSION => Ok(serde_json::from_value::<Self>(saved_conversation_json)?),
|
||||
"0.1.0" => {
|
||||
let saved_conversation =
|
||||
serde_json::from_value::<SavedConversationV0_1_0>(saved_conversation_json)?;
|
||||
Ok(Self {
|
||||
id: saved_conversation.id,
|
||||
zed: saved_conversation.zed,
|
||||
version: saved_conversation.version,
|
||||
text: saved_conversation.text,
|
||||
messages: saved_conversation.messages,
|
||||
message_metadata: saved_conversation.message_metadata,
|
||||
summary: saved_conversation.summary,
|
||||
})
|
||||
}
|
||||
_ => Err(anyhow!(
|
||||
"unrecognized saved conversation version: {}",
|
||||
version
|
||||
)),
|
||||
},
|
||||
_ => Err(anyhow!("version not found on saved conversation")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SavedConversationV0_1_0 {
|
||||
id: Option<String>,
|
||||
zed: String,
|
||||
version: String,
|
||||
text: String,
|
||||
messages: Vec<SavedMessage>,
|
||||
message_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
summary: String,
|
||||
api_url: Option<String>,
|
||||
model: OpenAiModel,
|
||||
}
|
||||
|
||||
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::<SavedConversationMetadata>::new();
|
||||
while let Some(path) = paths.next().await {
|
||||
let path = path?;
|
||||
if path.extension() != Some(OsStr::new("json")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let pattern = r" - \d+.zed.json$";
|
||||
let re = Regex::new(pattern).unwrap();
|
||||
|
||||
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 new 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)
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::assistant_panel::ConversationEditor;
|
||||
use crate::assistant_panel::ContextEditor;
|
||||
use anyhow::Result;
|
||||
pub use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandRegistry};
|
||||
use editor::{CompletionProvider, Editor};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{Model, Task, ViewContext, WeakView, WindowContext};
|
||||
use language::{Anchor, Buffer, CodeLabel, Documentation, LanguageServerId, ToPoint};
|
||||
use gpui::{AppContext, Model, Task, ViewContext, WeakView, WindowContext};
|
||||
use language::{Anchor, Buffer, CodeLabel, Documentation, HighlightId, LanguageServerId, ToPoint};
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use rope::Point;
|
||||
use std::{
|
||||
@@ -14,21 +14,27 @@ use std::{
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use ui::ActiveTheme;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub mod active_command;
|
||||
pub mod default_command;
|
||||
pub mod diagnostics_command;
|
||||
pub mod fetch_command;
|
||||
pub mod file_command;
|
||||
pub mod now_command;
|
||||
pub mod project_command;
|
||||
pub mod prompt_command;
|
||||
pub mod rustdoc_command;
|
||||
pub mod search_command;
|
||||
pub mod tabs_command;
|
||||
pub mod term_command;
|
||||
|
||||
pub(crate) struct SlashCommandCompletionProvider {
|
||||
editor: WeakView<ConversationEditor>,
|
||||
commands: Arc<SlashCommandRegistry>,
|
||||
cancel_flag: Mutex<Arc<AtomicBool>>,
|
||||
workspace: WeakView<Workspace>,
|
||||
editor: Option<WeakView<ContextEditor>>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
}
|
||||
|
||||
pub(crate) struct SlashCommandLine {
|
||||
@@ -40,9 +46,9 @@ pub(crate) struct SlashCommandLine {
|
||||
|
||||
impl SlashCommandCompletionProvider {
|
||||
pub fn new(
|
||||
editor: WeakView<ConversationEditor>,
|
||||
commands: Arc<SlashCommandRegistry>,
|
||||
workspace: WeakView<Workspace>,
|
||||
editor: Option<WeakView<ContextEditor>>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
|
||||
@@ -96,6 +102,30 @@ impl SlashCommandCompletionProvider {
|
||||
new_text.push(' ');
|
||||
}
|
||||
|
||||
let confirm = editor.clone().zip(workspace.clone()).and_then(
|
||||
|(editor, workspace)| {
|
||||
(!requires_argument).then(|| {
|
||||
let command_name = mat.string.clone();
|
||||
let command_range = command_range.clone();
|
||||
let editor = editor.clone();
|
||||
let workspace = workspace.clone();
|
||||
Arc::new(move |cx: &mut WindowContext| {
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.run_command(
|
||||
command_range.clone(),
|
||||
&command_name,
|
||||
None,
|
||||
true,
|
||||
workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}) as Arc<_>
|
||||
})
|
||||
},
|
||||
);
|
||||
Some(project::Completion {
|
||||
old_range: name_range.clone(),
|
||||
documentation: Some(Documentation::SingleLine(command.description())),
|
||||
@@ -104,25 +134,7 @@ impl SlashCommandCompletionProvider {
|
||||
server_id: LanguageServerId(0),
|
||||
lsp_completion: Default::default(),
|
||||
show_new_completions_on_confirm: requires_argument,
|
||||
confirm: (!requires_argument).then(|| {
|
||||
let command_name = mat.string.clone();
|
||||
let command_range = command_range.clone();
|
||||
let editor = editor.clone();
|
||||
let workspace = workspace.clone();
|
||||
Arc::new(move |cx: &mut WindowContext| {
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.run_command(
|
||||
command_range.clone(),
|
||||
&command_name,
|
||||
None,
|
||||
workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}) as Arc<_>
|
||||
}),
|
||||
confirm,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
@@ -157,33 +169,42 @@ impl SlashCommandCompletionProvider {
|
||||
Ok(completions
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|arg| project::Completion {
|
||||
old_range: argument_range.clone(),
|
||||
label: CodeLabel::plain(arg.clone(), None),
|
||||
new_text: arg.clone(),
|
||||
documentation: None,
|
||||
server_id: LanguageServerId(0),
|
||||
lsp_completion: Default::default(),
|
||||
show_new_completions_on_confirm: false,
|
||||
confirm: Some(Arc::new({
|
||||
let command_name = command_name.clone();
|
||||
let command_range = command_range.clone();
|
||||
let editor = editor.clone();
|
||||
let workspace = workspace.clone();
|
||||
move |cx| {
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.run_command(
|
||||
command_range.clone(),
|
||||
&command_name,
|
||||
Some(&arg),
|
||||
workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})),
|
||||
.map(|command_argument| {
|
||||
let confirm =
|
||||
editor
|
||||
.clone()
|
||||
.zip(workspace.clone())
|
||||
.map(|(editor, workspace)| {
|
||||
Arc::new({
|
||||
let command_range = command_range.clone();
|
||||
let command_name = command_name.clone();
|
||||
let command_argument = command_argument.clone();
|
||||
move |cx: &mut WindowContext| {
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.run_command(
|
||||
command_range.clone(),
|
||||
&command_name,
|
||||
Some(&command_argument),
|
||||
true,
|
||||
workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}) as Arc<_>
|
||||
});
|
||||
project::Completion {
|
||||
old_range: argument_range.clone(),
|
||||
label: CodeLabel::plain(command_argument.clone(), None),
|
||||
new_text: command_argument.clone(),
|
||||
documentation: None,
|
||||
server_id: LanguageServerId(0),
|
||||
lsp_completion: Default::default(),
|
||||
show_new_completions_on_confirm: false,
|
||||
confirm,
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
})
|
||||
@@ -199,6 +220,7 @@ impl CompletionProvider for SlashCommandCompletionProvider {
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
buffer_position: Anchor,
|
||||
_: editor::CompletionContext,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Task<Result<Vec<project::Completion>>> {
|
||||
let Some((name, argument, command_range, argument_range)) =
|
||||
@@ -327,3 +349,19 @@ impl SlashCommandLine {
|
||||
call
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_label_for_command(
|
||||
command_name: &str,
|
||||
arguments: &[&str],
|
||||
cx: &AppContext,
|
||||
) -> CodeLabel {
|
||||
let mut label = CodeLabel::default();
|
||||
label.push_str(command_name, None);
|
||||
label.push_str(" ", None);
|
||||
label.push_str(
|
||||
&arguments.join(" "),
|
||||
cx.theme().syntax().highlight_id("comment").map(HighlightId),
|
||||
);
|
||||
label.filter_range = 0..command_name.len();
|
||||
label
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
use super::{file_command::FilePlaceholder, SlashCommand, SlashCommandOutput};
|
||||
use super::{
|
||||
diagnostics_command::write_single_file_diagnostics,
|
||||
file_command::{build_entry_output_section, codeblock_fence_for_path},
|
||||
SlashCommand, SlashCommandOutput,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::SlashCommandOutputSection;
|
||||
use editor::Editor;
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use language::LspAdapterDelegate;
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
use ui::{IntoElement, WindowContext};
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
use ui::WindowContext;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct ActiveSlashCommand;
|
||||
@@ -19,15 +23,15 @@ impl SlashCommand for ActiveSlashCommand {
|
||||
"insert active tab".into()
|
||||
}
|
||||
|
||||
fn tooltip_text(&self) -> String {
|
||||
"insert active tab".into()
|
||||
fn menu_text(&self) -> String {
|
||||
"Insert Active Tab".into()
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
&self,
|
||||
self: Arc<Self>,
|
||||
_query: String,
|
||||
_cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
@@ -57,45 +61,38 @@ impl SlashCommand for ActiveSlashCommand {
|
||||
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let path = snapshot.resolve_file_path(cx, true);
|
||||
let text = cx.background_executor().spawn({
|
||||
let task = cx.background_executor().spawn({
|
||||
let path = path.clone();
|
||||
async move {
|
||||
let path = path
|
||||
.as_ref()
|
||||
.map(|path| path.to_string_lossy())
|
||||
.unwrap_or_else(|| Cow::Borrowed("untitled"));
|
||||
|
||||
let mut output = String::with_capacity(path.len() + snapshot.len() + 9);
|
||||
output.push_str("```");
|
||||
output.push_str(&path);
|
||||
output.push('\n');
|
||||
let mut output = String::new();
|
||||
output.push_str(&codeblock_fence_for_path(path.as_deref(), None));
|
||||
for chunk in snapshot.as_rope().chunks() {
|
||||
output.push_str(chunk);
|
||||
}
|
||||
if !output.ends_with('\n') {
|
||||
output.push('\n');
|
||||
}
|
||||
output.push_str("```");
|
||||
output
|
||||
output.push_str("```\n");
|
||||
let has_diagnostics =
|
||||
write_single_file_diagnostics(&mut output, path.as_deref(), &snapshot);
|
||||
if output.ends_with('\n') {
|
||||
output.pop();
|
||||
}
|
||||
(output, has_diagnostics)
|
||||
}
|
||||
});
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let text = text.await;
|
||||
let (text, has_diagnostics) = task.await;
|
||||
let range = 0..text.len();
|
||||
Ok(SlashCommandOutput {
|
||||
text,
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
sections: vec![build_entry_output_section(
|
||||
range,
|
||||
render_placeholder: Arc::new(move |id, unfold, _| {
|
||||
FilePlaceholder {
|
||||
id,
|
||||
path: path.clone(),
|
||||
line_range: None,
|
||||
unfold,
|
||||
}
|
||||
.into_any_element()
|
||||
}),
|
||||
}],
|
||||
path.as_deref(),
|
||||
false,
|
||||
None,
|
||||
)],
|
||||
run_commands_in_text: has_diagnostics,
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
79
crates/assistant/src/slash_command/default_command.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use super::{SlashCommand, SlashCommandOutput};
|
||||
use crate::prompt_library::PromptStore;
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::SlashCommandOutputSection;
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use language::LspAdapterDelegate;
|
||||
use std::{
|
||||
fmt::Write,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct DefaultSlashCommand;
|
||||
|
||||
impl SlashCommand for DefaultSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
"default".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"insert default prompt".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
"Insert Default Prompt".into()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
_query: String,
|
||||
_cancellation_flag: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
_argument: Option<&str>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let store = PromptStore::global(cx);
|
||||
cx.background_executor().spawn(async move {
|
||||
let store = store.await?;
|
||||
let prompts = store.default_prompt_metadata();
|
||||
|
||||
let mut text = String::new();
|
||||
text.push('\n');
|
||||
for prompt in prompts {
|
||||
if let Some(title) = prompt.title {
|
||||
writeln!(text, "/prompt {}", title).unwrap();
|
||||
}
|
||||
}
|
||||
text.pop();
|
||||
|
||||
if text.is_empty() {
|
||||
text.push('\n');
|
||||
}
|
||||
|
||||
Ok(SlashCommandOutput {
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range: 0..text.len(),
|
||||
icon: IconName::Library,
|
||||
label: "Default".into(),
|
||||
}],
|
||||
text,
|
||||
run_commands_in_text: true,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
490
crates/assistant/src/slash_command/diagnostics_command.rs
Normal file
@@ -0,0 +1,490 @@
|
||||
use super::{create_label_for_command, SlashCommand, SlashCommandOutput};
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::SlashCommandOutputSection;
|
||||
use fuzzy::{PathMatch, StringMatchCandidate};
|
||||
use gpui::{AppContext, Model, Task, View, WeakView};
|
||||
use language::{
|
||||
Anchor, BufferSnapshot, DiagnosticEntry, DiagnosticSeverity, LspAdapterDelegate,
|
||||
OffsetRangeExt, ToOffset,
|
||||
};
|
||||
use project::{DiagnosticSummary, PathMatchCandidateSet, Project};
|
||||
use rope::Point;
|
||||
use std::fmt::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{
|
||||
ops::Range,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
use ui::prelude::*;
|
||||
use util::paths::PathMatcher;
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct DiagnosticsCommand;
|
||||
|
||||
impl DiagnosticsCommand {
|
||||
fn search_paths(
|
||||
&self,
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: &View<Workspace>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Vec<PathMatch>> {
|
||||
if query.is_empty() {
|
||||
let workspace = workspace.read(cx);
|
||||
let entries = workspace.recent_navigation_history(Some(10), cx);
|
||||
let path_prefix: Arc<str> = "".into();
|
||||
Task::ready(
|
||||
entries
|
||||
.into_iter()
|
||||
.map(|(entry, _)| PathMatch {
|
||||
score: 0.,
|
||||
positions: Vec::new(),
|
||||
worktree_id: entry.worktree_id.to_usize(),
|
||||
path: entry.path.clone(),
|
||||
path_prefix: path_prefix.clone(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
} else {
|
||||
let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
|
||||
let candidate_sets = worktrees
|
||||
.into_iter()
|
||||
.map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
PathMatchCandidateSet {
|
||||
snapshot: worktree.snapshot(),
|
||||
include_ignored: worktree
|
||||
.root_entry()
|
||||
.map_or(false, |entry| entry.is_ignored),
|
||||
include_root_name: true,
|
||||
candidates: project::Candidates::Entries,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let executor = cx.background_executor().clone();
|
||||
cx.foreground_executor().spawn(async move {
|
||||
fuzzy::match_path_sets(
|
||||
candidate_sets.as_slice(),
|
||||
query.as_str(),
|
||||
None,
|
||||
false,
|
||||
100,
|
||||
&cancellation_flag,
|
||||
executor,
|
||||
)
|
||||
.await
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SlashCommand for DiagnosticsCommand {
|
||||
fn name(&self) -> String {
|
||||
"diagnostics".into()
|
||||
}
|
||||
|
||||
fn label(&self, cx: &AppContext) -> language::CodeLabel {
|
||||
create_label_for_command("diagnostics", &[INCLUDE_WARNINGS_ARGUMENT], cx)
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Insert diagnostics".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
"Insert Diagnostics".into()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
|
||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
};
|
||||
let query = query.split_whitespace().last().unwrap_or("").to_string();
|
||||
|
||||
let paths = self.search_paths(query.clone(), cancellation_flag.clone(), &workspace, cx);
|
||||
let executor = cx.background_executor().clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
let mut matches: Vec<String> = paths
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|path_match| {
|
||||
format!(
|
||||
"{}{}",
|
||||
path_match.path_prefix,
|
||||
path_match.path.to_string_lossy()
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
matches.extend(
|
||||
fuzzy::match_strings(
|
||||
&Options::match_candidates_for_args(),
|
||||
&query,
|
||||
false,
|
||||
10,
|
||||
&cancellation_flag,
|
||||
executor,
|
||||
)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|candidate| candidate.string),
|
||||
);
|
||||
|
||||
Ok(matches)
|
||||
})
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
argument: Option<&str>,
|
||||
workspace: WeakView<Workspace>,
|
||||
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
};
|
||||
|
||||
let options = Options::parse(argument);
|
||||
|
||||
let task = collect_diagnostics(workspace.read(cx).project().clone(), options, cx);
|
||||
cx.spawn(move |_| async move {
|
||||
let Some((text, sections)) = task.await? else {
|
||||
return Ok(SlashCommandOutput::default());
|
||||
};
|
||||
|
||||
Ok(SlashCommandOutput {
|
||||
text,
|
||||
sections: sections
|
||||
.into_iter()
|
||||
.map(|(range, placeholder_type)| SlashCommandOutputSection {
|
||||
range,
|
||||
icon: match placeholder_type {
|
||||
PlaceholderType::Root(_, _) => IconName::ExclamationTriangle,
|
||||
PlaceholderType::File(_) => IconName::File,
|
||||
PlaceholderType::Diagnostic(DiagnosticType::Error, _) => {
|
||||
IconName::XCircle
|
||||
}
|
||||
PlaceholderType::Diagnostic(DiagnosticType::Warning, _) => {
|
||||
IconName::ExclamationTriangle
|
||||
}
|
||||
},
|
||||
label: match placeholder_type {
|
||||
PlaceholderType::Root(summary, source) => {
|
||||
let mut label = String::new();
|
||||
label.push_str("Diagnostics");
|
||||
if let Some(source) = source {
|
||||
write!(label, " ({})", source).unwrap();
|
||||
}
|
||||
|
||||
if summary.error_count > 0 || summary.warning_count > 0 {
|
||||
label.push(':');
|
||||
|
||||
if summary.error_count > 0 {
|
||||
write!(label, " {} errors", summary.error_count).unwrap();
|
||||
if summary.warning_count > 0 {
|
||||
label.push_str(",");
|
||||
}
|
||||
}
|
||||
|
||||
if summary.warning_count > 0 {
|
||||
write!(label, " {} warnings", summary.warning_count)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
label.into()
|
||||
}
|
||||
PlaceholderType::File(file_path) => file_path.into(),
|
||||
PlaceholderType::Diagnostic(_, message) => message.into(),
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
run_commands_in_text: false,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Options {
|
||||
include_warnings: bool,
|
||||
path_matcher: Option<PathMatcher>,
|
||||
}
|
||||
|
||||
const INCLUDE_WARNINGS_ARGUMENT: &str = "--include-warnings";
|
||||
|
||||
impl Options {
|
||||
fn parse(arguments_line: Option<&str>) -> Self {
|
||||
arguments_line
|
||||
.map(|arguments_line| {
|
||||
let args = arguments_line.split_whitespace().collect::<Vec<_>>();
|
||||
let mut include_warnings = false;
|
||||
let mut path_matcher = None;
|
||||
for arg in args {
|
||||
if arg == INCLUDE_WARNINGS_ARGUMENT {
|
||||
include_warnings = true;
|
||||
} else {
|
||||
path_matcher = PathMatcher::new(&[arg.to_owned()]).log_err();
|
||||
}
|
||||
}
|
||||
Self {
|
||||
include_warnings,
|
||||
path_matcher,
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn match_candidates_for_args() -> [StringMatchCandidate; 1] {
|
||||
[StringMatchCandidate::new(
|
||||
0,
|
||||
INCLUDE_WARNINGS_ARGUMENT.to_string(),
|
||||
)]
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_diagnostics(
|
||||
project: Model<Project>,
|
||||
options: Options,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Option<(String, Vec<(Range<usize>, PlaceholderType)>)>>> {
|
||||
let error_source = if let Some(path_matcher) = &options.path_matcher {
|
||||
debug_assert_eq!(path_matcher.sources().len(), 1);
|
||||
Some(path_matcher.sources().first().cloned().unwrap_or_default())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let glob_is_exact_file_match = if let Some(path) = options
|
||||
.path_matcher
|
||||
.as_ref()
|
||||
.and_then(|pm| pm.sources().first())
|
||||
{
|
||||
PathBuf::try_from(path)
|
||||
.ok()
|
||||
.and_then(|path| {
|
||||
project.read(cx).worktrees().find_map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
let worktree_root_path = Path::new(worktree.root_name());
|
||||
let relative_path = path.strip_prefix(worktree_root_path).ok()?;
|
||||
worktree.absolutize(&relative_path).ok()
|
||||
})
|
||||
})
|
||||
.is_some()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let project_handle = project.downgrade();
|
||||
let diagnostic_summaries: Vec<_> = project
|
||||
.read(cx)
|
||||
.diagnostic_summaries(false, cx)
|
||||
.flat_map(|(path, _, summary)| {
|
||||
let worktree = project.read(cx).worktree_for_id(path.worktree_id, cx)?;
|
||||
let mut path_buf = PathBuf::from(worktree.read(cx).root_name());
|
||||
path_buf.push(&path.path);
|
||||
Some((path, path_buf, summary))
|
||||
})
|
||||
.collect();
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
let mut text = String::new();
|
||||
if let Some(error_source) = error_source.as_ref() {
|
||||
writeln!(text, "diagnostics: {}", error_source).unwrap();
|
||||
} else {
|
||||
writeln!(text, "diagnostics").unwrap();
|
||||
}
|
||||
let mut sections: Vec<(Range<usize>, PlaceholderType)> = Vec::new();
|
||||
|
||||
let mut project_summary = DiagnosticSummary::default();
|
||||
for (project_path, path, summary) in diagnostic_summaries {
|
||||
if let Some(path_matcher) = &options.path_matcher {
|
||||
if !path_matcher.is_match(&path) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
project_summary.error_count += summary.error_count;
|
||||
if options.include_warnings {
|
||||
project_summary.warning_count += summary.warning_count;
|
||||
} else if summary.error_count == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let last_end = text.len();
|
||||
let file_path = path.to_string_lossy().to_string();
|
||||
if !glob_is_exact_file_match {
|
||||
writeln!(&mut text, "{file_path}").unwrap();
|
||||
}
|
||||
|
||||
if let Some(buffer) = project_handle
|
||||
.update(&mut cx, |project, cx| project.open_buffer(project_path, cx))?
|
||||
.await
|
||||
.log_err()
|
||||
{
|
||||
collect_buffer_diagnostics(
|
||||
&mut text,
|
||||
&mut sections,
|
||||
cx.read_model(&buffer, |buffer, _| buffer.snapshot())?,
|
||||
options.include_warnings,
|
||||
);
|
||||
}
|
||||
|
||||
if !glob_is_exact_file_match {
|
||||
sections.push((
|
||||
last_end..text.len().saturating_sub(1),
|
||||
PlaceholderType::File(file_path),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// No diagnostics found
|
||||
if sections.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
sections.push((
|
||||
0..text.len(),
|
||||
PlaceholderType::Root(project_summary, error_source),
|
||||
));
|
||||
Ok(Some((text, sections)))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn buffer_has_error_diagnostics(snapshot: &BufferSnapshot) -> bool {
|
||||
for (_, group) in snapshot.diagnostic_groups(None) {
|
||||
let entry = &group.entries[group.primary_ix];
|
||||
if entry.diagnostic.severity == DiagnosticSeverity::ERROR {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn write_single_file_diagnostics(
|
||||
output: &mut String,
|
||||
path: Option<&Path>,
|
||||
snapshot: &BufferSnapshot,
|
||||
) -> bool {
|
||||
if let Some(path) = path {
|
||||
if buffer_has_error_diagnostics(&snapshot) {
|
||||
output.push_str("/diagnostics ");
|
||||
output.push_str(&path.to_string_lossy());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn collect_buffer_diagnostics(
|
||||
text: &mut String,
|
||||
sections: &mut Vec<(Range<usize>, PlaceholderType)>,
|
||||
snapshot: BufferSnapshot,
|
||||
include_warnings: bool,
|
||||
) {
|
||||
for (_, group) in snapshot.diagnostic_groups(None) {
|
||||
let entry = &group.entries[group.primary_ix];
|
||||
collect_diagnostic(text, sections, entry, &snapshot, include_warnings)
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_diagnostic(
|
||||
text: &mut String,
|
||||
sections: &mut Vec<(Range<usize>, PlaceholderType)>,
|
||||
entry: &DiagnosticEntry<Anchor>,
|
||||
snapshot: &BufferSnapshot,
|
||||
include_warnings: bool,
|
||||
) {
|
||||
const EXCERPT_EXPANSION_SIZE: u32 = 2;
|
||||
const MAX_MESSAGE_LENGTH: usize = 2000;
|
||||
|
||||
let ty = match entry.diagnostic.severity {
|
||||
DiagnosticSeverity::WARNING => {
|
||||
if !include_warnings {
|
||||
return;
|
||||
}
|
||||
DiagnosticType::Warning
|
||||
}
|
||||
DiagnosticSeverity::ERROR => DiagnosticType::Error,
|
||||
_ => return,
|
||||
};
|
||||
let prev_len = text.len();
|
||||
|
||||
let range = entry.range.to_point(snapshot);
|
||||
let diagnostic_row_number = range.start.row + 1;
|
||||
|
||||
let start_row = range.start.row.saturating_sub(EXCERPT_EXPANSION_SIZE);
|
||||
let end_row = (range.end.row + EXCERPT_EXPANSION_SIZE).min(snapshot.max_point().row) + 1;
|
||||
let excerpt_range =
|
||||
Point::new(start_row, 0).to_offset(&snapshot)..Point::new(end_row, 0).to_offset(&snapshot);
|
||||
|
||||
text.push_str("```");
|
||||
if let Some(language_name) = snapshot.language().map(|l| l.code_fence_block_name()) {
|
||||
text.push_str(&language_name);
|
||||
}
|
||||
text.push('\n');
|
||||
|
||||
let mut buffer_text = String::new();
|
||||
for chunk in snapshot.text_for_range(excerpt_range) {
|
||||
buffer_text.push_str(chunk);
|
||||
}
|
||||
|
||||
for (i, line) in buffer_text.lines().enumerate() {
|
||||
let line_number = start_row + i as u32 + 1;
|
||||
writeln!(text, "{}", line).unwrap();
|
||||
|
||||
if line_number == diagnostic_row_number {
|
||||
text.push_str("//");
|
||||
let prev_len = text.len();
|
||||
write!(text, " {}: ", ty.as_str()).unwrap();
|
||||
let padding = text.len() - prev_len;
|
||||
|
||||
let message = util::truncate(&entry.diagnostic.message, MAX_MESSAGE_LENGTH)
|
||||
.replace('\n', format!("\n//{:padding$}", "").as_str());
|
||||
|
||||
writeln!(text, "{message}").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(text, "```").unwrap();
|
||||
sections.push((
|
||||
prev_len..text.len().saturating_sub(1),
|
||||
PlaceholderType::Diagnostic(ty, entry.diagnostic.message.clone()),
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum PlaceholderType {
|
||||
Root(DiagnosticSummary, Option<String>),
|
||||
File(String),
|
||||
Diagnostic(DiagnosticType, String),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum DiagnosticType {
|
||||
Warning,
|
||||
Error,
|
||||
}
|
||||
|
||||
impl DiagnosticType {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
DiagnosticType::Warning => "warning",
|
||||
DiagnosticType::Error => "error",
|
||||
}
|
||||
}
|
||||
}
|
||||
163
crates/assistant/src/slash_command/fetch_command.rs
Normal file
@@ -0,0 +1,163 @@
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
|
||||
use futures::AsyncReadExt;
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler};
|
||||
use http::{AsyncBody, HttpClient, HttpClientWithUrl};
|
||||
use language::LspAdapterDelegate;
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
enum ContentType {
|
||||
Html,
|
||||
Plaintext,
|
||||
Json,
|
||||
}
|
||||
|
||||
pub(crate) struct FetchSlashCommand;
|
||||
|
||||
impl FetchSlashCommand {
|
||||
async fn build_message(http_client: Arc<HttpClientWithUrl>, url: &str) -> Result<String> {
|
||||
let mut url = url.to_owned();
|
||||
if !url.starts_with("https://") {
|
||||
url = format!("https://{url}");
|
||||
}
|
||||
|
||||
let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
|
||||
|
||||
let mut body = Vec::new();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_end(&mut body)
|
||||
.await
|
||||
.context("error reading response body")?;
|
||||
|
||||
if response.status().is_client_error() {
|
||||
let text = String::from_utf8_lossy(body.as_slice());
|
||||
bail!(
|
||||
"status error {}, response: {text:?}",
|
||||
response.status().as_u16()
|
||||
);
|
||||
}
|
||||
|
||||
let Some(content_type) = response.headers().get("content-type") else {
|
||||
bail!("missing Content-Type header");
|
||||
};
|
||||
let content_type = content_type
|
||||
.to_str()
|
||||
.context("invalid Content-Type header")?;
|
||||
let content_type = match content_type {
|
||||
"text/html" => ContentType::Html,
|
||||
"text/plain" => ContentType::Plaintext,
|
||||
"application/json" => ContentType::Json,
|
||||
_ => ContentType::Html,
|
||||
};
|
||||
|
||||
match content_type {
|
||||
ContentType::Html => {
|
||||
let mut handlers: Vec<TagHandler> = vec![
|
||||
Rc::new(RefCell::new(markdown::WebpageChromeRemover)),
|
||||
Rc::new(RefCell::new(markdown::ParagraphHandler)),
|
||||
Rc::new(RefCell::new(markdown::HeadingHandler)),
|
||||
Rc::new(RefCell::new(markdown::ListHandler)),
|
||||
Rc::new(RefCell::new(markdown::TableHandler::new())),
|
||||
Rc::new(RefCell::new(markdown::StyledTextHandler)),
|
||||
];
|
||||
if url.contains("wikipedia.org") {
|
||||
use html_to_markdown::structure::wikipedia;
|
||||
|
||||
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));
|
||||
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler)));
|
||||
handlers.push(Rc::new(
|
||||
RefCell::new(wikipedia::WikipediaCodeHandler::new()),
|
||||
));
|
||||
} else {
|
||||
handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));
|
||||
}
|
||||
|
||||
convert_html_to_markdown(&body[..], &mut handlers)
|
||||
}
|
||||
ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),
|
||||
ContentType::Json => {
|
||||
let json: serde_json::Value = serde_json::from_slice(&body)?;
|
||||
|
||||
Ok(format!(
|
||||
"```json\n{}\n```",
|
||||
serde_json::to_string_pretty(&json)?
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SlashCommand for FetchSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
"fetch".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"insert URL contents".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
"Insert fetched URL contents".into()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
_query: String,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
argument: Option<&str>,
|
||||
workspace: WeakView<Workspace>,
|
||||
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let Some(argument) = argument else {
|
||||
return Task::ready(Err(anyhow!("missing URL")));
|
||||
};
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
};
|
||||
|
||||
let http_client = workspace.read(cx).client().http_client();
|
||||
let url = argument.to_string();
|
||||
|
||||
let text = cx.background_executor().spawn({
|
||||
let url = url.clone();
|
||||
async move { Self::build_message(http_client, &url).await }
|
||||
});
|
||||
|
||||
let url = SharedString::from(url);
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let text = text.await?;
|
||||
let range = 0..text.len();
|
||||
Ok(SlashCommandOutput {
|
||||
text,
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range,
|
||||
icon: IconName::AtSign,
|
||||
label: format!("fetch {}", url).into(),
|
||||
}],
|
||||
run_commands_in_text: false,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,18 @@
|
||||
use super::{SlashCommand, SlashCommandOutput};
|
||||
use super::{diagnostics_command::write_single_file_diagnostics, SlashCommand, SlashCommandOutput};
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::SlashCommandOutputSection;
|
||||
use fuzzy::PathMatch;
|
||||
use gpui::{AppContext, RenderOnce, SharedString, Task, View, WeakView};
|
||||
use language::{LineEnding, LspAdapterDelegate};
|
||||
use project::PathMatchCandidateSet;
|
||||
use gpui::{AppContext, Model, Task, View, WeakView};
|
||||
use language::{BufferSnapshot, LineEnding, LspAdapterDelegate};
|
||||
use project::{PathMatchCandidateSet, Project};
|
||||
use std::{
|
||||
fmt::Write,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
use ui::{prelude::*, ButtonLike, ElevationIndex};
|
||||
use ui::prelude::*;
|
||||
use util::{paths::PathMatcher, ResultExt};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct FileSlashCommand;
|
||||
@@ -58,7 +60,7 @@ impl FileSlashCommand {
|
||||
.root_entry()
|
||||
.map_or(false, |entry| entry.is_ignored),
|
||||
include_root_name: true,
|
||||
directories_only: false,
|
||||
candidates: project::Candidates::Entries,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@@ -86,11 +88,11 @@ impl SlashCommand for FileSlashCommand {
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"insert a file".into()
|
||||
"insert file".into()
|
||||
}
|
||||
|
||||
fn tooltip_text(&self) -> String {
|
||||
"insert file".into()
|
||||
fn menu_text(&self) -> String {
|
||||
"Insert File".into()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
@@ -98,13 +100,13 @@ impl SlashCommand for FileSlashCommand {
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
&self,
|
||||
self: Arc<Self>,
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
|
||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
};
|
||||
|
||||
@@ -139,87 +141,225 @@ impl SlashCommand for FileSlashCommand {
|
||||
return Task::ready(Err(anyhow!("missing path")));
|
||||
};
|
||||
|
||||
let path = PathBuf::from(argument);
|
||||
let abs_path = workspace
|
||||
.read(cx)
|
||||
.visible_worktrees(cx)
|
||||
.find_map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
let worktree_root_path = Path::new(worktree.root_name());
|
||||
let relative_path = path.strip_prefix(worktree_root_path).ok()?;
|
||||
worktree.absolutize(&relative_path).ok()
|
||||
});
|
||||
let task = collect_files(workspace.read(cx).project().clone(), argument, cx);
|
||||
|
||||
let Some(abs_path) = abs_path else {
|
||||
return Task::ready(Err(anyhow!("missing path")));
|
||||
};
|
||||
|
||||
let fs = workspace.read(cx).app_state().fs.clone();
|
||||
let argument = argument.to_string();
|
||||
let text = cx.background_executor().spawn(async move {
|
||||
let mut content = fs.load(&abs_path).await?;
|
||||
LineEnding::normalize(&mut content);
|
||||
let mut output = String::with_capacity(argument.len() + content.len() + 9);
|
||||
output.push_str("```");
|
||||
output.push_str(&argument);
|
||||
output.push('\n');
|
||||
output.push_str(&content);
|
||||
if !output.ends_with('\n') {
|
||||
output.push('\n');
|
||||
}
|
||||
output.push_str("```");
|
||||
anyhow::Ok(output)
|
||||
});
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let text = text.await?;
|
||||
let range = 0..text.len();
|
||||
let (text, ranges) = task.await?;
|
||||
Ok(SlashCommandOutput {
|
||||
text,
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range,
|
||||
render_placeholder: Arc::new(move |id, unfold, _cx| {
|
||||
FilePlaceholder {
|
||||
path: Some(path.clone()),
|
||||
line_range: None,
|
||||
id,
|
||||
unfold,
|
||||
}
|
||||
.into_any_element()
|
||||
}),
|
||||
}],
|
||||
sections: ranges
|
||||
.into_iter()
|
||||
.map(|(range, path, entry_type)| {
|
||||
build_entry_output_section(
|
||||
range,
|
||||
Some(&path),
|
||||
entry_type == EntryType::Directory,
|
||||
None,
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
run_commands_in_text: true,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct FilePlaceholder {
|
||||
pub path: Option<PathBuf>,
|
||||
pub line_range: Option<Range<u32>>,
|
||||
pub id: ElementId,
|
||||
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum EntryType {
|
||||
File,
|
||||
Directory,
|
||||
}
|
||||
|
||||
impl RenderOnce for FilePlaceholder {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
let unfold = self.unfold;
|
||||
let title = if let Some(path) = self.path.as_ref() {
|
||||
SharedString::from(path.to_string_lossy().to_string())
|
||||
} else {
|
||||
SharedString::from("untitled")
|
||||
};
|
||||
fn collect_files(
|
||||
project: Model<Project>,
|
||||
glob_input: &str,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<(String, Vec<(Range<usize>, PathBuf, EntryType)>)>> {
|
||||
let Ok(matcher) = PathMatcher::new(&[glob_input.to_owned()]) else {
|
||||
return Task::ready(Err(anyhow!("invalid path")));
|
||||
};
|
||||
|
||||
ButtonLike::new(self.id)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.child(Icon::new(IconName::File))
|
||||
.child(Label::new(title))
|
||||
.when_some(self.line_range, |button, line_range| {
|
||||
button.child(Label::new(":")).child(Label::new(format!(
|
||||
"{}-{}",
|
||||
line_range.start, line_range.end
|
||||
)))
|
||||
})
|
||||
.on_click(move |_, cx| unfold(cx))
|
||||
let project_handle = project.downgrade();
|
||||
let snapshots = project
|
||||
.read(cx)
|
||||
.worktrees()
|
||||
.map(|worktree| worktree.read(cx).snapshot())
|
||||
.collect::<Vec<_>>();
|
||||
cx.spawn(|mut cx| async move {
|
||||
let mut text = String::new();
|
||||
let mut ranges = Vec::new();
|
||||
for snapshot in snapshots {
|
||||
let worktree_id = snapshot.id();
|
||||
let mut directory_stack: Vec<(Arc<Path>, String, usize)> = Vec::new();
|
||||
let mut folded_directory_names_stack = Vec::new();
|
||||
let mut is_top_level_directory = true;
|
||||
for entry in snapshot.entries(false, 0) {
|
||||
let mut path_including_worktree_name = PathBuf::new();
|
||||
path_including_worktree_name.push(snapshot.root_name());
|
||||
path_including_worktree_name.push(&entry.path);
|
||||
if !matcher.is_match(&path_including_worktree_name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
while let Some((dir, _, _)) = directory_stack.last() {
|
||||
if entry.path.starts_with(dir) {
|
||||
break;
|
||||
}
|
||||
let (_, entry_name, start) = directory_stack.pop().unwrap();
|
||||
ranges.push((
|
||||
start..text.len().saturating_sub(1),
|
||||
PathBuf::from(entry_name),
|
||||
EntryType::Directory,
|
||||
));
|
||||
}
|
||||
|
||||
let filename = entry
|
||||
.path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_str()
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
|
||||
if entry.is_dir() {
|
||||
// Auto-fold directories that contain no files
|
||||
let mut child_entries = snapshot.child_entries(&entry.path);
|
||||
if let Some(child) = child_entries.next() {
|
||||
if child_entries.next().is_none() && child.kind.is_dir() {
|
||||
if is_top_level_directory {
|
||||
is_top_level_directory = false;
|
||||
folded_directory_names_stack.push(
|
||||
path_including_worktree_name.to_string_lossy().to_string(),
|
||||
);
|
||||
} else {
|
||||
folded_directory_names_stack.push(filename.to_string());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Skip empty directories
|
||||
folded_directory_names_stack.clear();
|
||||
continue;
|
||||
}
|
||||
let prefix_paths = folded_directory_names_stack.drain(..).as_slice().join("/");
|
||||
let entry_start = text.len();
|
||||
if prefix_paths.is_empty() {
|
||||
if is_top_level_directory {
|
||||
text.push_str(&path_including_worktree_name.to_string_lossy());
|
||||
is_top_level_directory = false;
|
||||
} else {
|
||||
text.push_str(&filename);
|
||||
}
|
||||
directory_stack.push((entry.path.clone(), filename, entry_start));
|
||||
} else {
|
||||
let entry_name = format!("{}/{}", prefix_paths, &filename);
|
||||
text.push_str(&entry_name);
|
||||
directory_stack.push((entry.path.clone(), entry_name, entry_start));
|
||||
}
|
||||
text.push('\n');
|
||||
} else if entry.is_file() {
|
||||
let Some(open_buffer_task) = project_handle
|
||||
.update(&mut cx, |project, cx| {
|
||||
project.open_buffer((worktree_id, &entry.path), cx)
|
||||
})
|
||||
.ok()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if let Some(buffer) = open_buffer_task.await.log_err() {
|
||||
let snapshot = cx.read_model(&buffer, |buffer, _| buffer.snapshot())?;
|
||||
let prev_len = text.len();
|
||||
collect_file_content(&mut text, &snapshot, filename.clone());
|
||||
text.push('\n');
|
||||
if !write_single_file_diagnostics(
|
||||
&mut text,
|
||||
Some(&path_including_worktree_name),
|
||||
&snapshot,
|
||||
) {
|
||||
text.pop();
|
||||
}
|
||||
ranges.push((
|
||||
prev_len..text.len(),
|
||||
PathBuf::from(filename),
|
||||
EntryType::File,
|
||||
));
|
||||
text.push('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while let Some((dir, _, start)) = directory_stack.pop() {
|
||||
let mut root_path = PathBuf::new();
|
||||
root_path.push(snapshot.root_name());
|
||||
root_path.push(&dir);
|
||||
ranges.push((start..text.len(), root_path, EntryType::Directory));
|
||||
}
|
||||
}
|
||||
Ok((text, ranges))
|
||||
})
|
||||
}
|
||||
|
||||
fn collect_file_content(buffer: &mut String, snapshot: &BufferSnapshot, filename: String) {
|
||||
let mut content = snapshot.text();
|
||||
LineEnding::normalize(&mut content);
|
||||
buffer.reserve(filename.len() + content.len() + 9);
|
||||
buffer.push_str(&codeblock_fence_for_path(
|
||||
Some(&PathBuf::from(filename)),
|
||||
None,
|
||||
));
|
||||
buffer.push_str(&content);
|
||||
if !buffer.ends_with('\n') {
|
||||
buffer.push('\n');
|
||||
}
|
||||
buffer.push_str("```");
|
||||
}
|
||||
|
||||
pub fn codeblock_fence_for_path(path: Option<&Path>, row_range: Option<Range<u32>>) -> String {
|
||||
let mut text = String::new();
|
||||
write!(text, "```").unwrap();
|
||||
|
||||
if let Some(path) = path {
|
||||
if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) {
|
||||
write!(text, "{} ", extension).unwrap();
|
||||
}
|
||||
|
||||
write!(text, "{}", path.display()).unwrap();
|
||||
} else {
|
||||
write!(text, "untitled").unwrap();
|
||||
}
|
||||
|
||||
if let Some(row_range) = row_range {
|
||||
write!(text, ":{}-{}", row_range.start + 1, row_range.end + 1).unwrap();
|
||||
}
|
||||
|
||||
text.push('\n');
|
||||
text
|
||||
}
|
||||
|
||||
pub fn build_entry_output_section(
|
||||
range: Range<usize>,
|
||||
path: Option<&Path>,
|
||||
is_directory: bool,
|
||||
line_range: Option<Range<u32>>,
|
||||
) -> SlashCommandOutputSection<usize> {
|
||||
let mut label = if let Some(path) = path {
|
||||
path.to_string_lossy().to_string()
|
||||
} else {
|
||||
"untitled".to_string()
|
||||
};
|
||||
if let Some(line_range) = line_range {
|
||||
write!(label, ":{}-{}", line_range.start, line_range.end).unwrap();
|
||||
}
|
||||
|
||||
let icon = if is_directory {
|
||||
IconName::Folder
|
||||
} else {
|
||||
IconName::File
|
||||
};
|
||||
|
||||
SlashCommandOutputSection {
|
||||
range,
|
||||
icon,
|
||||
label: label.into(),
|
||||
}
|
||||
}
|
||||
|
||||
82
crates/assistant/src/slash_command/now_command.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
|
||||
use chrono::{DateTime, Local};
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use language::LspAdapterDelegate;
|
||||
use ui::{prelude::*, ButtonLike, ElevationIndex};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct NowSlashCommand;
|
||||
|
||||
impl SlashCommand for NowSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
"now".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"insert the current date and time".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
"Insert current date and time".into()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
_query: String,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
_argument: Option<&str>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||
_cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let now = Local::now();
|
||||
let text = format!("Today is {now}.", now = now.to_rfc3339());
|
||||
let range = 0..text.len();
|
||||
|
||||
Task::ready(Ok(SlashCommandOutput {
|
||||
text,
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range,
|
||||
icon: IconName::CountdownTimer,
|
||||
label: now.to_rfc3339().into(),
|
||||
}],
|
||||
run_commands_in_text: false,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct NowPlaceholder {
|
||||
pub id: ElementId,
|
||||
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
|
||||
pub now: DateTime<Local>,
|
||||
}
|
||||
|
||||
impl RenderOnce for NowPlaceholder {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
let unfold = self.unfold;
|
||||
|
||||
ButtonLike::new(self.id)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.child(Icon::new(IconName::CountdownTimer))
|
||||
.child(Label::new(self.now.to_rfc3339()))
|
||||
.on_click(move |_, cx| unfold(cx))
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ use std::{
|
||||
path::Path,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
use ui::{prelude::*, ButtonLike, ElevationIndex};
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct ProjectSlashCommand;
|
||||
@@ -94,18 +94,18 @@ impl SlashCommand for ProjectSlashCommand {
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"insert current project context".into()
|
||||
"insert project metadata".into()
|
||||
}
|
||||
|
||||
fn tooltip_text(&self) -> String {
|
||||
"insert current project context".into()
|
||||
fn menu_text(&self) -> String {
|
||||
"Insert Project Metadata".into()
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
&self,
|
||||
self: Arc<Self>,
|
||||
_query: String,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
@@ -138,16 +138,10 @@ impl SlashCommand for ProjectSlashCommand {
|
||||
text,
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range,
|
||||
render_placeholder: Arc::new(move |id, unfold, _cx| {
|
||||
ButtonLike::new(id)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.child(Icon::new(IconName::FileTree))
|
||||
.child(Label::new("Project"))
|
||||
.on_click(move |_, cx| unfold(cx))
|
||||
.into_any_element()
|
||||
}),
|
||||
icon: IconName::FileTree,
|
||||
label: "Project".into(),
|
||||
}],
|
||||
run_commands_in_text: false,
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1,23 +1,14 @@
|
||||
use super::{SlashCommand, SlashCommandOutput};
|
||||
use crate::prompts::PromptLibrary;
|
||||
use crate::prompt_library::PromptStore;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use assistant_slash_command::SlashCommandOutputSection;
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use language::LspAdapterDelegate;
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
use ui::{prelude::*, ButtonLike, ElevationIndex};
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct PromptSlashCommand {
|
||||
library: Arc<PromptLibrary>,
|
||||
}
|
||||
|
||||
impl PromptSlashCommand {
|
||||
pub fn new(library: Arc<PromptLibrary>) -> Self {
|
||||
Self { library }
|
||||
}
|
||||
}
|
||||
pub(crate) struct PromptSlashCommand;
|
||||
|
||||
impl SlashCommand for PromptSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
@@ -25,11 +16,11 @@ impl SlashCommand for PromptSlashCommand {
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"insert a prompt from the library".into()
|
||||
"insert prompt from library".into()
|
||||
}
|
||||
|
||||
fn tooltip_text(&self) -> String {
|
||||
"insert prompt".into()
|
||||
fn menu_text(&self) -> String {
|
||||
"Insert Prompt from Library".into()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
@@ -37,33 +28,18 @@ impl SlashCommand for PromptSlashCommand {
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
&self,
|
||||
self: Arc<Self>,
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_cancellation_flag: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
let library = self.library.clone();
|
||||
let executor = cx.background_executor().clone();
|
||||
let store = PromptStore::global(cx);
|
||||
cx.background_executor().spawn(async move {
|
||||
let candidates = library
|
||||
.prompts()
|
||||
let prompts = store.await?.search(query).await;
|
||||
Ok(prompts
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(ix, prompt)| StringMatchCandidate::new(ix, prompt.1.title().to_string()))
|
||||
.collect::<Vec<_>>();
|
||||
let matches = fuzzy::match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
false,
|
||||
100,
|
||||
&cancellation_flag,
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
Ok(matches
|
||||
.into_iter()
|
||||
.map(|mat| candidates[mat.candidate_id].string.clone())
|
||||
.filter_map(|prompt| Some(prompt.title?.to_string()))
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
@@ -79,38 +55,33 @@ impl SlashCommand for PromptSlashCommand {
|
||||
return Task::ready(Err(anyhow!("missing prompt name")));
|
||||
};
|
||||
|
||||
let library = self.library.clone();
|
||||
let store = PromptStore::global(cx);
|
||||
let title = SharedString::from(title.to_string());
|
||||
let prompt = cx.background_executor().spawn({
|
||||
let title = title.clone();
|
||||
async move {
|
||||
let prompt = library
|
||||
.prompts()
|
||||
.into_iter()
|
||||
.map(|prompt| (prompt.1.title(), prompt))
|
||||
.find(|(t, _)| t == &title)
|
||||
.with_context(|| format!("no prompt found with title {:?}", title))?
|
||||
.1;
|
||||
anyhow::Ok(prompt.1.body())
|
||||
let store = store.await?;
|
||||
let prompt_id = store
|
||||
.id_for_title(&title)
|
||||
.with_context(|| format!("no prompt found with title {:?}", title))?;
|
||||
let body = store.load(prompt_id).await?;
|
||||
anyhow::Ok(body)
|
||||
}
|
||||
});
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let prompt = prompt.await?;
|
||||
let mut prompt = prompt.await?;
|
||||
if prompt.is_empty() {
|
||||
prompt.push('\n');
|
||||
}
|
||||
let range = 0..prompt.len();
|
||||
Ok(SlashCommandOutput {
|
||||
text: prompt,
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range,
|
||||
render_placeholder: Arc::new(move |id, unfold, _cx| {
|
||||
ButtonLike::new(id)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.child(Icon::new(IconName::Library))
|
||||
.child(Label::new(title.clone()))
|
||||
.on_click(move |_, cx| unfold(cx))
|
||||
.into_any_element()
|
||||
}),
|
||||
icon: IconName::Library,
|
||||
label: title,
|
||||
}],
|
||||
run_commands_in_text: true,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,26 +1,55 @@
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
|
||||
use fs::Fs;
|
||||
use futures::AsyncReadExt;
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use gpui::{AppContext, Model, Task, WeakView};
|
||||
use http::{AsyncBody, HttpClient, HttpClientWithUrl};
|
||||
use language::LspAdapterDelegate;
|
||||
use rustdoc_to_markdown::convert_rustdoc_to_markdown;
|
||||
use ui::{prelude::*, ButtonLike, ElevationIndex};
|
||||
use project::{Project, ProjectPath};
|
||||
use rustdoc::{convert_rustdoc_to_markdown, CrateName, LocalProvider, RustdocSource, RustdocStore};
|
||||
use ui::prelude::*;
|
||||
use util::{maybe, ResultExt};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct RustdocSlashCommand;
|
||||
|
||||
impl RustdocSlashCommand {
|
||||
async fn build_message(
|
||||
fs: Arc<dyn Fs>,
|
||||
http_client: Arc<HttpClientWithUrl>,
|
||||
crate_name: String,
|
||||
) -> Result<String> {
|
||||
crate_name: CrateName,
|
||||
module_path: Vec<String>,
|
||||
path_to_cargo_toml: Option<&Path>,
|
||||
) -> Result<(RustdocSource, String)> {
|
||||
let cargo_workspace_root = path_to_cargo_toml.and_then(|path| path.parent());
|
||||
if let Some(cargo_workspace_root) = cargo_workspace_root {
|
||||
let mut local_cargo_doc_path = cargo_workspace_root.join("target/doc");
|
||||
local_cargo_doc_path.push(crate_name.as_ref());
|
||||
if !module_path.is_empty() {
|
||||
local_cargo_doc_path.push(module_path.join("/"));
|
||||
}
|
||||
local_cargo_doc_path.push("index.html");
|
||||
|
||||
if let Ok(contents) = fs.load(&local_cargo_doc_path).await {
|
||||
let (markdown, _items) = convert_rustdoc_to_markdown(contents.as_bytes())?;
|
||||
|
||||
return Ok((RustdocSource::Local, markdown));
|
||||
}
|
||||
}
|
||||
|
||||
let version = "latest";
|
||||
let path = format!(
|
||||
"{crate_name}/{version}/{crate_name}/{module_path}",
|
||||
module_path = module_path.join("/")
|
||||
);
|
||||
|
||||
let mut response = http_client
|
||||
.get(
|
||||
&format!("https://docs.rs/{crate_name}"),
|
||||
&format!("https://docs.rs/{path}"),
|
||||
AsyncBody::default(),
|
||||
true,
|
||||
)
|
||||
@@ -41,7 +70,22 @@ impl RustdocSlashCommand {
|
||||
);
|
||||
}
|
||||
|
||||
convert_rustdoc_to_markdown(&body[..])
|
||||
let (markdown, _items) = convert_rustdoc_to_markdown(&body[..])?;
|
||||
|
||||
Ok((RustdocSource::DocsDotRs, markdown))
|
||||
}
|
||||
|
||||
fn path_to_cargo_toml(project: Model<Project>, cx: &mut AppContext) -> Option<Arc<Path>> {
|
||||
let worktree = project.read(cx).worktrees().next()?;
|
||||
let worktree = worktree.read(cx);
|
||||
let entry = worktree.entry_for_path("Cargo.toml")?;
|
||||
let path = ProjectPath {
|
||||
worktree_id: worktree.id(),
|
||||
path: entry.path.clone(),
|
||||
};
|
||||
Some(Arc::from(
|
||||
project.read(cx).absolute_path(&path, cx)?.as_path(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,11 +95,11 @@ impl SlashCommand for RustdocSlashCommand {
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"insert the docs for a Rust crate".into()
|
||||
"insert Rust docs".into()
|
||||
}
|
||||
|
||||
fn tooltip_text(&self) -> String {
|
||||
"insert rustdoc".into()
|
||||
fn menu_text(&self) -> String {
|
||||
"Insert Rust Documentation".into()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
@@ -63,13 +107,42 @@ impl SlashCommand for RustdocSlashCommand {
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
&self,
|
||||
_query: String,
|
||||
self: Arc<Self>,
|
||||
query: String,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_cx: &mut AppContext,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
let index_provider_deps = maybe!({
|
||||
let workspace = workspace.ok_or_else(|| anyhow!("no workspace"))?;
|
||||
let workspace = workspace
|
||||
.upgrade()
|
||||
.ok_or_else(|| anyhow!("workspace was dropped"))?;
|
||||
let project = workspace.read(cx).project().clone();
|
||||
let fs = project.read(cx).fs().clone();
|
||||
let cargo_workspace_root = Self::path_to_cargo_toml(project, cx)
|
||||
.and_then(|path| path.parent().map(|path| path.to_path_buf()))
|
||||
.ok_or_else(|| anyhow!("no Cargo workspace root found"))?;
|
||||
|
||||
anyhow::Ok((fs, cargo_workspace_root))
|
||||
});
|
||||
|
||||
let store = RustdocStore::global(cx);
|
||||
cx.background_executor().spawn(async move {
|
||||
if let Some((crate_name, rest)) = query.split_once(':') {
|
||||
if rest.is_empty() {
|
||||
if let Some((fs, cargo_workspace_root)) = index_provider_deps.log_err() {
|
||||
let provider = Box::new(LocalProvider::new(fs, cargo_workspace_root));
|
||||
// We don't need to hold onto this task, as the `RustdocStore` will hold it
|
||||
// until it completes.
|
||||
let _ = store.clone().index(crate_name.into(), provider);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let items = store.search(query).await;
|
||||
Ok(items)
|
||||
})
|
||||
}
|
||||
|
||||
fn run(
|
||||
@@ -86,52 +159,80 @@ impl SlashCommand for RustdocSlashCommand {
|
||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
};
|
||||
|
||||
let project = workspace.read(cx).project().clone();
|
||||
let fs = project.read(cx).fs().clone();
|
||||
let http_client = workspace.read(cx).client().http_client();
|
||||
let crate_name = argument.to_string();
|
||||
let path_to_cargo_toml = Self::path_to_cargo_toml(project, cx);
|
||||
|
||||
let mut path_components = argument.split("::");
|
||||
let crate_name = match path_components
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("missing crate name"))
|
||||
{
|
||||
Ok(crate_name) => CrateName::from(crate_name),
|
||||
Err(err) => return Task::ready(Err(err)),
|
||||
};
|
||||
let item_path = path_components.map(ToString::to_string).collect::<Vec<_>>();
|
||||
|
||||
let text = cx.background_executor().spawn({
|
||||
let rustdoc_store = RustdocStore::global(cx);
|
||||
let crate_name = crate_name.clone();
|
||||
async move { Self::build_message(http_client, crate_name).await }
|
||||
let item_path = item_path.clone();
|
||||
async move {
|
||||
let item_docs = rustdoc_store
|
||||
.load(
|
||||
crate_name.clone(),
|
||||
if item_path.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(item_path.join("::"))
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Ok(item_docs) = item_docs {
|
||||
anyhow::Ok((RustdocSource::Index, item_docs.docs().to_owned()))
|
||||
} else {
|
||||
Self::build_message(
|
||||
fs,
|
||||
http_client,
|
||||
crate_name,
|
||||
item_path,
|
||||
path_to_cargo_toml.as_deref(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let crate_name = SharedString::from(crate_name);
|
||||
let module_path = if item_path.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(SharedString::from(item_path.join("::")))
|
||||
};
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let text = text.await?;
|
||||
let (source, text) = text.await?;
|
||||
let range = 0..text.len();
|
||||
let crate_path = module_path
|
||||
.map(|module_path| format!("{}::{}", crate_name, module_path))
|
||||
.unwrap_or_else(|| crate_name.to_string());
|
||||
Ok(SlashCommandOutput {
|
||||
text,
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range,
|
||||
render_placeholder: Arc::new(move |id, unfold, _cx| {
|
||||
RustdocPlaceholder {
|
||||
id,
|
||||
unfold,
|
||||
crate_name: crate_name.clone(),
|
||||
icon: IconName::FileRust,
|
||||
label: format!(
|
||||
"rustdoc ({source}): {crate_path}",
|
||||
source = match source {
|
||||
RustdocSource::Index => "index",
|
||||
RustdocSource::Local => "local",
|
||||
RustdocSource::DocsDotRs => "docs.rs",
|
||||
}
|
||||
.into_any_element()
|
||||
}),
|
||||
)
|
||||
.into(),
|
||||
}],
|
||||
run_commands_in_text: false,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct RustdocPlaceholder {
|
||||
pub id: ElementId,
|
||||
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
|
||||
pub crate_name: SharedString,
|
||||
}
|
||||
|
||||
impl RenderOnce for RustdocPlaceholder {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
let unfold = self.unfold;
|
||||
|
||||
ButtonLike::new(self.id)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.child(Icon::new(IconName::FileRust))
|
||||
.child(Label::new(format!("rustdoc: {}", self.crate_name)))
|
||||
.on_click(move |_, cx| unfold(cx))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
use super::{file_command::FilePlaceholder, SlashCommand, SlashCommandOutput};
|
||||
use super::{
|
||||
create_label_for_command,
|
||||
file_command::{build_entry_output_section, codeblock_fence_for_path},
|
||||
SlashCommand, SlashCommandOutput,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::SlashCommandOutputSection;
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use language::{CodeLabel, HighlightId, LineEnding, LspAdapterDelegate};
|
||||
use language::{CodeLabel, LineEnding, LspAdapterDelegate};
|
||||
use semantic_index::SemanticIndex;
|
||||
use std::{
|
||||
fmt::Write,
|
||||
path::PathBuf,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
use ui::{prelude::*, ButtonLike, ElevationIndex, Icon, IconName};
|
||||
use ui::{prelude::*, IconName};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -21,22 +25,15 @@ impl SlashCommand for SearchSlashCommand {
|
||||
}
|
||||
|
||||
fn label(&self, cx: &AppContext) -> CodeLabel {
|
||||
let mut label = CodeLabel::default();
|
||||
label.push_str("search ", None);
|
||||
label.push_str(
|
||||
"--n",
|
||||
cx.theme().syntax().highlight_id("comment").map(HighlightId),
|
||||
);
|
||||
label.filter_range = 0.."search".len();
|
||||
label
|
||||
create_label_for_command("search", &["--n"], cx)
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"semantically search files".into()
|
||||
"semantic search".into()
|
||||
}
|
||||
|
||||
fn tooltip_text(&self) -> String {
|
||||
"search".into()
|
||||
fn menu_text(&self) -> String {
|
||||
"Semantic Search".into()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
@@ -44,10 +41,10 @@ impl SlashCommand for SearchSlashCommand {
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
&self,
|
||||
self: Arc<Self>,
|
||||
_query: String,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
@@ -125,9 +122,8 @@ impl SlashCommand for SearchSlashCommand {
|
||||
let range_start = result.range.start.min(file_content.len());
|
||||
let range_end = result.range.end.min(file_content.len());
|
||||
|
||||
let start_line =
|
||||
file_content[0..range_start].matches('\n').count() as u32 + 1;
|
||||
let end_line = file_content[0..range_end].matches('\n').count() as u32 + 1;
|
||||
let start_row = file_content[0..range_start].matches('\n').count() as u32;
|
||||
let end_row = file_content[0..range_end].matches('\n').count() as u32;
|
||||
let start_line_byte_offset = file_content[0..range_start]
|
||||
.rfind('\n')
|
||||
.map(|pos| pos + 1)
|
||||
@@ -138,50 +134,37 @@ impl SlashCommand for SearchSlashCommand {
|
||||
.unwrap_or_else(|| file_content.len());
|
||||
|
||||
let section_start_ix = text.len();
|
||||
writeln!(
|
||||
text,
|
||||
"```{}:{}-{}",
|
||||
result.path.display(),
|
||||
start_line,
|
||||
end_line,
|
||||
)
|
||||
.unwrap();
|
||||
text.push_str(&codeblock_fence_for_path(
|
||||
Some(&result.path),
|
||||
Some(start_row..end_row),
|
||||
));
|
||||
|
||||
let mut excerpt =
|
||||
file_content[start_line_byte_offset..end_line_byte_offset].to_string();
|
||||
LineEnding::normalize(&mut excerpt);
|
||||
text.push_str(&excerpt);
|
||||
writeln!(text, "\n```\n").unwrap();
|
||||
let section_end_ix = text.len() - 1;
|
||||
|
||||
sections.push(SlashCommandOutputSection {
|
||||
range: section_start_ix..section_end_ix,
|
||||
render_placeholder: Arc::new(move |id, unfold, _| {
|
||||
FilePlaceholder {
|
||||
id,
|
||||
path: Some(full_path.clone()),
|
||||
line_range: Some(start_line..end_line),
|
||||
unfold,
|
||||
}
|
||||
.into_any_element()
|
||||
}),
|
||||
});
|
||||
sections.push(build_entry_output_section(
|
||||
section_start_ix..section_end_ix,
|
||||
Some(&full_path),
|
||||
false,
|
||||
Some(start_row + 1..end_row + 1),
|
||||
));
|
||||
}
|
||||
|
||||
let query = SharedString::from(query);
|
||||
sections.push(SlashCommandOutputSection {
|
||||
range: 0..text.len(),
|
||||
render_placeholder: Arc::new(move |id, unfold, _cx| {
|
||||
ButtonLike::new(id)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.child(Icon::new(IconName::MagnifyingGlass))
|
||||
.child(Label::new(query.clone()))
|
||||
.on_click(move |_, cx| unfold(cx))
|
||||
.into_any_element()
|
||||
}),
|
||||
icon: IconName::MagnifyingGlass,
|
||||
label: query,
|
||||
});
|
||||
|
||||
SlashCommandOutput { text, sections }
|
||||
SlashCommandOutput {
|
||||
text,
|
||||
sections,
|
||||
run_commands_in_text: false,
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
use super::{file_command::FilePlaceholder, SlashCommand, SlashCommandOutput};
|
||||
use super::{
|
||||
diagnostics_command::write_single_file_diagnostics,
|
||||
file_command::{build_entry_output_section, codeblock_fence_for_path},
|
||||
SlashCommand, SlashCommandOutput,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::SlashCommandOutputSection;
|
||||
use collections::HashMap;
|
||||
use editor::Editor;
|
||||
use gpui::{AppContext, Entity, Task, WeakView};
|
||||
use language::LspAdapterDelegate;
|
||||
use std::{fmt::Write, path::Path, sync::Arc};
|
||||
use ui::{IntoElement, WindowContext};
|
||||
use std::{fmt::Write, sync::Arc};
|
||||
use ui::WindowContext;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct TabsSlashCommand;
|
||||
@@ -17,11 +20,11 @@ impl SlashCommand for TabsSlashCommand {
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"insert content from open tabs".into()
|
||||
"insert open tabs".into()
|
||||
}
|
||||
|
||||
fn tooltip_text(&self) -> String {
|
||||
"insert open tabs".into()
|
||||
fn menu_text(&self) -> String {
|
||||
"Insert Open Tabs".into()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
@@ -29,10 +32,10 @@ impl SlashCommand for TabsSlashCommand {
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
&self,
|
||||
self: Arc<Self>,
|
||||
_query: String,
|
||||
_cancel: Arc<std::sync::atomic::AtomicBool>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
@@ -75,41 +78,38 @@ impl SlashCommand for TabsSlashCommand {
|
||||
|
||||
let mut sections = Vec::new();
|
||||
let mut text = String::new();
|
||||
let mut has_diagnostics = false;
|
||||
for (full_path, buffer, _) in open_buffers {
|
||||
let section_start_ix = text.len();
|
||||
writeln!(
|
||||
text,
|
||||
"```{}\n",
|
||||
full_path
|
||||
.as_deref()
|
||||
.unwrap_or(Path::new("untitled"))
|
||||
.display()
|
||||
)
|
||||
.unwrap();
|
||||
text.push_str(&codeblock_fence_for_path(full_path.as_deref(), None));
|
||||
for chunk in buffer.as_rope().chunks() {
|
||||
text.push_str(chunk);
|
||||
}
|
||||
if !text.ends_with('\n') {
|
||||
text.push('\n');
|
||||
}
|
||||
writeln!(text, "```\n").unwrap();
|
||||
let section_end_ix = text.len() - 1;
|
||||
writeln!(text, "```").unwrap();
|
||||
if write_single_file_diagnostics(&mut text, full_path.as_deref(), &buffer) {
|
||||
has_diagnostics = true;
|
||||
}
|
||||
if !text.ends_with('\n') {
|
||||
text.push('\n');
|
||||
}
|
||||
|
||||
sections.push(SlashCommandOutputSection {
|
||||
range: section_start_ix..section_end_ix,
|
||||
render_placeholder: Arc::new(move |id, unfold, _| {
|
||||
FilePlaceholder {
|
||||
id,
|
||||
path: full_path.clone(),
|
||||
line_range: None,
|
||||
unfold,
|
||||
}
|
||||
.into_any_element()
|
||||
}),
|
||||
});
|
||||
let section_end_ix = text.len() - 1;
|
||||
sections.push(build_entry_output_section(
|
||||
section_start_ix..section_end_ix,
|
||||
full_path.as_deref(),
|
||||
false,
|
||||
None,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(SlashCommandOutput { text, sections })
|
||||
Ok(SlashCommandOutput {
|
||||
text,
|
||||
sections,
|
||||
run_commands_in_text: has_diagnostics,
|
||||
})
|
||||
}),
|
||||
Err(error) => Task::ready(Err(error)),
|
||||
}
|
||||
|
||||
105
crates/assistant/src/slash_command/term_command.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use language::{CodeLabel, LspAdapterDelegate};
|
||||
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
use super::create_label_for_command;
|
||||
|
||||
pub(crate) struct TermSlashCommand;
|
||||
|
||||
const LINE_COUNT_ARG: &str = "--line-count";
|
||||
|
||||
impl SlashCommand for TermSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
"term".into()
|
||||
}
|
||||
|
||||
fn label(&self, cx: &AppContext) -> CodeLabel {
|
||||
create_label_for_command("term", &[LINE_COUNT_ARG], cx)
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"insert terminal output".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
"Insert terminal output".into()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
_query: String,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Ok(vec![LINE_COUNT_ARG.to_string()]))
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
argument: Option<&str>,
|
||||
workspace: WeakView<Workspace>,
|
||||
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
|
||||
};
|
||||
let Some(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
|
||||
return Task::ready(Err(anyhow::anyhow!("no terminal panel open")));
|
||||
};
|
||||
let Some(active_terminal) = terminal_panel
|
||||
.read(cx)
|
||||
.pane()
|
||||
.read(cx)
|
||||
.active_item()
|
||||
.and_then(|t| t.downcast::<TerminalView>())
|
||||
else {
|
||||
return Task::ready(Err(anyhow::anyhow!("no active terminal")));
|
||||
};
|
||||
|
||||
let line_count = argument.and_then(|a| parse_argument(a)).unwrap_or(20);
|
||||
|
||||
let lines = active_terminal
|
||||
.read(cx)
|
||||
.model()
|
||||
.read(cx)
|
||||
.last_n_non_empty_lines(line_count);
|
||||
|
||||
let mut text = String::new();
|
||||
text.push_str("Terminal output:\n");
|
||||
text.push_str(&lines.join("\n"));
|
||||
let range = 0..text.len();
|
||||
|
||||
Task::ready(Ok(SlashCommandOutput {
|
||||
text,
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range,
|
||||
icon: IconName::Terminal,
|
||||
label: "Terminal".into(),
|
||||
}],
|
||||
run_commands_in_text: false,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_argument(argument: &str) -> Option<usize> {
|
||||
let mut args = argument.split(' ');
|
||||
if args.next() == Some(LINE_COUNT_ARG) {
|
||||
if let Some(line_count) = args.next().and_then(|s| s.parse::<usize>().ok()) {
|
||||
return Some(line_count);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -18,4 +18,5 @@ derive_more.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
parking_lot.workspace = true
|
||||
serde.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
mod slash_command_registry;
|
||||
|
||||
use anyhow::Result;
|
||||
use gpui::{AnyElement, AppContext, ElementId, Task, WeakView, WindowContext};
|
||||
use gpui::{AnyElement, AppContext, ElementId, SharedString, Task, WeakView, WindowContext};
|
||||
use language::{CodeLabel, LspAdapterDelegate};
|
||||
use serde::{Deserialize, Serialize};
|
||||
pub use slash_command_registry::*;
|
||||
use std::{
|
||||
ops::Range,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
use workspace::Workspace;
|
||||
use workspace::{ui::IconName, Workspace};
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
SlashCommandRegistry::default_global(cx);
|
||||
@@ -20,12 +21,12 @@ pub trait SlashCommand: 'static + Send + Sync {
|
||||
CodeLabel::plain(self.name(), None)
|
||||
}
|
||||
fn description(&self) -> String;
|
||||
fn tooltip_text(&self) -> String;
|
||||
fn menu_text(&self) -> String;
|
||||
fn complete_argument(
|
||||
&self,
|
||||
self: Arc<Self>,
|
||||
query: String,
|
||||
cancel: Arc<AtomicBool>,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>>;
|
||||
fn requires_argument(&self) -> bool;
|
||||
@@ -49,13 +50,16 @@ pub type RenderFoldPlaceholder = Arc<
|
||||
+ Fn(ElementId, Arc<dyn Fn(&mut WindowContext)>, &mut WindowContext) -> AnyElement,
|
||||
>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SlashCommandOutput {
|
||||
pub text: String,
|
||||
pub sections: Vec<SlashCommandOutputSection<usize>>,
|
||||
pub run_commands_in_text: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct SlashCommandOutputSection<T> {
|
||||
pub range: Range<T>,
|
||||
pub render_placeholder: RenderFoldPlaceholder,
|
||||
pub icon: IconName,
|
||||
pub label: SharedString,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use collections::HashMap;
|
||||
use collections::{BTreeSet, HashMap};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use gpui::Global;
|
||||
use gpui::{AppContext, ReadGlobal};
|
||||
@@ -16,6 +16,7 @@ impl Global for GlobalSlashCommandRegistry {}
|
||||
#[derive(Default)]
|
||||
struct SlashCommandRegistryState {
|
||||
commands: HashMap<Arc<str>, Arc<dyn SlashCommand>>,
|
||||
featured_commands: BTreeSet<Arc<str>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -40,16 +41,19 @@ impl SlashCommandRegistry {
|
||||
Arc::new(Self {
|
||||
state: RwLock::new(SlashCommandRegistryState {
|
||||
commands: HashMap::default(),
|
||||
featured_commands: BTreeSet::default(),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
/// Registers the provided [`SlashCommand`].
|
||||
pub fn register_command(&self, command: impl SlashCommand) {
|
||||
self.state
|
||||
.write()
|
||||
.commands
|
||||
.insert(command.name().into(), Arc::new(command));
|
||||
pub fn register_command(&self, command: impl SlashCommand, is_featured: bool) {
|
||||
let mut state = self.state.write();
|
||||
let command_name: Arc<str> = command.name().into();
|
||||
if is_featured {
|
||||
state.featured_commands.insert(command_name.clone());
|
||||
}
|
||||
state.commands.insert(command_name, Arc::new(command));
|
||||
}
|
||||
|
||||
/// Returns the names of registered [`SlashCommand`]s.
|
||||
@@ -57,6 +61,16 @@ impl SlashCommandRegistry {
|
||||
self.state.read().commands.keys().cloned().collect()
|
||||
}
|
||||
|
||||
/// Returns the names of registered, featured [`SlashCommand`]s.
|
||||
pub fn featured_command_names(&self) -> Vec<Arc<str>> {
|
||||
self.state
|
||||
.read()
|
||||
.featured_commands
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Returns the [`SlashCommand`] with the given name.
|
||||
pub fn command(&self, name: &str) -> Option<Arc<dyn SlashCommand>> {
|
||||
self.state.read().commands.get(name).cloned()
|
||||
|
||||
@@ -23,7 +23,10 @@ use smol::{fs::File, process::Command};
|
||||
use http::{HttpClient, HttpClientWithUrl};
|
||||
use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
|
||||
use std::{
|
||||
env::consts::{ARCH, OS},
|
||||
env::{
|
||||
self,
|
||||
consts::{ARCH, OS},
|
||||
},
|
||||
ffi::OsString,
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
@@ -138,20 +141,29 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
|
||||
let auto_updater = cx.new_model(|cx| {
|
||||
let updater = AutoUpdater::new(version, http_client);
|
||||
|
||||
let mut update_subscription = AutoUpdateSetting::get_global(cx)
|
||||
.0
|
||||
.then(|| updater.start_polling(cx));
|
||||
let poll_for_updates = ReleaseChannel::try_global(cx)
|
||||
.map(|channel| channel.poll_for_updates())
|
||||
.unwrap_or(false);
|
||||
|
||||
cx.observe_global::<SettingsStore>(move |updater, cx| {
|
||||
if AutoUpdateSetting::get_global(cx).0 {
|
||||
if update_subscription.is_none() {
|
||||
update_subscription = Some(updater.start_polling(cx))
|
||||
if option_env!("ZED_UPDATE_EXPLANATION").is_none()
|
||||
&& env::var("ZED_UPDATE_EXPLANATION").is_err()
|
||||
&& poll_for_updates
|
||||
{
|
||||
let mut update_subscription = AutoUpdateSetting::get_global(cx)
|
||||
.0
|
||||
.then(|| updater.start_polling(cx));
|
||||
|
||||
cx.observe_global::<SettingsStore>(move |updater, cx| {
|
||||
if AutoUpdateSetting::get_global(cx).0 {
|
||||
if update_subscription.is_none() {
|
||||
update_subscription = Some(updater.start_polling(cx))
|
||||
}
|
||||
} else {
|
||||
update_subscription.take();
|
||||
}
|
||||
} else {
|
||||
update_subscription.take();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
updater
|
||||
});
|
||||
@@ -159,6 +171,33 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
|
||||
}
|
||||
|
||||
pub fn check(_: &Check, cx: &mut WindowContext) {
|
||||
if let Some(message) = option_env!("ZED_UPDATE_EXPLANATION") {
|
||||
drop(cx.prompt(
|
||||
gpui::PromptLevel::Info,
|
||||
"Zed was installed via a package manager.",
|
||||
Some(message),
|
||||
&["Ok"],
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(message) = env::var("ZED_UPDATE_EXPLANATION").ok() {
|
||||
drop(cx.prompt(
|
||||
gpui::PromptLevel::Info,
|
||||
"Zed was installed via a package manager.",
|
||||
Some(&message),
|
||||
&["Ok"],
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
if !ReleaseChannel::try_global(cx)
|
||||
.map(|channel| channel.poll_for_updates())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(updater) = AutoUpdater::get(cx) {
|
||||
updater.update(cx, |updater, cx| updater.poll(cx));
|
||||
} else {
|
||||
@@ -342,16 +381,6 @@ impl AutoUpdater {
|
||||
}
|
||||
|
||||
async fn update(this: Model<Self>, mut cx: AsyncAppContext) -> Result<()> {
|
||||
// Skip auto-update for flatpaks
|
||||
#[cfg(target_os = "linux")]
|
||||
if matches!(std::env::var("ZED_IS_FLATPAK_INSTALL"), Ok(_)) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.status = AutoUpdateStatus::Idle;
|
||||
cx.notify();
|
||||
})?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (client, current_version) = this.read_with(&cx, |this, _| {
|
||||
(this.http_client.clone(), this.current_version)
|
||||
})?;
|
||||
@@ -509,7 +538,7 @@ async fn install_release_linux(
|
||||
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 home_dir = PathBuf::from(env::var("HOME").context("no HOME env var set")?);
|
||||
|
||||
let extracted = temp_dir.path().join("zed");
|
||||
fs::create_dir_all(&extracted)
|
||||
|
||||
@@ -86,10 +86,16 @@ impl Render for Breadcrumbs {
|
||||
.style(ButtonStyle::Subtle)
|
||||
.on_click(move |_, cx| {
|
||||
if let Some(editor) = editor.upgrade() {
|
||||
outline::toggle(editor, &outline::Toggle, cx)
|
||||
outline::toggle(editor, &editor::actions::ToggleOutline, cx)
|
||||
}
|
||||
})
|
||||
.tooltip(|cx| Tooltip::for_action("Show symbol outline", &outline::Toggle, cx)),
|
||||
.tooltip(|cx| {
|
||||
Tooltip::for_action(
|
||||
"Show symbol outline",
|
||||
&editor::actions::ToggleOutline,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
),
|
||||
None => element
|
||||
// Match the height of the `ButtonLike` in the other arm.
|
||||
|
||||
@@ -13,6 +13,7 @@ path = "src/call.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
no-webrtc = ["live_kit_client/no-webrtc"]
|
||||
test-support = [
|
||||
"client/test-support",
|
||||
"collections/test-support",
|
||||
|
||||
@@ -114,7 +114,6 @@ impl ActiveCall {
|
||||
async fn handle_incoming_call(
|
||||
this: Model<Self>,
|
||||
envelope: TypedEnvelope<proto::IncomingCall>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::Ack> {
|
||||
let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
|
||||
@@ -142,7 +141,6 @@ impl ActiveCall {
|
||||
async fn handle_call_canceled(
|
||||
this: Model<Self>,
|
||||
envelope: TypedEnvelope<proto::CallCanceled>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, _| {
|
||||
|
||||
@@ -267,7 +267,7 @@ impl Room {
|
||||
.await
|
||||
{
|
||||
Ok(()) => Ok(room),
|
||||
Err(error) => Err(anyhow!("room creation failed: {:?}", error)),
|
||||
Err(error) => Err(error.context("room creation failed")),
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -697,7 +697,6 @@ impl Room {
|
||||
async fn handle_room_updated(
|
||||
this: Model<Self>,
|
||||
envelope: TypedEnvelope<proto::RoomUpdated>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let room = envelope
|
||||
|
||||
@@ -138,7 +138,6 @@ impl ChannelBuffer {
|
||||
async fn handle_update_channel_buffer(
|
||||
this: Model<Self>,
|
||||
update_channel_buffer: TypedEnvelope<proto::UpdateChannelBuffer>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let ops = update_channel_buffer
|
||||
@@ -160,7 +159,6 @@ impl ChannelBuffer {
|
||||
async fn handle_update_channel_buffer_collaborators(
|
||||
this: Model<Self>,
|
||||
message: TypedEnvelope<proto::UpdateChannelBufferCollaborators>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
|
||||
@@ -528,7 +528,6 @@ impl ChannelChat {
|
||||
async fn handle_message_sent(
|
||||
this: Model<Self>,
|
||||
message: TypedEnvelope<proto::ChannelMessageSent>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
|
||||
@@ -553,7 +552,6 @@ impl ChannelChat {
|
||||
async fn handle_message_removed(
|
||||
this: Model<Self>,
|
||||
message: TypedEnvelope<proto::RemoveChannelMessage>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
@@ -565,7 +563,6 @@ impl ChannelChat {
|
||||
async fn handle_message_updated(
|
||||
this: Model<Self>,
|
||||
message: TypedEnvelope<proto::ChannelMessageUpdate>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
|
||||
|
||||
@@ -62,6 +62,7 @@ pub struct ChannelStore {
|
||||
opened_buffers: HashMap<ChannelId, OpenedModelHandle<ChannelBuffer>>,
|
||||
opened_chats: HashMap<ChannelId, OpenedModelHandle<ChannelChat>>,
|
||||
client: Arc<Client>,
|
||||
did_subscribe: bool,
|
||||
user_store: Model<UserStore>,
|
||||
_rpc_subscriptions: [Subscription; 2],
|
||||
_watch_connection_status: Task<Option<()>>,
|
||||
@@ -243,6 +244,20 @@ impl ChannelStore {
|
||||
.log_err();
|
||||
}),
|
||||
channel_states: Default::default(),
|
||||
did_subscribe: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initialize(&mut self) {
|
||||
if !self.did_subscribe {
|
||||
if self
|
||||
.client
|
||||
.send(proto::SubscribeToChannels {})
|
||||
.log_err()
|
||||
.is_some()
|
||||
{
|
||||
self.did_subscribe = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -873,7 +888,6 @@ impl ChannelStore {
|
||||
async fn handle_update_channels(
|
||||
this: Model<Self>,
|
||||
message: TypedEnvelope<proto::UpdateChannels>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, _| {
|
||||
@@ -887,7 +901,6 @@ impl ChannelStore {
|
||||
async fn handle_update_user_channels(
|
||||
this: Model<Self>,
|
||||
message: TypedEnvelope<proto::UpdateUserChannels>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
@@ -1035,7 +1048,7 @@ impl ChannelStore {
|
||||
|
||||
fn handle_disconnect(&mut self, wait_for_reconnect: bool, cx: &mut ModelContext<Self>) {
|
||||
cx.notify();
|
||||
|
||||
self.did_subscribe = false;
|
||||
self.disconnect_channel_buffers_task.get_or_insert_with(|| {
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
if wait_for_reconnect {
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::channel_chat::ChannelChatEvent;
|
||||
use super::*;
|
||||
use client::{test::FakeServer, Client, UserStore};
|
||||
use clock::FakeSystemClock;
|
||||
use gpui::{AppContext, Context, Model, TestAppContext};
|
||||
use gpui::{AppContext, Context, Model, SemanticVersion, TestAppContext};
|
||||
use http::FakeHttpClient;
|
||||
use rpc::proto::{self};
|
||||
use settings::SettingsStore;
|
||||
@@ -340,7 +340,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||
fn init_test(cx: &mut AppContext) -> Model<ChannelStore> {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
release_channel::init("0.0.0", cx);
|
||||
release_channel::init(SemanticVersion::default(), cx);
|
||||
client::init_settings(cx);
|
||||
|
||||
let clock = Arc::new(FakeSystemClock::default());
|
||||
|
||||
@@ -19,9 +19,10 @@ path = "src/main.rs"
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
clap.workspace = true
|
||||
libc.workspace = true
|
||||
ipc-channel = "0.18"
|
||||
once_cell.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
release_channel.workspace = true
|
||||
serde.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
use anyhow::{Context, Result};
|
||||
use clap::Parser;
|
||||
use cli::{ipc::IpcOneShotServer, CliRequest, CliResponse, IpcHandshake};
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
env, fs, io,
|
||||
path::{Path, PathBuf},
|
||||
process::ExitStatus,
|
||||
sync::Arc,
|
||||
thread::{self, JoinHandle},
|
||||
};
|
||||
use util::paths::PathLikeWithPosition;
|
||||
@@ -54,7 +56,7 @@ struct Args {
|
||||
fn parse_path_with_position(
|
||||
argument_str: &str,
|
||||
) -> Result<PathLikeWithPosition<PathBuf>, std::convert::Infallible> {
|
||||
PathLikeWithPosition::parse_str(argument_str, |path_str| {
|
||||
PathLikeWithPosition::parse_str(argument_str, |_, path_str| {
|
||||
Ok(Path::new(path_str).to_path_buf())
|
||||
})
|
||||
}
|
||||
@@ -123,26 +125,34 @@ fn main() -> Result<()> {
|
||||
None
|
||||
};
|
||||
|
||||
let sender: JoinHandle<anyhow::Result<()>> = thread::spawn(move || {
|
||||
let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
|
||||
let (tx, rx) = (handshake.requests, handshake.responses);
|
||||
tx.send(CliRequest::Open {
|
||||
paths,
|
||||
wait: args.wait,
|
||||
open_new_workspace,
|
||||
dev_server_token: args.dev_server_token,
|
||||
})?;
|
||||
let exit_status = Arc::new(Mutex::new(None));
|
||||
|
||||
while let Ok(response) = rx.recv() {
|
||||
match response {
|
||||
CliResponse::Ping => {}
|
||||
CliResponse::Stdout { message } => println!("{message}"),
|
||||
CliResponse::Stderr { message } => eprintln!("{message}"),
|
||||
CliResponse::Exit { status } => std::process::exit(status),
|
||||
let sender: JoinHandle<anyhow::Result<()>> = thread::spawn({
|
||||
let exit_status = exit_status.clone();
|
||||
move || {
|
||||
let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
|
||||
let (tx, rx) = (handshake.requests, handshake.responses);
|
||||
tx.send(CliRequest::Open {
|
||||
paths,
|
||||
wait: args.wait,
|
||||
open_new_workspace,
|
||||
dev_server_token: args.dev_server_token,
|
||||
})?;
|
||||
|
||||
while let Ok(response) = rx.recv() {
|
||||
match response {
|
||||
CliResponse::Ping => {}
|
||||
CliResponse::Stdout { message } => println!("{message}"),
|
||||
CliResponse::Stderr { message } => eprintln!("{message}"),
|
||||
CliResponse::Exit { status } => {
|
||||
exit_status.lock().replace(status);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
});
|
||||
|
||||
if args.foreground {
|
||||
@@ -152,6 +162,9 @@ fn main() -> Result<()> {
|
||||
sender.join().unwrap()?;
|
||||
}
|
||||
|
||||
if let Some(exit_status) = exit_status.lock().take() {
|
||||
std::process::exit(exit_status);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -161,10 +174,7 @@ mod linux {
|
||||
env,
|
||||
ffi::OsString,
|
||||
io,
|
||||
os::{
|
||||
linux::net::SocketAddrExt,
|
||||
unix::net::{SocketAddr, UnixDatagram},
|
||||
},
|
||||
os::unix::net::{SocketAddr, UnixDatagram},
|
||||
path::{Path, PathBuf},
|
||||
process::{self, ExitStatus},
|
||||
thread,
|
||||
@@ -191,14 +201,15 @@ mod linux {
|
||||
let cli = env::current_exe()?;
|
||||
let dir = cli
|
||||
.parent()
|
||||
.and_then(Path::parent)
|
||||
.ok_or_else(|| anyhow!("no parent path for cli"))?;
|
||||
|
||||
match dir.join("zed").canonicalize() {
|
||||
match dir.join("libexec").join("zed-editor").canonicalize() {
|
||||
Ok(path) => Ok(path),
|
||||
// development builds have Zed capitalized
|
||||
Err(e) => match dir.join("Zed").canonicalize() {
|
||||
Ok(path) => Ok(path),
|
||||
Err(_) => Err(e),
|
||||
// In development cli and zed are in the ./target/ directory together
|
||||
Err(e) => match cli.parent().unwrap().join("zed").canonicalize() {
|
||||
Ok(path) if path != cli => Ok(path),
|
||||
_ => Err(e),
|
||||
},
|
||||
}
|
||||
}?;
|
||||
@@ -222,12 +233,9 @@ mod linux {
|
||||
}
|
||||
|
||||
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_path = paths::support_dir().join(format!("zed-{}.sock", *RELEASE_CHANNEL));
|
||||
let sock = UnixDatagram::unbound()?;
|
||||
if sock.connect_addr(&sock_addr).is_err() {
|
||||
if sock.connect(&sock_path).is_err() {
|
||||
self.boot_background(ipc_url)?;
|
||||
} else {
|
||||
sock.send(ipc_url.as_bytes())?;
|
||||
@@ -254,10 +262,8 @@ mod linux {
|
||||
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());
|
||||
}
|
||||
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)]);
|
||||
@@ -315,7 +321,7 @@ mod flatpak {
|
||||
if let Some(flatpak_dir) = get_flatpak_dir() {
|
||||
let mut args = vec!["/usr/bin/flatpak-spawn".into(), "--host".into()];
|
||||
args.append(&mut get_xdg_env_args());
|
||||
args.push("--env=ZED_IS_FLATPAK_INSTALL=1".into());
|
||||
args.push("--env=ZED_UPDATE_EXPLANATION=Please use flatpak to update zed".into());
|
||||
args.push(
|
||||
format!(
|
||||
"--env={EXTRA_LIB_ENV_NAME}={}",
|
||||
@@ -333,7 +339,7 @@ mod flatpak {
|
||||
|
||||
if !is_app_location_set {
|
||||
args.push("--zed".into());
|
||||
args.push(flatpak_dir.join("bin").join("zed-app").into());
|
||||
args.push(flatpak_dir.join("libexec").join("zed-editor").into());
|
||||
}
|
||||
|
||||
let error = exec::execvp("/usr/bin/flatpak-spawn", args);
|
||||
@@ -347,8 +353,8 @@ mod flatpak {
|
||||
&& env::var("FLATPAK_ID").map_or(false, |id| id.starts_with("dev.zed.Zed"))
|
||||
{
|
||||
if args.zed.is_none() {
|
||||
args.zed = Some("/app/bin/zed-app".into());
|
||||
env::set_var("ZED_IS_FLATPAK_INSTALL", "1");
|
||||
args.zed = Some("/app/libexec/zed-editor".into());
|
||||
env::set_var("ZED_UPDATE_EXPLANATION", "Please use flatpak to update zed");
|
||||
}
|
||||
}
|
||||
args
|
||||
|
||||
@@ -19,17 +19,18 @@ test-support = ["clock/test-support", "collections/test-support", "gpui/test-sup
|
||||
anyhow.workspace = true
|
||||
async-recursion = "0.3"
|
||||
async-tungstenite = { version = "0.16", features = ["async-std", "async-native-tls"] }
|
||||
async-native-tls = { version = "0.5.0", features = ["vendored"] }
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
http.workspace = true
|
||||
lazy_static.workspace = true
|
||||
log.workspace = true
|
||||
once_cell.workspace = true
|
||||
paths.workspace = true
|
||||
parking_lot.workspace = true
|
||||
postage.workspace = true
|
||||
rand.workspace = true
|
||||
@@ -50,6 +51,7 @@ time.workspace = true
|
||||
tiny_http = "0.8"
|
||||
url.workspace = true
|
||||
util.workspace = true
|
||||
worktree.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
clock = { workspace = true, features = ["test-support"] }
|
||||
@@ -60,9 +62,10 @@ settings = { workspace = true, features = ["test-support"] }
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
http = { workspace = true, features = ["test-support"] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
async-native-tls = {"version" = "0.5.0", features = ["vendored"]}
|
||||
# This is an indirect dependency of async-tungstenite that is included
|
||||
# here so we can vendor libssl with the feature flag.
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["async-native-tls"]
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
cocoa.workspace = true
|
||||
isahc = { workspace = true, features = ["static-curl"] }
|
||||
async-native-tls = { version = "0.5.0", features = ["vendored"] }
|
||||
|
||||
@@ -509,7 +509,7 @@ impl Client {
|
||||
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"),
|
||||
path: paths::config_dir().join("development_auth"),
|
||||
})
|
||||
} else {
|
||||
Arc::new(KeychainCredentialsProvider)
|
||||
@@ -689,6 +689,22 @@ impl Client {
|
||||
entity: WeakModel<E>,
|
||||
handler: H,
|
||||
) -> Subscription
|
||||
where
|
||||
M: EnvelopedMessage,
|
||||
E: 'static,
|
||||
H: 'static + Sync + Fn(Model<E>, TypedEnvelope<M>, AsyncAppContext) -> F + Send + Sync,
|
||||
F: 'static + Future<Output = Result<()>>,
|
||||
{
|
||||
self.add_message_handler_impl(entity, move |model, message, _, cx| {
|
||||
handler(model, message, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn add_message_handler_impl<M, E, H, F>(
|
||||
self: &Arc<Self>,
|
||||
entity: WeakModel<E>,
|
||||
handler: H,
|
||||
) -> Subscription
|
||||
where
|
||||
M: EnvelopedMessage,
|
||||
E: 'static,
|
||||
@@ -737,19 +753,11 @@ impl Client {
|
||||
where
|
||||
M: RequestMessage,
|
||||
E: 'static,
|
||||
H: 'static
|
||||
+ Sync
|
||||
+ Fn(Model<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F
|
||||
+ Send
|
||||
+ Sync,
|
||||
H: 'static + Sync + Fn(Model<E>, TypedEnvelope<M>, AsyncAppContext) -> F + Send + Sync,
|
||||
F: 'static + Future<Output = Result<M::Response>>,
|
||||
{
|
||||
self.add_message_handler(model, move |handle, envelope, this, cx| {
|
||||
Self::respond_to_request(
|
||||
envelope.receipt(),
|
||||
handler(handle, envelope, this.clone(), cx),
|
||||
this,
|
||||
)
|
||||
self.add_message_handler_impl(model, move |handle, envelope, this, cx| {
|
||||
Self::respond_to_request(envelope.receipt(), handler(handle, envelope, cx), this)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -757,11 +765,11 @@ impl Client {
|
||||
where
|
||||
M: EntityMessage,
|
||||
E: 'static,
|
||||
H: 'static + Fn(Model<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F + Send + Sync,
|
||||
H: 'static + Fn(Model<E>, TypedEnvelope<M>, AsyncAppContext) -> F + Send + Sync,
|
||||
F: 'static + Future<Output = Result<()>>,
|
||||
{
|
||||
self.add_entity_message_handler::<M, E, _, _>(move |subscriber, message, client, cx| {
|
||||
handler(subscriber.downcast::<E>().unwrap(), message, client, cx)
|
||||
self.add_entity_message_handler::<M, E, _, _>(move |subscriber, message, _, cx| {
|
||||
handler(subscriber.downcast::<E>().unwrap(), message, cx)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -808,13 +816,13 @@ impl Client {
|
||||
where
|
||||
M: EntityMessage + RequestMessage,
|
||||
E: 'static,
|
||||
H: 'static + Fn(Model<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F + Send + Sync,
|
||||
H: 'static + Fn(Model<E>, TypedEnvelope<M>, AsyncAppContext) -> F + Send + Sync,
|
||||
F: 'static + Future<Output = Result<M::Response>>,
|
||||
{
|
||||
self.add_model_message_handler(move |entity, envelope, client, cx| {
|
||||
self.add_entity_message_handler::<M, E, _, _>(move |entity, envelope, client, cx| {
|
||||
Self::respond_to_request::<M, _>(
|
||||
envelope.receipt(),
|
||||
handler(entity, envelope, client.clone(), cx),
|
||||
handler(entity.downcast::<E>().unwrap(), envelope, cx),
|
||||
client,
|
||||
)
|
||||
})
|
||||
@@ -1429,6 +1437,31 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_dynamic(
|
||||
&self,
|
||||
envelope: proto::Envelope,
|
||||
request_type: &'static str,
|
||||
) -> impl Future<Output = Result<proto::Envelope>> {
|
||||
let client_id = self.id();
|
||||
log::debug!(
|
||||
"rpc request start. client_id:{}. name:{}",
|
||||
client_id,
|
||||
request_type
|
||||
);
|
||||
let response = self
|
||||
.connection_id()
|
||||
.map(|conn_id| self.peer.request_dynamic(conn_id, envelope, request_type));
|
||||
async move {
|
||||
let response = response?.await;
|
||||
log::debug!(
|
||||
"rpc request finish. client_id:{}. name:{}",
|
||||
client_id,
|
||||
request_type
|
||||
);
|
||||
Ok(response?.0)
|
||||
}
|
||||
}
|
||||
|
||||
fn respond<T: RequestMessage>(&self, receipt: Receipt<T>, response: T::Response) -> Result<()> {
|
||||
log::debug!("rpc respond. client_id:{}. name:{}", self.id(), T::NAME);
|
||||
self.peer.respond(receipt, response)
|
||||
@@ -1704,6 +1737,7 @@ mod tests {
|
||||
use gpui::{BackgroundExecutor, Context, TestAppContext};
|
||||
use http::FakeHttpClient;
|
||||
use parking_lot::Mutex;
|
||||
use proto::TypedEnvelope;
|
||||
use settings::SettingsStore;
|
||||
use std::future;
|
||||
|
||||
@@ -1886,7 +1920,7 @@ mod tests {
|
||||
let (done_tx1, mut done_rx1) = smol::channel::unbounded();
|
||||
let (done_tx2, mut done_rx2) = smol::channel::unbounded();
|
||||
client.add_model_message_handler(
|
||||
move |model: Model<TestModel>, _: TypedEnvelope<proto::JoinProject>, _, mut cx| {
|
||||
move |model: Model<TestModel>, _: TypedEnvelope<proto::JoinProject>, mut cx| {
|
||||
match model.update(&mut cx, |model, _| model.id).unwrap() {
|
||||
1 => done_tx1.try_send(()).unwrap(),
|
||||
2 => done_tx2.try_send(()).unwrap(),
|
||||
@@ -1948,7 +1982,7 @@ mod tests {
|
||||
let (done_tx2, mut done_rx2) = smol::channel::unbounded();
|
||||
let subscription1 = client.add_message_handler(
|
||||
model.downgrade(),
|
||||
move |_, _: TypedEnvelope<proto::Ping>, _, _| {
|
||||
move |_, _: TypedEnvelope<proto::Ping>, _| {
|
||||
done_tx1.try_send(()).unwrap();
|
||||
async { Ok(()) }
|
||||
},
|
||||
@@ -1956,7 +1990,7 @@ mod tests {
|
||||
drop(subscription1);
|
||||
let _subscription2 = client.add_message_handler(
|
||||
model.downgrade(),
|
||||
move |_, _: TypedEnvelope<proto::Ping>, _, _| {
|
||||
move |_, _: TypedEnvelope<proto::Ping>, _| {
|
||||
done_tx2.try_send(()).unwrap();
|
||||
async { Ok(()) }
|
||||
},
|
||||
@@ -1982,7 +2016,7 @@ mod tests {
|
||||
let (done_tx, mut done_rx) = smol::channel::unbounded();
|
||||
let subscription = client.add_message_handler(
|
||||
model.clone().downgrade(),
|
||||
move |model: Model<TestModel>, _: TypedEnvelope<proto::Ping>, _, mut cx| {
|
||||
move |model: Model<TestModel>, _: TypedEnvelope<proto::Ping>, mut cx| {
|
||||
model
|
||||
.update(&mut cx, |model, _| model.subscription.take())
|
||||
.unwrap();
|
||||
|
||||
@@ -3,8 +3,9 @@ mod event_coalescer;
|
||||
use crate::{ChannelId, TelemetrySettings};
|
||||
use chrono::{DateTime, Utc};
|
||||
use clock::SystemClock;
|
||||
use collections::{HashMap, HashSet};
|
||||
use futures::Future;
|
||||
use gpui::{AppContext, AppMetadata, BackgroundExecutor, Task};
|
||||
use gpui::{AppContext, BackgroundExecutor, Task};
|
||||
use http::{self, HttpClient, HttpClientWithUrl, Method};
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::Mutex;
|
||||
@@ -23,6 +24,7 @@ use tempfile::NamedTempFile;
|
||||
#[cfg(not(debug_assertions))]
|
||||
use util::ResultExt;
|
||||
use util::TryFutureExt;
|
||||
use worktree::{UpdatedEntriesSet, WorktreeId};
|
||||
|
||||
use self::event_coalescer::EventCoalescer;
|
||||
|
||||
@@ -39,7 +41,6 @@ struct TelemetryState {
|
||||
installation_id: Option<Arc<str>>, // Per app installation (different for dev, nightly, preview, and stable)
|
||||
session_id: Option<String>, // Per app launch
|
||||
release_channel: Option<&'static str>,
|
||||
app_metadata: AppMetadata,
|
||||
architecture: &'static str,
|
||||
events_queue: Vec<EventWrapper>,
|
||||
flush_events_task: Option<Task<()>>,
|
||||
@@ -48,6 +49,29 @@ struct TelemetryState {
|
||||
first_event_date_time: Option<DateTime<Utc>>,
|
||||
event_coalescer: EventCoalescer,
|
||||
max_queue_size: usize,
|
||||
worktree_id_map: WorktreeIdMap,
|
||||
|
||||
os_name: String,
|
||||
app_version: String,
|
||||
os_version: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct WorktreeIdMap(HashMap<String, ProjectCache>);
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ProjectCache {
|
||||
name: String,
|
||||
worktree_ids_reported: HashSet<WorktreeId>,
|
||||
}
|
||||
|
||||
impl ProjectCache {
|
||||
fn new(name: String) -> Self {
|
||||
Self {
|
||||
name,
|
||||
worktree_ids_reported: HashSet::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -71,6 +95,87 @@ static ZED_CLIENT_CHECKSUM_SEED: Lazy<Option<Vec<u8>>> = Lazy::new(|| {
|
||||
})
|
||||
});
|
||||
|
||||
pub fn os_name() -> String {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
"macOS".to_string()
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
format!("Linux {}", gpui::guess_compositor())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
"Windows".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Note: This might do blocking IO! Only call from background threads
|
||||
pub fn os_version() -> String {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use cocoa::base::nil;
|
||||
use cocoa::foundation::NSProcessInfo;
|
||||
|
||||
unsafe {
|
||||
let process_info = cocoa::foundation::NSProcessInfo::processInfo(nil);
|
||||
let version = process_info.operatingSystemVersion();
|
||||
gpui::SemanticVersion::new(
|
||||
version.majorVersion as usize,
|
||||
version.minorVersion as usize,
|
||||
version.patchVersion as usize,
|
||||
)
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use std::path::Path;
|
||||
|
||||
let content = if let Ok(file) = std::fs::read_to_string(&Path::new("/etc/os-release")) {
|
||||
file
|
||||
} else if let Ok(file) = std::fs::read_to_string(&Path::new("/usr/lib/os-release")) {
|
||||
file
|
||||
} else {
|
||||
log::error!("Failed to load /etc/os-release, /usr/lib/os-release");
|
||||
"".to_string()
|
||||
};
|
||||
let mut name = "unknown".to_string();
|
||||
let mut version = "unknown".to_string();
|
||||
|
||||
for line in content.lines() {
|
||||
if line.starts_with("ID=") {
|
||||
name = line.trim_start_matches("ID=").trim_matches('"').to_string();
|
||||
}
|
||||
if line.starts_with("VERSION_ID=") {
|
||||
version = line
|
||||
.trim_start_matches("VERSION_ID=")
|
||||
.trim_matches('"')
|
||||
.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
format!("{} {}", name, version)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let mut info = unsafe { std::mem::zeroed() };
|
||||
let status = unsafe { windows::Wdk::System::SystemServices::RtlGetVersion(&mut info) };
|
||||
if status.is_ok() {
|
||||
gpui::SemanticVersion::new(
|
||||
info.dwMajorVersion as _,
|
||||
info.dwMinorVersion as _,
|
||||
info.dwBuildNumber as _,
|
||||
)
|
||||
.to_string()
|
||||
} else {
|
||||
"unknown".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Telemetry {
|
||||
pub fn new(
|
||||
clock: Arc<dyn SystemClock>,
|
||||
@@ -84,7 +189,6 @@ impl Telemetry {
|
||||
|
||||
let state = Arc::new(Mutex::new(TelemetryState {
|
||||
settings: *TelemetrySettings::get_global(cx),
|
||||
app_metadata: cx.app_metadata(),
|
||||
architecture: env::consts::ARCH,
|
||||
release_channel,
|
||||
installation_id: None,
|
||||
@@ -97,6 +201,20 @@ impl Telemetry {
|
||||
first_event_date_time: None,
|
||||
event_coalescer: EventCoalescer::new(clock.clone()),
|
||||
max_queue_size: MAX_QUEUE_LEN,
|
||||
worktree_id_map: WorktreeIdMap(HashMap::from_iter([
|
||||
(
|
||||
"yarn.lock".to_string(),
|
||||
ProjectCache::new("yarn".to_string()),
|
||||
),
|
||||
(
|
||||
"package.json".to_string(),
|
||||
ProjectCache::new("node".to_string()),
|
||||
),
|
||||
])),
|
||||
|
||||
os_version: None,
|
||||
os_name: os_name(),
|
||||
app_version: release_channel::AppVersion::global(cx).to_string(),
|
||||
}));
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
@@ -105,7 +223,7 @@ impl Telemetry {
|
||||
let state = state.clone();
|
||||
async move {
|
||||
if let Some(tempfile) =
|
||||
NamedTempFile::new_in(util::paths::CONFIG_DIR.as_path()).log_err()
|
||||
NamedTempFile::new_in(paths::config_dir().as_path()).log_err()
|
||||
{
|
||||
state.lock().log_file = Some(tempfile);
|
||||
}
|
||||
@@ -168,6 +286,9 @@ impl Telemetry {
|
||||
let mut state = self.state.lock();
|
||||
state.installation_id = installation_id.map(|id| id.into());
|
||||
state.session_id = Some(session_id);
|
||||
state.app_version = release_channel::AppVersion::global(cx).to_string();
|
||||
state.os_name = os_name();
|
||||
|
||||
drop(state);
|
||||
|
||||
let this = self.clone();
|
||||
@@ -360,6 +481,52 @@ impl Telemetry {
|
||||
self.report_event(event)
|
||||
}
|
||||
|
||||
pub fn report_discovered_project_events(
|
||||
self: &Arc<Self>,
|
||||
worktree_id: WorktreeId,
|
||||
updated_entries_set: &UpdatedEntriesSet,
|
||||
) {
|
||||
let project_names: Vec<String> = {
|
||||
let mut state = self.state.lock();
|
||||
state
|
||||
.worktree_id_map
|
||||
.0
|
||||
.iter_mut()
|
||||
.filter_map(|(project_file_name, project_type_telemetry)| {
|
||||
if project_type_telemetry
|
||||
.worktree_ids_reported
|
||||
.contains(&worktree_id)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let project_file_found = updated_entries_set.iter().any(|(path, _, _)| {
|
||||
path.as_ref()
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.map(|name_str| name_str == project_file_name)
|
||||
.unwrap_or(false)
|
||||
});
|
||||
|
||||
if !project_file_found {
|
||||
return None;
|
||||
}
|
||||
|
||||
project_type_telemetry
|
||||
.worktree_ids_reported
|
||||
.insert(worktree_id);
|
||||
|
||||
Some(project_type_telemetry.name.clone())
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
// Done on purpose to avoid calling `self.state.lock()` multiple times
|
||||
for project_name in project_names {
|
||||
self.report_app_event(format!("open {} project", project_name));
|
||||
}
|
||||
}
|
||||
|
||||
fn report_event(self: &Arc<Self>, event: Event) {
|
||||
let mut state = self.state.lock();
|
||||
|
||||
@@ -423,10 +590,6 @@ impl Telemetry {
|
||||
return;
|
||||
}
|
||||
|
||||
if ZED_CLIENT_CHECKSUM_SEED.is_none() {
|
||||
return;
|
||||
};
|
||||
|
||||
let this = self.clone();
|
||||
self.executor
|
||||
.spawn(
|
||||
@@ -445,20 +608,14 @@ impl Telemetry {
|
||||
|
||||
{
|
||||
let state = this.state.lock();
|
||||
|
||||
let request_body = EventRequestBody {
|
||||
installation_id: state.installation_id.as_deref().map(Into::into),
|
||||
session_id: state.session_id.clone(),
|
||||
is_staff: state.is_staff,
|
||||
app_version: state
|
||||
.app_metadata
|
||||
.app_version
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
os_name: state.app_metadata.os_name.to_string(),
|
||||
os_version: state
|
||||
.app_metadata
|
||||
.os_version
|
||||
.map(|version| version.to_string()),
|
||||
app_version: state.app_version.clone(),
|
||||
os_name: state.os_name.clone(),
|
||||
os_version: state.os_version.clone(),
|
||||
architecture: state.architecture.to_string(),
|
||||
|
||||
release_channel: state.release_channel.map(Into::into),
|
||||
@@ -468,9 +625,7 @@ impl Telemetry {
|
||||
serde_json::to_writer(&mut json_bytes, &request_body)?;
|
||||
}
|
||||
|
||||
let Some(checksum) = calculate_json_checksum(&json_bytes) else {
|
||||
return Ok(());
|
||||
};
|
||||
let checksum = calculate_json_checksum(&json_bytes).unwrap_or("".to_string());
|
||||
|
||||
let request = http::Request::builder()
|
||||
.method(Method::POST)
|
||||
|
||||
@@ -242,7 +242,6 @@ impl UserStore {
|
||||
async fn handle_update_invite_info(
|
||||
this: Model<Self>,
|
||||
message: TypedEnvelope<proto::UpdateInviteInfo>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
@@ -258,7 +257,6 @@ impl UserStore {
|
||||
async fn handle_show_contacts(
|
||||
this: Model<Self>,
|
||||
_: TypedEnvelope<proto::ShowContacts>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |_, cx| cx.emit(Event::ShowContacts))?;
|
||||
@@ -272,7 +270,6 @@ impl UserStore {
|
||||
async fn handle_update_contacts(
|
||||
this: Model<Self>,
|
||||
message: TypedEnvelope<proto::UpdateContacts>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, _| {
|
||||
|
||||
@@ -96,6 +96,7 @@ node_runtime.workspace = true
|
||||
notifications = { workspace = true, features = ["test-support"] }
|
||||
pretty_assertions.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
recent_projects = { workspace = true }
|
||||
release_channel.workspace = true
|
||||
dev_server_projects.workspace = true
|
||||
rpc = { workspace = true, features = ["test-support"] }
|
||||
@@ -107,4 +108,5 @@ theme.workspace = true
|
||||
unindent.workspace = true
|
||||
util.workspace = true
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
worktree = { workspace = true, features = ["test-support"] }
|
||||
headless.workspace = true
|
||||
|
||||
@@ -122,6 +122,11 @@ spec:
|
||||
secretKeyRef:
|
||||
name: anthropic
|
||||
key: api_key
|
||||
- name: GOOGLE_AI_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: google-ai
|
||||
key: api_key
|
||||
- name: BLOB_STORE_ACCESS_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
|
||||
@@ -308,6 +308,13 @@ pub async fn post_panic(
|
||||
.map_err(|_| Error::Http(StatusCode::BAD_REQUEST, "invalid json".into()))?;
|
||||
let panic = report.panic;
|
||||
|
||||
if panic.os_name == "Linux" && panic.os_version == Some("1.0.0".to_string()) {
|
||||
return Err(Error::Http(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"invalid os version".into(),
|
||||
))?;
|
||||
}
|
||||
|
||||
tracing::error!(
|
||||
service = "client",
|
||||
version = %panic.app_version,
|
||||
@@ -394,12 +401,7 @@ pub async fn post_events(
|
||||
))?;
|
||||
};
|
||||
|
||||
if checksum != expected {
|
||||
return Err(Error::Http(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"invalid checksum".into(),
|
||||
))?;
|
||||
}
|
||||
let checksum_matched = checksum == expected;
|
||||
|
||||
let request_body: telemetry_events::EventRequestBody =
|
||||
serde_json::from_slice(&body).map_err(|err| {
|
||||
@@ -424,6 +426,7 @@ pub async fn post_events(
|
||||
&request_body,
|
||||
first_event_at,
|
||||
country_code.clone(),
|
||||
checksum_matched,
|
||||
)),
|
||||
// Needed for clients sending old copilot_event types
|
||||
Event::Copilot(_) => {}
|
||||
@@ -436,6 +439,7 @@ pub async fn post_events(
|
||||
&request_body,
|
||||
first_event_at,
|
||||
country_code.clone(),
|
||||
checksum_matched,
|
||||
))
|
||||
}
|
||||
Event::Call(event) => to_upload.call_events.push(CallEventRow::from_event(
|
||||
@@ -443,6 +447,7 @@ pub async fn post_events(
|
||||
&wrapper,
|
||||
&request_body,
|
||||
first_event_at,
|
||||
checksum_matched,
|
||||
)),
|
||||
Event::Assistant(event) => {
|
||||
to_upload
|
||||
@@ -452,6 +457,7 @@ pub async fn post_events(
|
||||
&wrapper,
|
||||
&request_body,
|
||||
first_event_at,
|
||||
checksum_matched,
|
||||
))
|
||||
}
|
||||
Event::Cpu(event) => to_upload.cpu_events.push(CpuEventRow::from_event(
|
||||
@@ -459,36 +465,42 @@ pub async fn post_events(
|
||||
&wrapper,
|
||||
&request_body,
|
||||
first_event_at,
|
||||
checksum_matched,
|
||||
)),
|
||||
Event::Memory(event) => to_upload.memory_events.push(MemoryEventRow::from_event(
|
||||
event.clone(),
|
||||
&wrapper,
|
||||
&request_body,
|
||||
first_event_at,
|
||||
checksum_matched,
|
||||
)),
|
||||
Event::App(event) => to_upload.app_events.push(AppEventRow::from_event(
|
||||
event.clone(),
|
||||
&wrapper,
|
||||
&request_body,
|
||||
first_event_at,
|
||||
checksum_matched,
|
||||
)),
|
||||
Event::Setting(event) => to_upload.setting_events.push(SettingEventRow::from_event(
|
||||
event.clone(),
|
||||
&wrapper,
|
||||
&request_body,
|
||||
first_event_at,
|
||||
checksum_matched,
|
||||
)),
|
||||
Event::Edit(event) => to_upload.edit_events.push(EditEventRow::from_event(
|
||||
event.clone(),
|
||||
&wrapper,
|
||||
&request_body,
|
||||
first_event_at,
|
||||
checksum_matched,
|
||||
)),
|
||||
Event::Action(event) => to_upload.action_events.push(ActionEventRow::from_event(
|
||||
event.clone(),
|
||||
&wrapper,
|
||||
&request_body,
|
||||
first_event_at,
|
||||
checksum_matched,
|
||||
)),
|
||||
Event::Extension(event) => {
|
||||
let metadata = app
|
||||
@@ -503,6 +515,7 @@ pub async fn post_events(
|
||||
&request_body,
|
||||
metadata,
|
||||
first_event_at,
|
||||
checksum_matched,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -650,29 +663,30 @@ where
|
||||
|
||||
#[derive(Serialize, Debug, clickhouse::Row)]
|
||||
pub struct EditorEventRow {
|
||||
pub installation_id: String,
|
||||
pub operation: String,
|
||||
pub app_version: String,
|
||||
pub file_extension: String,
|
||||
pub os_name: String,
|
||||
pub os_version: String,
|
||||
pub release_channel: String,
|
||||
pub signed_in: bool,
|
||||
pub vim_mode: bool,
|
||||
installation_id: String,
|
||||
operation: String,
|
||||
app_version: String,
|
||||
file_extension: String,
|
||||
os_name: String,
|
||||
os_version: String,
|
||||
release_channel: String,
|
||||
signed_in: bool,
|
||||
vim_mode: bool,
|
||||
#[serde(serialize_with = "serialize_country_code")]
|
||||
pub country_code: String,
|
||||
pub region_code: String,
|
||||
pub city: String,
|
||||
pub time: i64,
|
||||
pub copilot_enabled: bool,
|
||||
pub copilot_enabled_for_language: bool,
|
||||
pub historical_event: bool,
|
||||
pub architecture: String,
|
||||
pub is_staff: Option<bool>,
|
||||
pub session_id: Option<String>,
|
||||
pub major: Option<i32>,
|
||||
pub minor: Option<i32>,
|
||||
pub patch: Option<i32>,
|
||||
country_code: String,
|
||||
region_code: String,
|
||||
city: String,
|
||||
time: i64,
|
||||
copilot_enabled: bool,
|
||||
copilot_enabled_for_language: bool,
|
||||
historical_event: bool,
|
||||
architecture: String,
|
||||
is_staff: Option<bool>,
|
||||
session_id: Option<String>,
|
||||
major: Option<i32>,
|
||||
minor: Option<i32>,
|
||||
patch: Option<i32>,
|
||||
checksum_matched: bool,
|
||||
}
|
||||
|
||||
impl EditorEventRow {
|
||||
@@ -682,6 +696,7 @@ impl EditorEventRow {
|
||||
body: &EventRequestBody,
|
||||
first_event_at: chrono::DateTime<chrono::Utc>,
|
||||
country_code: Option<String>,
|
||||
checksum_matched: bool,
|
||||
) -> Self {
|
||||
let semver = body.semver();
|
||||
let time =
|
||||
@@ -692,6 +707,7 @@ impl EditorEventRow {
|
||||
major: semver.map(|v| v.major() as i32),
|
||||
minor: semver.map(|v| v.minor() as i32),
|
||||
patch: semver.map(|v| v.patch() as i32),
|
||||
checksum_matched,
|
||||
release_channel: body.release_channel.clone().unwrap_or_default(),
|
||||
os_name: body.os_name.clone(),
|
||||
os_version: body.os_version.clone().unwrap_or_default(),
|
||||
@@ -716,25 +732,26 @@ impl EditorEventRow {
|
||||
|
||||
#[derive(Serialize, Debug, clickhouse::Row)]
|
||||
pub struct InlineCompletionEventRow {
|
||||
pub installation_id: String,
|
||||
pub provider: String,
|
||||
pub suggestion_accepted: bool,
|
||||
pub app_version: String,
|
||||
pub file_extension: String,
|
||||
pub os_name: String,
|
||||
pub os_version: String,
|
||||
pub release_channel: String,
|
||||
pub signed_in: bool,
|
||||
installation_id: String,
|
||||
provider: String,
|
||||
suggestion_accepted: bool,
|
||||
app_version: String,
|
||||
file_extension: String,
|
||||
os_name: String,
|
||||
os_version: String,
|
||||
release_channel: String,
|
||||
signed_in: bool,
|
||||
#[serde(serialize_with = "serialize_country_code")]
|
||||
pub country_code: String,
|
||||
pub region_code: String,
|
||||
pub city: String,
|
||||
pub time: i64,
|
||||
pub is_staff: Option<bool>,
|
||||
pub session_id: Option<String>,
|
||||
pub major: Option<i32>,
|
||||
pub minor: Option<i32>,
|
||||
pub patch: Option<i32>,
|
||||
country_code: String,
|
||||
region_code: String,
|
||||
city: String,
|
||||
time: i64,
|
||||
is_staff: Option<bool>,
|
||||
session_id: Option<String>,
|
||||
major: Option<i32>,
|
||||
minor: Option<i32>,
|
||||
patch: Option<i32>,
|
||||
checksum_matched: bool,
|
||||
}
|
||||
|
||||
impl InlineCompletionEventRow {
|
||||
@@ -744,6 +761,7 @@ impl InlineCompletionEventRow {
|
||||
body: &EventRequestBody,
|
||||
first_event_at: chrono::DateTime<chrono::Utc>,
|
||||
country_code: Option<String>,
|
||||
checksum_matched: bool,
|
||||
) -> Self {
|
||||
let semver = body.semver();
|
||||
let time =
|
||||
@@ -754,6 +772,7 @@ impl InlineCompletionEventRow {
|
||||
major: semver.map(|v| v.major() as i32),
|
||||
minor: semver.map(|v| v.minor() as i32),
|
||||
patch: semver.map(|v| v.patch() as i32),
|
||||
checksum_matched,
|
||||
release_channel: body.release_channel.clone().unwrap_or_default(),
|
||||
os_name: body.os_name.clone(),
|
||||
os_version: body.os_version.clone().unwrap_or_default(),
|
||||
@@ -780,6 +799,9 @@ pub struct CallEventRow {
|
||||
minor: Option<i32>,
|
||||
patch: Option<i32>,
|
||||
release_channel: String,
|
||||
os_name: String,
|
||||
os_version: String,
|
||||
checksum_matched: bool,
|
||||
|
||||
// ClientEventBase
|
||||
installation_id: String,
|
||||
@@ -799,6 +821,7 @@ impl CallEventRow {
|
||||
wrapper: &EventWrapper,
|
||||
body: &EventRequestBody,
|
||||
first_event_at: chrono::DateTime<chrono::Utc>,
|
||||
checksum_matched: bool,
|
||||
) -> Self {
|
||||
let semver = body.semver();
|
||||
let time =
|
||||
@@ -809,7 +832,10 @@ impl CallEventRow {
|
||||
major: semver.map(|v| v.major() as i32),
|
||||
minor: semver.map(|v| v.minor() as i32),
|
||||
patch: semver.map(|v| v.patch() as i32),
|
||||
checksum_matched,
|
||||
release_channel: body.release_channel.clone().unwrap_or_default(),
|
||||
os_name: body.os_name.clone(),
|
||||
os_version: body.os_version.clone().unwrap_or_default(),
|
||||
installation_id: body.installation_id.clone().unwrap_or_default(),
|
||||
session_id: body.session_id.clone(),
|
||||
is_staff: body.is_staff,
|
||||
@@ -828,7 +854,10 @@ pub struct AssistantEventRow {
|
||||
major: Option<i32>,
|
||||
minor: Option<i32>,
|
||||
patch: Option<i32>,
|
||||
checksum_matched: bool,
|
||||
release_channel: String,
|
||||
os_name: String,
|
||||
os_version: String,
|
||||
|
||||
// ClientEventBase
|
||||
installation_id: Option<String>,
|
||||
@@ -850,6 +879,7 @@ impl AssistantEventRow {
|
||||
wrapper: &EventWrapper,
|
||||
body: &EventRequestBody,
|
||||
first_event_at: chrono::DateTime<chrono::Utc>,
|
||||
checksum_matched: bool,
|
||||
) -> Self {
|
||||
let semver = body.semver();
|
||||
let time =
|
||||
@@ -860,7 +890,10 @@ impl AssistantEventRow {
|
||||
major: semver.map(|v| v.major() as i32),
|
||||
minor: semver.map(|v| v.minor() as i32),
|
||||
patch: semver.map(|v| v.patch() as i32),
|
||||
checksum_matched,
|
||||
release_channel: body.release_channel.clone().unwrap_or_default(),
|
||||
os_name: body.os_name.clone(),
|
||||
os_version: body.os_version.clone().unwrap_or_default(),
|
||||
installation_id: body.installation_id.clone(),
|
||||
session_id: body.session_id.clone(),
|
||||
is_staff: body.is_staff,
|
||||
@@ -878,18 +911,21 @@ impl AssistantEventRow {
|
||||
|
||||
#[derive(Debug, clickhouse::Row, Serialize)]
|
||||
pub struct CpuEventRow {
|
||||
pub installation_id: Option<String>,
|
||||
pub is_staff: Option<bool>,
|
||||
pub usage_as_percentage: f32,
|
||||
pub core_count: u32,
|
||||
pub app_version: String,
|
||||
pub release_channel: String,
|
||||
pub time: i64,
|
||||
pub session_id: Option<String>,
|
||||
installation_id: Option<String>,
|
||||
is_staff: Option<bool>,
|
||||
usage_as_percentage: f32,
|
||||
core_count: u32,
|
||||
app_version: String,
|
||||
release_channel: String,
|
||||
os_name: String,
|
||||
os_version: String,
|
||||
time: i64,
|
||||
session_id: Option<String>,
|
||||
// pub normalized_cpu_usage: f64, MATERIALIZED
|
||||
pub major: Option<i32>,
|
||||
pub minor: Option<i32>,
|
||||
pub patch: Option<i32>,
|
||||
major: Option<i32>,
|
||||
minor: Option<i32>,
|
||||
patch: Option<i32>,
|
||||
checksum_matched: bool,
|
||||
}
|
||||
|
||||
impl CpuEventRow {
|
||||
@@ -898,6 +934,7 @@ impl CpuEventRow {
|
||||
wrapper: &EventWrapper,
|
||||
body: &EventRequestBody,
|
||||
first_event_at: chrono::DateTime<chrono::Utc>,
|
||||
checksum_matched: bool,
|
||||
) -> Self {
|
||||
let semver = body.semver();
|
||||
let time =
|
||||
@@ -908,7 +945,10 @@ impl CpuEventRow {
|
||||
major: semver.map(|v| v.major() as i32),
|
||||
minor: semver.map(|v| v.minor() as i32),
|
||||
patch: semver.map(|v| v.patch() as i32),
|
||||
checksum_matched,
|
||||
release_channel: body.release_channel.clone().unwrap_or_default(),
|
||||
os_name: body.os_name.clone(),
|
||||
os_version: body.os_version.clone().unwrap_or_default(),
|
||||
installation_id: body.installation_id.clone(),
|
||||
session_id: body.session_id.clone(),
|
||||
is_staff: body.is_staff,
|
||||
@@ -926,7 +966,10 @@ pub struct MemoryEventRow {
|
||||
major: Option<i32>,
|
||||
minor: Option<i32>,
|
||||
patch: Option<i32>,
|
||||
checksum_matched: bool,
|
||||
release_channel: String,
|
||||
os_name: String,
|
||||
os_version: String,
|
||||
|
||||
// ClientEventBase
|
||||
installation_id: Option<String>,
|
||||
@@ -945,6 +988,7 @@ impl MemoryEventRow {
|
||||
wrapper: &EventWrapper,
|
||||
body: &EventRequestBody,
|
||||
first_event_at: chrono::DateTime<chrono::Utc>,
|
||||
checksum_matched: bool,
|
||||
) -> Self {
|
||||
let semver = body.semver();
|
||||
let time =
|
||||
@@ -955,7 +999,10 @@ impl MemoryEventRow {
|
||||
major: semver.map(|v| v.major() as i32),
|
||||
minor: semver.map(|v| v.minor() as i32),
|
||||
patch: semver.map(|v| v.patch() as i32),
|
||||
checksum_matched,
|
||||
release_channel: body.release_channel.clone().unwrap_or_default(),
|
||||
os_name: body.os_name.clone(),
|
||||
os_version: body.os_version.clone().unwrap_or_default(),
|
||||
installation_id: body.installation_id.clone(),
|
||||
session_id: body.session_id.clone(),
|
||||
is_staff: body.is_staff,
|
||||
@@ -973,7 +1020,10 @@ pub struct AppEventRow {
|
||||
major: Option<i32>,
|
||||
minor: Option<i32>,
|
||||
patch: Option<i32>,
|
||||
checksum_matched: bool,
|
||||
release_channel: String,
|
||||
os_name: String,
|
||||
os_version: String,
|
||||
|
||||
// ClientEventBase
|
||||
installation_id: Option<String>,
|
||||
@@ -991,6 +1041,7 @@ impl AppEventRow {
|
||||
wrapper: &EventWrapper,
|
||||
body: &EventRequestBody,
|
||||
first_event_at: chrono::DateTime<chrono::Utc>,
|
||||
checksum_matched: bool,
|
||||
) -> Self {
|
||||
let semver = body.semver();
|
||||
let time =
|
||||
@@ -1001,7 +1052,10 @@ impl AppEventRow {
|
||||
major: semver.map(|v| v.major() as i32),
|
||||
minor: semver.map(|v| v.minor() as i32),
|
||||
patch: semver.map(|v| v.patch() as i32),
|
||||
checksum_matched,
|
||||
release_channel: body.release_channel.clone().unwrap_or_default(),
|
||||
os_name: body.os_name.clone(),
|
||||
os_version: body.os_version.clone().unwrap_or_default(),
|
||||
installation_id: body.installation_id.clone(),
|
||||
session_id: body.session_id.clone(),
|
||||
is_staff: body.is_staff,
|
||||
@@ -1018,7 +1072,10 @@ pub struct SettingEventRow {
|
||||
major: Option<i32>,
|
||||
minor: Option<i32>,
|
||||
patch: Option<i32>,
|
||||
checksum_matched: bool,
|
||||
release_channel: String,
|
||||
os_name: String,
|
||||
os_version: String,
|
||||
|
||||
// ClientEventBase
|
||||
installation_id: Option<String>,
|
||||
@@ -1036,6 +1093,7 @@ impl SettingEventRow {
|
||||
wrapper: &EventWrapper,
|
||||
body: &EventRequestBody,
|
||||
first_event_at: chrono::DateTime<chrono::Utc>,
|
||||
checksum_matched: bool,
|
||||
) -> Self {
|
||||
let semver = body.semver();
|
||||
let time =
|
||||
@@ -1045,8 +1103,11 @@ impl SettingEventRow {
|
||||
app_version: body.app_version.clone(),
|
||||
major: semver.map(|v| v.major() as i32),
|
||||
minor: semver.map(|v| v.minor() as i32),
|
||||
checksum_matched,
|
||||
patch: semver.map(|v| v.patch() as i32),
|
||||
release_channel: body.release_channel.clone().unwrap_or_default(),
|
||||
os_name: body.os_name.clone(),
|
||||
os_version: body.os_version.clone().unwrap_or_default(),
|
||||
installation_id: body.installation_id.clone(),
|
||||
session_id: body.session_id.clone(),
|
||||
is_staff: body.is_staff,
|
||||
@@ -1064,7 +1125,10 @@ pub struct ExtensionEventRow {
|
||||
major: Option<i32>,
|
||||
minor: Option<i32>,
|
||||
patch: Option<i32>,
|
||||
checksum_matched: bool,
|
||||
release_channel: String,
|
||||
os_name: String,
|
||||
os_version: String,
|
||||
|
||||
// ClientEventBase
|
||||
installation_id: Option<String>,
|
||||
@@ -1087,6 +1151,7 @@ impl ExtensionEventRow {
|
||||
body: &EventRequestBody,
|
||||
extension_metadata: Option<ExtensionMetadata>,
|
||||
first_event_at: chrono::DateTime<chrono::Utc>,
|
||||
checksum_matched: bool,
|
||||
) -> Self {
|
||||
let semver = body.semver();
|
||||
let time =
|
||||
@@ -1097,7 +1162,10 @@ impl ExtensionEventRow {
|
||||
major: semver.map(|v| v.major() as i32),
|
||||
minor: semver.map(|v| v.minor() as i32),
|
||||
patch: semver.map(|v| v.patch() as i32),
|
||||
checksum_matched,
|
||||
release_channel: body.release_channel.clone().unwrap_or_default(),
|
||||
os_name: body.os_name.clone(),
|
||||
os_version: body.os_version.clone().unwrap_or_default(),
|
||||
installation_id: body.installation_id.clone(),
|
||||
session_id: body.session_id.clone(),
|
||||
is_staff: body.is_staff,
|
||||
@@ -1126,7 +1194,10 @@ pub struct EditEventRow {
|
||||
major: Option<i32>,
|
||||
minor: Option<i32>,
|
||||
patch: Option<i32>,
|
||||
checksum_matched: bool,
|
||||
release_channel: String,
|
||||
os_name: String,
|
||||
os_version: String,
|
||||
|
||||
// ClientEventBase
|
||||
installation_id: Option<String>,
|
||||
@@ -1148,6 +1219,7 @@ impl EditEventRow {
|
||||
wrapper: &EventWrapper,
|
||||
body: &EventRequestBody,
|
||||
first_event_at: chrono::DateTime<chrono::Utc>,
|
||||
checksum_matched: bool,
|
||||
) -> Self {
|
||||
let semver = body.semver();
|
||||
let time =
|
||||
@@ -1161,7 +1233,10 @@ impl EditEventRow {
|
||||
major: semver.map(|v| v.major() as i32),
|
||||
minor: semver.map(|v| v.minor() as i32),
|
||||
patch: semver.map(|v| v.patch() as i32),
|
||||
checksum_matched,
|
||||
release_channel: body.release_channel.clone().unwrap_or_default(),
|
||||
os_name: body.os_name.clone(),
|
||||
os_version: body.os_version.clone().unwrap_or_default(),
|
||||
installation_id: body.installation_id.clone(),
|
||||
session_id: body.session_id.clone(),
|
||||
is_staff: body.is_staff,
|
||||
@@ -1180,7 +1255,10 @@ pub struct ActionEventRow {
|
||||
major: Option<i32>,
|
||||
minor: Option<i32>,
|
||||
patch: Option<i32>,
|
||||
checksum_matched: bool,
|
||||
release_channel: String,
|
||||
os_name: String,
|
||||
os_version: String,
|
||||
|
||||
// ClientEventBase
|
||||
installation_id: Option<String>,
|
||||
@@ -1200,6 +1278,7 @@ impl ActionEventRow {
|
||||
wrapper: &EventWrapper,
|
||||
body: &EventRequestBody,
|
||||
first_event_at: chrono::DateTime<chrono::Utc>,
|
||||
checksum_matched: bool,
|
||||
) -> Self {
|
||||
let semver = body.semver();
|
||||
let time =
|
||||
@@ -1210,7 +1289,10 @@ impl ActionEventRow {
|
||||
major: semver.map(|v| v.major() as i32),
|
||||
minor: semver.map(|v| v.minor() as i32),
|
||||
patch: semver.map(|v| v.patch() as i32),
|
||||
checksum_matched,
|
||||
release_channel: body.release_channel.clone().unwrap_or_default(),
|
||||
os_name: body.os_name.clone(),
|
||||
os_version: body.os_version.clone().unwrap_or_default(),
|
||||
installation_id: body.installation_id.clone(),
|
||||
session_id: body.session_id.clone(),
|
||||
is_staff: body.is_staff,
|
||||
|
||||
@@ -239,61 +239,74 @@ async fn fetch_extensions_from_blob_store(
|
||||
) -> anyhow::Result<()> {
|
||||
log::info!("fetching extensions from blob store");
|
||||
|
||||
let list = blob_store_client
|
||||
.list_objects()
|
||||
.bucket(blob_store_bucket)
|
||||
.prefix("extensions/")
|
||||
.send()
|
||||
.await?;
|
||||
let mut next_marker = None;
|
||||
let mut published_versions = HashMap::<String, Vec<String>>::default();
|
||||
|
||||
let objects = list.contents.unwrap_or_default();
|
||||
loop {
|
||||
let list = blob_store_client
|
||||
.list_objects()
|
||||
.bucket(blob_store_bucket)
|
||||
.prefix("extensions/")
|
||||
.set_marker(next_marker.clone())
|
||||
.send()
|
||||
.await?;
|
||||
let objects = list.contents.unwrap_or_default();
|
||||
log::info!("fetched {} object(s) from blob store", objects.len());
|
||||
|
||||
let mut published_versions = HashMap::<&str, Vec<&str>>::default();
|
||||
for object in &objects {
|
||||
let Some(key) = object.key.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
let mut parts = key.split('/');
|
||||
let Some(_) = parts.next().filter(|part| *part == "extensions") else {
|
||||
continue;
|
||||
};
|
||||
let Some(extension_id) = parts.next() else {
|
||||
continue;
|
||||
};
|
||||
let Some(version) = parts.next() else {
|
||||
continue;
|
||||
};
|
||||
if parts.next() == Some("manifest.json") {
|
||||
published_versions
|
||||
.entry(extension_id)
|
||||
.or_default()
|
||||
.push(version);
|
||||
for object in &objects {
|
||||
let Some(key) = object.key.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
let mut parts = key.split('/');
|
||||
let Some(_) = parts.next().filter(|part| *part == "extensions") else {
|
||||
continue;
|
||||
};
|
||||
let Some(extension_id) = parts.next() else {
|
||||
continue;
|
||||
};
|
||||
let Some(version) = parts.next() else {
|
||||
continue;
|
||||
};
|
||||
if parts.next() == Some("manifest.json") {
|
||||
published_versions
|
||||
.entry(extension_id.to_owned())
|
||||
.or_default()
|
||||
.push(version.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
if let (Some(true), Some(last_object)) = (list.is_truncated, objects.last()) {
|
||||
next_marker.clone_from(&last_object.key);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("found {} published extensions", published_versions.len());
|
||||
|
||||
let known_versions = app_state.db.get_known_extension_versions().await?;
|
||||
|
||||
let mut new_versions = HashMap::<&str, Vec<NewExtensionVersion>>::default();
|
||||
let empty = Vec::new();
|
||||
for (extension_id, published_versions) in published_versions {
|
||||
for (extension_id, published_versions) in &published_versions {
|
||||
let known_versions = known_versions.get(extension_id).unwrap_or(&empty);
|
||||
|
||||
for published_version in published_versions {
|
||||
if known_versions
|
||||
.binary_search_by_key(&published_version, String::as_str)
|
||||
.binary_search_by_key(&published_version, |known_version| known_version)
|
||||
.is_err()
|
||||
{
|
||||
if let Some(extension) = fetch_extension_manifest(
|
||||
blob_store_client,
|
||||
blob_store_bucket,
|
||||
extension_id,
|
||||
published_version,
|
||||
&extension_id,
|
||||
&published_version,
|
||||
)
|
||||
.await
|
||||
.log_err()
|
||||
{
|
||||
new_versions
|
||||
.entry(extension_id)
|
||||
.entry(&extension_id)
|
||||
.or_default()
|
||||
.push(extension);
|
||||
}
|
||||
|
||||
@@ -277,7 +277,7 @@ mod test {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_verify_access_token(cx: &mut gpui::TestAppContext) {
|
||||
let test_db = crate::db::TestDb::postgres(cx.executor().clone());
|
||||
let test_db = crate::db::TestDb::sqlite(cx.executor().clone());
|
||||
let db = test_db.db();
|
||||
|
||||
let user = db
|
||||
|
||||
@@ -654,6 +654,7 @@ pub struct ChannelsForUser {
|
||||
pub channel_memberships: Vec<channel_member::Model>,
|
||||
pub channel_participants: HashMap<ChannelId, Vec<UserId>>,
|
||||
pub hosted_projects: Vec<proto::HostedProject>,
|
||||
pub invited_channels: Vec<Channel>,
|
||||
|
||||
pub observed_buffer_versions: Vec<proto::ChannelBufferVersion>,
|
||||
pub observed_channel_messages: Vec<proto::ChannelMessageId>,
|
||||
|
||||
@@ -416,7 +416,9 @@ impl Database {
|
||||
user_id: UserId,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<MembershipUpdated> {
|
||||
let new_channels = self.get_user_channels(user_id, Some(channel), tx).await?;
|
||||
let new_channels = self
|
||||
.get_user_channels(user_id, Some(channel), false, tx)
|
||||
.await?;
|
||||
let removed_channels = self
|
||||
.get_channel_descendants_excluding_self([channel], tx)
|
||||
.await?
|
||||
@@ -481,44 +483,10 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns all channel invites for the user with the given ID.
|
||||
pub async fn get_channel_invites_for_user(&self, user_id: UserId) -> Result<Vec<Channel>> {
|
||||
self.transaction(|tx| async move {
|
||||
let mut role_for_channel: HashMap<ChannelId, ChannelRole> = HashMap::default();
|
||||
|
||||
let channel_invites = channel_member::Entity::find()
|
||||
.filter(
|
||||
channel_member::Column::UserId
|
||||
.eq(user_id)
|
||||
.and(channel_member::Column::Accepted.eq(false)),
|
||||
)
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
for invite in channel_invites {
|
||||
role_for_channel.insert(invite.channel_id, invite.role);
|
||||
}
|
||||
|
||||
let channels = channel::Entity::find()
|
||||
.filter(channel::Column::Id.is_in(role_for_channel.keys().copied()))
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
let channels = channels.into_iter().map(Channel::from_model).collect();
|
||||
|
||||
Ok(channels)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns all channels for the user with the given ID.
|
||||
pub async fn get_channels_for_user(&self, user_id: UserId) -> Result<ChannelsForUser> {
|
||||
self.transaction(|tx| async move {
|
||||
let tx = tx;
|
||||
|
||||
self.get_user_channels(user_id, None, &tx).await
|
||||
})
|
||||
.await
|
||||
self.transaction(|tx| async move { self.get_user_channels(user_id, None, true, &tx).await })
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns all channels for the user with the given ID that are descendants
|
||||
@@ -527,25 +495,37 @@ impl Database {
|
||||
&self,
|
||||
user_id: UserId,
|
||||
ancestor_channel: Option<&channel::Model>,
|
||||
include_invites: bool,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<ChannelsForUser> {
|
||||
let mut filter = channel_member::Column::UserId
|
||||
.eq(user_id)
|
||||
.and(channel_member::Column::Accepted.eq(true));
|
||||
|
||||
let mut filter = channel_member::Column::UserId.eq(user_id);
|
||||
if !include_invites {
|
||||
filter = filter.and(channel_member::Column::Accepted.eq(true))
|
||||
}
|
||||
if let Some(ancestor) = ancestor_channel {
|
||||
filter = filter.and(channel_member::Column::ChannelId.eq(ancestor.root_id()));
|
||||
}
|
||||
|
||||
let channel_memberships = channel_member::Entity::find()
|
||||
let mut channels = Vec::<channel::Model>::new();
|
||||
let mut invited_channels = Vec::<Channel>::new();
|
||||
let mut channel_memberships = Vec::<channel_member::Model>::new();
|
||||
let mut rows = channel_member::Entity::find()
|
||||
.filter(filter)
|
||||
.all(tx)
|
||||
.await?;
|
||||
|
||||
let channels = channel::Entity::find()
|
||||
.filter(channel::Column::Id.is_in(channel_memberships.iter().map(|m| m.channel_id)))
|
||||
.all(tx)
|
||||
.inner_join(channel::Entity)
|
||||
.select_also(channel::Entity)
|
||||
.stream(tx)
|
||||
.await?;
|
||||
while let Some(row) = rows.next().await {
|
||||
if let (membership, Some(channel)) = row? {
|
||||
if membership.accepted {
|
||||
channel_memberships.push(membership);
|
||||
channels.push(channel);
|
||||
} else {
|
||||
invited_channels.push(Channel::from_model(channel));
|
||||
}
|
||||
}
|
||||
}
|
||||
drop(rows);
|
||||
|
||||
let mut descendants = self
|
||||
.get_channel_descendants_excluding_self(channels.iter(), tx)
|
||||
@@ -643,6 +623,7 @@ impl Database {
|
||||
Ok(ChannelsForUser {
|
||||
channel_memberships,
|
||||
channels,
|
||||
invited_channels,
|
||||
hosted_projects,
|
||||
channel_participants,
|
||||
latest_buffer_versions,
|
||||
|
||||
@@ -379,6 +379,7 @@ fn metadata_from_extension_and_version(
|
||||
|
||||
pub fn convert_time_to_chrono(time: time::PrimitiveDateTime) -> chrono::DateTime<Utc> {
|
||||
chrono::DateTime::from_naive_utc_and_offset(
|
||||
#[allow(deprecated)]
|
||||
chrono::NaiveDateTime::from_timestamp_opt(time.assume_utc().unix_timestamp(), 0).unwrap(),
|
||||
Utc,
|
||||
)
|
||||
|
||||
@@ -2,6 +2,8 @@ mod buffer_tests;
|
||||
mod channel_tests;
|
||||
mod contributor_tests;
|
||||
mod db_tests;
|
||||
// we only run postgres tests on macos right now
|
||||
#[cfg(target_os = "macos")]
|
||||
mod embedding_tests;
|
||||
mod extension_tests;
|
||||
mod feature_flag_tests;
|
||||
@@ -108,6 +110,7 @@ impl TestDb {
|
||||
#[macro_export]
|
||||
macro_rules! test_both_dbs {
|
||||
($test_name:ident, $postgres_test_name:ident, $sqlite_test_name:ident) => {
|
||||
#[cfg(target_os = "macos")]
|
||||
#[gpui::test]
|
||||
async fn $postgres_test_name(cx: &mut gpui::TestAppContext) {
|
||||
let test_db = $crate::db::TestDb::postgres(cx.executor().clone());
|
||||
|
||||
@@ -176,23 +176,23 @@ async fn test_channel_invites(db: &Arc<Database>) {
|
||||
.unwrap();
|
||||
|
||||
let user_2_invites = db
|
||||
.get_channel_invites_for_user(user_2) // -> [channel_1_1, channel_1_2]
|
||||
.get_channels_for_user(user_2)
|
||||
.await
|
||||
.unwrap()
|
||||
.invited_channels
|
||||
.into_iter()
|
||||
.map(|channel| channel.id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(user_2_invites, &[channel_1_1, channel_1_2]);
|
||||
|
||||
let user_3_invites = db
|
||||
.get_channel_invites_for_user(user_3) // -> [channel_1_1]
|
||||
.get_channels_for_user(user_3)
|
||||
.await
|
||||
.unwrap()
|
||||
.invited_channels
|
||||
.into_iter()
|
||||
.map(|channel| channel.id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(user_3_invites, &[channel_1_1]);
|
||||
|
||||
let (mut members, _) = db
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
use super::*;
|
||||
use crate::test_both_dbs;
|
||||
use gpui::TestAppContext;
|
||||
use pretty_assertions::{assert_eq, assert_ne};
|
||||
use std::sync::Arc;
|
||||
use tests::TestDb;
|
||||
|
||||
test_both_dbs!(
|
||||
test_get_users,
|
||||
@@ -564,9 +562,10 @@ fn test_fuzzy_like_string() {
|
||||
assert_eq!(Database::fuzzy_like_string(" z "), "%z%");
|
||||
}
|
||||
|
||||
#[cfg(target = "macos")]
|
||||
#[gpui::test]
|
||||
async fn test_fuzzy_search_users(cx: &mut TestAppContext) {
|
||||
let test_db = TestDb::postgres(cx.executor());
|
||||
async fn test_fuzzy_search_users(cx: &mut gpui::TestAppContext) {
|
||||
let test_db = tests::TestDb::postgres(cx.executor());
|
||||
let db = test_db.db();
|
||||
for (i, github_login) in [
|
||||
"California",
|
||||
|
||||
@@ -545,6 +545,12 @@ impl Server {
|
||||
.add_request_handler(user_handler(
|
||||
forward_mutating_project_request::<proto::MultiLspQuery>,
|
||||
))
|
||||
.add_request_handler(user_handler(
|
||||
forward_mutating_project_request::<proto::RestartLanguageServers>,
|
||||
))
|
||||
.add_request_handler(user_handler(
|
||||
forward_mutating_project_request::<proto::LinkedEditingRange>,
|
||||
))
|
||||
.add_message_handler(create_buffer_for_peer)
|
||||
.add_request_handler(update_buffer)
|
||||
.add_message_handler(broadcast_project_message_from_host::<proto::RefreshInlayHints>)
|
||||
@@ -557,6 +563,7 @@ impl Server {
|
||||
.add_request_handler(user_handler(request_contact))
|
||||
.add_request_handler(user_handler(remove_contact))
|
||||
.add_request_handler(user_handler(respond_to_contact_request))
|
||||
.add_message_handler(subscribe_to_channels)
|
||||
.add_request_handler(user_handler(create_channel))
|
||||
.add_request_handler(user_handler(delete_channel))
|
||||
.add_request_handler(user_handler(invite_channel_member))
|
||||
@@ -1105,34 +1112,25 @@ impl Server {
|
||||
.await?;
|
||||
}
|
||||
|
||||
let (contacts, channels_for_user, channel_invites, dev_server_projects) =
|
||||
future::try_join4(
|
||||
self.app_state.db.get_contacts(user.id),
|
||||
self.app_state.db.get_channels_for_user(user.id),
|
||||
self.app_state.db.get_channel_invites_for_user(user.id),
|
||||
self.app_state.db.dev_server_projects_update(user.id),
|
||||
)
|
||||
.await?;
|
||||
let (contacts, dev_server_projects) = future::try_join(
|
||||
self.app_state.db.get_contacts(user.id),
|
||||
self.app_state.db.dev_server_projects_update(user.id),
|
||||
)
|
||||
.await?;
|
||||
|
||||
{
|
||||
let mut pool = self.connection_pool.lock();
|
||||
pool.add_connection(connection_id, user.id, user.admin, zed_version);
|
||||
for membership in &channels_for_user.channel_memberships {
|
||||
pool.subscribe_to_channel(user.id, membership.channel_id, membership.role)
|
||||
}
|
||||
self.peer.send(
|
||||
connection_id,
|
||||
build_initial_contacts_update(contacts, &pool),
|
||||
)?;
|
||||
self.peer.send(
|
||||
connection_id,
|
||||
build_update_user_channels(&channels_for_user),
|
||||
)?;
|
||||
self.peer.send(
|
||||
connection_id,
|
||||
build_channels_update(channels_for_user, channel_invites),
|
||||
)?;
|
||||
}
|
||||
|
||||
if should_auto_subscribe_to_channels(zed_version) {
|
||||
subscribe_user_to_channels(user.id, session).await?;
|
||||
}
|
||||
|
||||
send_dev_server_projects_update(user.id, dev_server_projects, session).await;
|
||||
|
||||
if let Some(incoming_call) =
|
||||
@@ -3399,6 +3397,36 @@ async fn remove_contact(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn should_auto_subscribe_to_channels(version: ZedVersion) -> bool {
|
||||
version.0.minor() < 139
|
||||
}
|
||||
|
||||
async fn subscribe_to_channels(_: proto::SubscribeToChannels, session: Session) -> Result<()> {
|
||||
subscribe_user_to_channels(
|
||||
session.user_id().ok_or_else(|| anyhow!("must be a user"))?,
|
||||
&session,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn subscribe_user_to_channels(user_id: UserId, session: &Session) -> Result<(), Error> {
|
||||
let channels_for_user = session.db().await.get_channels_for_user(user_id).await?;
|
||||
let mut pool = session.connection_pool().await;
|
||||
for membership in &channels_for_user.channel_memberships {
|
||||
pool.subscribe_to_channel(user_id, membership.channel_id, membership.role)
|
||||
}
|
||||
session.peer.send(
|
||||
session.connection_id,
|
||||
build_update_user_channels(&channels_for_user),
|
||||
)?;
|
||||
session.peer.send(
|
||||
session.connection_id,
|
||||
build_channels_update(channels_for_user),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Creates a new channel.
|
||||
async fn create_channel(
|
||||
request: proto::CreateChannel,
|
||||
@@ -4475,6 +4503,7 @@ async fn complete_with_google_ai(
|
||||
session.http_client.clone(),
|
||||
google_ai::API_URL,
|
||||
api_key.as_ref(),
|
||||
&request.model.clone(),
|
||||
crate::ai::language_model_request_to_google_ai(request)?,
|
||||
)
|
||||
.await
|
||||
@@ -5034,7 +5063,7 @@ fn notify_membership_updated(
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut update = build_channels_update(result.new_channels, vec![]);
|
||||
let mut update = build_channels_update(result.new_channels);
|
||||
update.delete_channels = result
|
||||
.removed_channels
|
||||
.into_iter()
|
||||
@@ -5064,10 +5093,7 @@ fn build_update_user_channels(channels: &ChannelsForUser) -> proto::UpdateUserCh
|
||||
}
|
||||
}
|
||||
|
||||
fn build_channels_update(
|
||||
channels: ChannelsForUser,
|
||||
channel_invites: Vec<db::Channel>,
|
||||
) -> proto::UpdateChannels {
|
||||
fn build_channels_update(channels: ChannelsForUser) -> proto::UpdateChannels {
|
||||
let mut update = proto::UpdateChannels::default();
|
||||
|
||||
for channel in channels.channels {
|
||||
@@ -5086,7 +5112,7 @@ fn build_channels_update(
|
||||
});
|
||||
}
|
||||
|
||||
for channel in channel_invites {
|
||||
for channel in channels.invited_channels {
|
||||
update.channel_invitations.push(channel.to_proto());
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC
|
||||
assert_eq!(projects.len(), 1);
|
||||
assert_eq!(projects[0].path, "/remote");
|
||||
workspace::join_dev_server_project(
|
||||
projects[0].id,
|
||||
projects[0].project_id.unwrap(),
|
||||
client.app_state.clone(),
|
||||
None,
|
||||
@@ -207,6 +208,7 @@ async fn create_dev_server_project(
|
||||
assert_eq!(projects.len(), 1);
|
||||
assert_eq!(projects[0].path, "/remote");
|
||||
workspace::join_dev_server_project(
|
||||
projects[0].id,
|
||||
projects[0].project_id.unwrap(),
|
||||
client_app_state,
|
||||
None,
|
||||
@@ -491,6 +493,7 @@ async fn test_dev_server_reconnect(
|
||||
.update(cx2, |store, cx| {
|
||||
let projects = store.dev_server_projects();
|
||||
workspace::join_dev_server_project(
|
||||
projects[0].id,
|
||||
projects[0].project_id.unwrap(),
|
||||
client2.app_state.clone(),
|
||||
None,
|
||||
@@ -572,7 +575,8 @@ async fn test_save_as_remote(cx1: &mut gpui::TestAppContext, cx2: &mut gpui::Tes
|
||||
|
||||
let title = remote_workspace
|
||||
.update(&mut cx, |ws, cx| {
|
||||
ws.active_item(cx).unwrap().tab_description(0, &cx).unwrap()
|
||||
let active_item = ws.active_item(cx).unwrap();
|
||||
active_item.tab_description(0, &cx).unwrap()
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
|
||||