Compare commits
327 Commits
diagnostic
...
auto-comma
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9182bd51f2 | ||
|
|
481a554923 | ||
|
|
c129e3b668 | ||
|
|
12e3c3b502 | ||
|
|
54afa6f69f | ||
|
|
55511d1591 | ||
|
|
6c0cb9eaa3 | ||
|
|
24e7b69f8f | ||
|
|
a4cdca5141 | ||
|
|
86cd87e993 | ||
|
|
88000eb7e2 | ||
|
|
ab5a462e0c | ||
|
|
79430fc7d2 | ||
|
|
f96e4ba84f | ||
|
|
7be1ffb9ec | ||
|
|
93a5d0ca29 | ||
|
|
328d98dddc | ||
|
|
76ab9e4d66 | ||
|
|
c477c12956 | ||
|
|
1ffd87b87e | ||
|
|
df11b646da | ||
|
|
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 |
@@ -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
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
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
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
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
|
||||
|
||||
110
.github/workflows/ci.yml
vendored
110
.github/workflows/ci.yml
vendored
@@ -38,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
|
||||
@@ -54,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
|
||||
|
||||
@@ -74,8 +74,8 @@ 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
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
clean: false
|
||||
|
||||
- name: cargo clippy
|
||||
run: cargo xtask clippy
|
||||
run: ./script/clippy
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
@@ -101,7 +101,6 @@ 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
|
||||
@@ -118,7 +117,10 @@ 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
|
||||
@@ -140,7 +142,7 @@ 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
|
||||
@@ -305,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
|
||||
@@ -316,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
|
||||
@@ -328,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
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
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
|
||||
|
||||
|
||||
2
.github/workflows/release_nightly.yml
vendored
2
.github/workflows/release_nightly.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
uses: ./.github/actions/check_style
|
||||
|
||||
- name: Run clippy
|
||||
run: cargo xtask clippy
|
||||
run: ./script/clippy
|
||||
tests:
|
||||
timeout-minutes: 60
|
||||
name: Run tests
|
||||
|
||||
17
.mailmap
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": []
|
||||
}
|
||||
]
|
||||
|
||||
1031
Cargo.lock
generated
1031
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
49
Cargo.toml
49
Cargo.toml
@@ -61,22 +61,28 @@ members = [
|
||||
"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",
|
||||
"crates/task",
|
||||
"crates/tasks_ui",
|
||||
"crates/search",
|
||||
@@ -130,6 +136,7 @@ members = [
|
||||
"extensions/prisma",
|
||||
"extensions/purescript",
|
||||
"extensions/ruby",
|
||||
"extensions/snippets",
|
||||
"extensions/svelte",
|
||||
"extensions/terraform",
|
||||
"extensions/toml",
|
||||
@@ -163,7 +170,6 @@ 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" }
|
||||
@@ -207,13 +213,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" }
|
||||
@@ -221,9 +231,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 = { path = "crates/rustdoc" }
|
||||
task = { path = "crates/task" }
|
||||
tasks_ui = { path = "crates/tasks_ui" }
|
||||
search = { path = "crates/search" }
|
||||
@@ -257,29 +269,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"
|
||||
@@ -287,18 +302,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.25.1"
|
||||
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"
|
||||
@@ -307,6 +321,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"] }
|
||||
@@ -323,6 +338,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"
|
||||
@@ -338,6 +354,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"] }
|
||||
@@ -399,12 +417,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",
|
||||
@@ -440,11 +459,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 }
|
||||
@@ -463,6 +483,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"
|
||||
|
||||
@@ -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
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
assets/icons/list_tree.svg
Normal file
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
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
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 |
3
assets/icons/stop.svg
Normal file
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
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 |
@@ -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": {
|
||||
@@ -43,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",
|
||||
@@ -87,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",
|
||||
@@ -165,7 +152,8 @@
|
||||
// "focus": false
|
||||
// }
|
||||
// ],
|
||||
"ctrl->": "assistant::QuoteSelection"
|
||||
"ctrl->": "assistant::QuoteSelection",
|
||||
"ctrl-alt-e": "editor::SelectEnclosingSymbol"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -295,6 +283,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",
|
||||
@@ -419,7 +414,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",
|
||||
@@ -439,6 +434,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",
|
||||
@@ -562,6 +558,19 @@
|
||||
"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": {
|
||||
@@ -583,7 +592,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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -188,7 +188,8 @@
|
||||
"focus": false
|
||||
}
|
||||
],
|
||||
"cmd->": "assistant::QuoteSelection"
|
||||
"cmd->": "assistant::QuoteSelection",
|
||||
"cmd-alt-e": "editor::SelectEnclosingSymbol"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -475,6 +476,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",
|
||||
@@ -584,6 +586,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": {
|
||||
|
||||
@@ -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",
|
||||
@@ -381,6 +385,10 @@
|
||||
"shift-s": "vim::SubstituteLine",
|
||||
">": ["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": {
|
||||
@@ -582,13 +612,13 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -606,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"]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -616,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",
|
||||
@@ -628,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": [],
|
||||
@@ -131,7 +131,14 @@
|
||||
// The default number of lines to expand excerpts in the multibuffer by.
|
||||
"expand_excerpt_lines": 3,
|
||||
// Globs to match against file paths to determine if a file is private.
|
||||
"private_files": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"],
|
||||
"private_files": [
|
||||
"**/.env*",
|
||||
"**/*.pem",
|
||||
"**/*.key",
|
||||
"**/*.cert",
|
||||
"**/*.crt",
|
||||
"**/secrets.yml"
|
||||
],
|
||||
// Whether to use additional LSP queries to format (and amend) the code after
|
||||
// every "trigger" symbol input, defined by LSP server capabilities.
|
||||
"use_on_type_format": true,
|
||||
@@ -139,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:
|
||||
//
|
||||
@@ -176,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": {
|
||||
@@ -218,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
|
||||
},
|
||||
@@ -226,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:
|
||||
///
|
||||
@@ -240,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
|
||||
@@ -293,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.
|
||||
@@ -354,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.
|
||||
@@ -425,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
|
||||
@@ -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
|
||||
@@ -697,7 +748,7 @@
|
||||
}
|
||||
},
|
||||
"JavaScript": {
|
||||
"language_servers": ["typescript-language-server", "!vtsls", "..."],
|
||||
"language_servers": ["!typescript-language-server", "vtsls", "..."],
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
@@ -740,7 +791,7 @@
|
||||
}
|
||||
},
|
||||
"TSX": {
|
||||
"language_servers": ["typescript-language-server", "!vtsls", "..."],
|
||||
"language_servers": ["!typescript-language-server", "vtsls", "..."],
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
@@ -751,7 +802,7 @@
|
||||
}
|
||||
},
|
||||
"TypeScript": {
|
||||
"language_servers": ["typescript-language-server", "!vtsls", "..."],
|
||||
"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)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ pub const ANTHROPIC_API_URL: &'static str = "https://api.anthropic.com";
|
||||
#[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")]
|
||||
@@ -22,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)
|
||||
@@ -35,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",
|
||||
@@ -43,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,6 +16,7 @@ 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
|
||||
@@ -35,22 +36,27 @@ 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.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
|
||||
|
||||
@@ -10,30 +10,31 @@ mod search;
|
||||
mod slash_command;
|
||||
mod streaming_diff;
|
||||
|
||||
pub use assistant_panel::AssistantPanel;
|
||||
|
||||
use assistant_settings::{AnthropicModel, AssistantSettings, CloudModel, OpenAiModel};
|
||||
pub use assistant_panel::{AssistantPanel, AssistantPanelEvent};
|
||||
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 fs::Fs;
|
||||
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
|
||||
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, fetch_command, file_command, project_command, prompt_command,
|
||||
rustdoc_command, search_command, tabs_command,
|
||||
active_command, auto_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,
|
||||
};
|
||||
pub(crate) use streaming_diff::*;
|
||||
use util::paths::EMBEDDINGS_DIR;
|
||||
|
||||
actions!(
|
||||
assistant,
|
||||
@@ -91,6 +92,7 @@ pub enum LanguageModel {
|
||||
Cloud(CloudModel),
|
||||
OpenAi(OpenAiModel),
|
||||
Anthropic(AnthropicModel),
|
||||
Ollama(OllamaModel),
|
||||
}
|
||||
|
||||
impl Default for LanguageModel {
|
||||
@@ -105,6 +107,7 @@ impl LanguageModel {
|
||||
LanguageModel::OpenAi(model) => format!("openai/{}", model.id()),
|
||||
LanguageModel::Anthropic(model) => format!("anthropic/{}", model.id()),
|
||||
LanguageModel::Cloud(model) => format!("zed.dev/{}", model.id()),
|
||||
LanguageModel::Ollama(model) => format!("ollama/{}", model.id()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +116,7 @@ impl LanguageModel {
|
||||
LanguageModel::OpenAi(model) => model.display_name().into(),
|
||||
LanguageModel::Anthropic(model) => model.display_name().into(),
|
||||
LanguageModel::Cloud(model) => model.display_name().into(),
|
||||
LanguageModel::Ollama(model) => model.display_name().into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +125,7 @@ impl LanguageModel {
|
||||
LanguageModel::OpenAi(model) => model.max_token_count(),
|
||||
LanguageModel::Anthropic(model) => model.max_token_count(),
|
||||
LanguageModel::Cloud(model) => model.max_token_count(),
|
||||
LanguageModel::Ollama(model) => model.max_token_count(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +134,7 @@ impl LanguageModel {
|
||||
LanguageModel::OpenAi(model) => model.id(),
|
||||
LanguageModel::Anthropic(model) => model.id(),
|
||||
LanguageModel::Cloud(model) => model.id(),
|
||||
LanguageModel::Ollama(model) => model.id(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -179,8 +185,12 @@ impl LanguageModelRequest {
|
||||
match &self.model {
|
||||
LanguageModel::OpenAi(_) => {}
|
||||
LanguageModel::Anthropic(_) => {}
|
||||
LanguageModel::Ollama(_) => {}
|
||||
LanguageModel::Cloud(model) => match model {
|
||||
CloudModel::Claude3Opus | CloudModel::Claude3Sonnet | CloudModel::Claude3Haiku => {
|
||||
CloudModel::Claude3Opus
|
||||
| CloudModel::Claude3Sonnet
|
||||
| CloudModel::Claude3Haiku
|
||||
| CloudModel::Claude3_5Sonnet => {
|
||||
preprocess_anthropic_request(self);
|
||||
}
|
||||
_ => {}
|
||||
@@ -255,7 +265,7 @@ impl Assistant {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, cx: &mut AppContext) {
|
||||
cx.set_global(Assistant::default());
|
||||
AssistantSettings::register(cx);
|
||||
|
||||
@@ -264,7 +274,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,
|
||||
)
|
||||
@@ -279,7 +289,8 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
assistant_slash_command::init(cx);
|
||||
register_slash_commands(cx);
|
||||
assistant_panel::init(cx);
|
||||
inline_assistant::init(client.telemetry().clone(), cx);
|
||||
inline_assistant::init(fs.clone(), client.telemetry().clone(), cx);
|
||||
RustdocStore::init_global(cx);
|
||||
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_namespace(Assistant::NAMESPACE);
|
||||
@@ -300,6 +311,7 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
|
||||
fn register_slash_commands(cx: &mut AppContext) {
|
||||
let slash_command_registry = SlashCommandRegistry::global(cx);
|
||||
slash_command_registry.register_command(auto_command::AutoCommand, true);
|
||||
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);
|
||||
@@ -307,10 +319,31 @@ fn register_slash_commands(cx: &mut AppContext) {
|
||||
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);
|
||||
}
|
||||
|
||||
pub fn humanize_token_count(count: usize) -> String {
|
||||
match count {
|
||||
0..=999 => count.to_string(),
|
||||
1000..=9999 => {
|
||||
let thousands = count / 1000;
|
||||
let hundreds = (count % 1000 + 50) / 100;
|
||||
if hundreds == 0 {
|
||||
format!("{}k", thousands)
|
||||
} else if hundreds == 10 {
|
||||
format!("{}k", thousands + 1)
|
||||
} else {
|
||||
format!("{}.{}k", thousands, hundreds)
|
||||
}
|
||||
}
|
||||
_ => format!("{}k", (count + 500) / 1000),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::{
|
||||
assistant_settings::{AssistantDockPosition, AssistantSettings},
|
||||
humanize_token_count,
|
||||
prompt_library::open_prompt_library,
|
||||
search::*,
|
||||
slash_command::{
|
||||
@@ -15,32 +16,36 @@ use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
|
||||
use client::telemetry::Telemetry;
|
||||
use collections::{BTreeSet, HashMap, HashSet};
|
||||
use editor::actions::ShowCompletions;
|
||||
use editor::{
|
||||
actions::{FoldAt, MoveToEndOfLine, Newline, UnfoldAt},
|
||||
display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, Flap, ToDisplayPoint},
|
||||
actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt},
|
||||
display_map::{
|
||||
BlockDisposition, BlockId, BlockProperties, BlockStyle, Crease, RenderBlock, ToDisplayPoint,
|
||||
},
|
||||
scroll::{Autoscroll, AutoscrollStrategy},
|
||||
Anchor, Editor, EditorEvent, RowExt, ToOffset as _, ToPoint,
|
||||
};
|
||||
use editor::{display_map::FlapId, FoldPlaceholder};
|
||||
use editor::{display_map::CreaseId, FoldPlaceholder};
|
||||
use file_icons::FileIcons;
|
||||
use fs::Fs;
|
||||
use futures::future::Shared;
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use gpui::{
|
||||
div, point, rems, Action, AnyElement, AnyView, AppContext, AsyncAppContext, AsyncWindowContext,
|
||||
ClipboardItem, Context as _, Empty, EventEmitter, FocusHandle, FocusableView,
|
||||
InteractiveElement, IntoElement, Model, ModelContext, ParentElement, Pixels, Render,
|
||||
SharedString, StatefulInteractiveElement, Styled, Subscription, Task, UpdateGlobal, View,
|
||||
ViewContext, VisualContext, WeakView, WindowContext,
|
||||
div, percentage, point, rems, Action, Animation, AnimationExt, AnyElement, AnyView, AppContext,
|
||||
AsyncAppContext, AsyncWindowContext, ClipboardItem, Context as _, Empty, EventEmitter,
|
||||
FocusHandle, FocusOutEvent, FocusableView, InteractiveElement, IntoElement, Model,
|
||||
ModelContext, ParentElement, Pixels, Render, SharedString, StatefulInteractiveElement, Styled,
|
||||
Subscription, Task, Transformation, UpdateGlobal, View, ViewContext, VisualContext, WeakView,
|
||||
WindowContext,
|
||||
};
|
||||
use language::{
|
||||
language_settings::SoftWrap, AnchorRangeExt, AutoindentMode, Buffer, LanguageRegistry,
|
||||
language_settings::SoftWrap, AnchorRangeExt as _, AutoindentMode, Buffer, LanguageRegistry,
|
||||
LspAdapterDelegate, OffsetRangeExt as _, Point, ToOffset as _,
|
||||
};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use paths::contexts_dir;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{Project, ProjectLspAdapterDelegate, ProjectTransaction};
|
||||
use rustdoc::{CrateName, RustdocStore};
|
||||
use search::{buffer_search::DivRegistrar, BufferSearchBar};
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
@@ -54,10 +59,10 @@ use std::{
|
||||
};
|
||||
use telemetry_events::AssistantKind;
|
||||
use ui::{
|
||||
popover_menu, prelude::*, ButtonLike, ContextMenu, ElevationIndex, KeyBinding, ListItem,
|
||||
ListItemSpacing, PopoverMenuHandle, Tab, TabBar, Tooltip,
|
||||
prelude::*, ButtonLike, ContextMenu, Disclosure, ElevationIndex, KeyBinding, ListItem,
|
||||
ListItemSpacing, PopoverMenu, PopoverMenuHandle, Tab, TabBar, Tooltip,
|
||||
};
|
||||
use util::{paths::CONTEXTS_DIR, post_inc, ResultExt, TryFutureExt};
|
||||
use util::{post_inc, ResultExt, TryFutureExt};
|
||||
use uuid::Uuid;
|
||||
use workspace::NewFile;
|
||||
use workspace::{
|
||||
@@ -79,13 +84,16 @@ pub fn init(cx: &mut AppContext) {
|
||||
workspace.toggle_panel_focus::<AssistantPanel>(cx);
|
||||
})
|
||||
.register_action(AssistantPanel::inline_assist)
|
||||
.register_action(AssistantPanel::cancel_last_inline_assist)
|
||||
.register_action(ContextEditor::quote_selection);
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub enum AssistantPanelEvent {
|
||||
ContextEdited,
|
||||
}
|
||||
|
||||
pub struct AssistantPanel {
|
||||
workspace: WeakView<Workspace>,
|
||||
width: Option<Pixels>,
|
||||
@@ -297,7 +305,7 @@ impl AssistantPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_out(&mut self, cx: &mut ViewContext<Self>) {
|
||||
fn focus_out(&mut self, _event: FocusOutEvent, cx: &mut ViewContext<Self>) {
|
||||
self.toolbar
|
||||
.update(cx, |toolbar, cx| toolbar.focus_changed(false, cx));
|
||||
cx.notify();
|
||||
@@ -357,11 +365,11 @@ impl AssistantPanel {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(assistant) = workspace.panel::<AssistantPanel>(cx) else {
|
||||
let Some(assistant_panel) = workspace.panel::<AssistantPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let context_editor = assistant
|
||||
let context_editor = assistant_panel
|
||||
.read(cx)
|
||||
.active_context_editor()
|
||||
.and_then(|editor| {
|
||||
@@ -388,25 +396,37 @@ impl AssistantPanel {
|
||||
return;
|
||||
};
|
||||
|
||||
if assistant.update(cx, |assistant, cx| assistant.is_authenticated(cx)) {
|
||||
if assistant_panel.update(cx, |panel, cx| panel.is_authenticated(cx)) {
|
||||
InlineAssistant::update_global(cx, |assistant, cx| {
|
||||
assistant.assist(
|
||||
&active_editor,
|
||||
Some(cx.view().downgrade()),
|
||||
include_context,
|
||||
include_context.then_some(&assistant_panel),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
} else {
|
||||
let assistant = assistant.downgrade();
|
||||
let assistant_panel = assistant_panel.downgrade();
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
assistant
|
||||
assistant_panel
|
||||
.update(&mut cx, |assistant, cx| assistant.authenticate(cx))?
|
||||
.await?;
|
||||
if assistant.update(&mut cx, |assistant, cx| assistant.is_authenticated(cx))? {
|
||||
if assistant_panel
|
||||
.update(&mut cx, |assistant, cx| assistant.is_authenticated(cx))?
|
||||
{
|
||||
cx.update(|cx| {
|
||||
let assistant_panel = if include_context {
|
||||
assistant_panel.upgrade()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
InlineAssistant::update_global(cx, |assistant, cx| {
|
||||
assistant.assist(&active_editor, Some(workspace), include_context, cx)
|
||||
assistant.assist(
|
||||
&active_editor,
|
||||
Some(workspace),
|
||||
assistant_panel.as_ref(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})?
|
||||
} else {
|
||||
@@ -421,19 +441,6 @@ impl AssistantPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel_last_inline_assist(
|
||||
_workspace: &mut Workspace,
|
||||
_: &editor::actions::Cancel,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
let canceled = InlineAssistant::update_global(cx, |assistant, cx| {
|
||||
assistant.cancel_last_inline_assist(cx)
|
||||
});
|
||||
if !canceled {
|
||||
cx.propagate();
|
||||
}
|
||||
}
|
||||
|
||||
fn new_context(&mut self, cx: &mut ViewContext<Self>) -> Option<View<ContextEditor>> {
|
||||
let workspace = self.workspace.upgrade()?;
|
||||
|
||||
@@ -470,7 +477,7 @@ impl AssistantPanel {
|
||||
_subscriptions: subscriptions,
|
||||
});
|
||||
self.show_saved_contexts = false;
|
||||
|
||||
cx.emit(AssistantPanelEvent::ContextEdited);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -482,6 +489,7 @@ impl AssistantPanel {
|
||||
) {
|
||||
match event {
|
||||
ContextEditorEvent::TabContentChanged => cx.notify(),
|
||||
ContextEditorEvent::Edited => cx.emit(AssistantPanelEvent::ContextEdited),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -590,7 +598,7 @@ impl AssistantPanel {
|
||||
fn render_popover_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let assistant = cx.view().clone();
|
||||
let zoomed = self.zoomed;
|
||||
popover_menu("assistant-popover")
|
||||
PopoverMenu::new("assistant-popover")
|
||||
.trigger(IconButton::new("trigger", IconName::Menu))
|
||||
.menu(move |cx| {
|
||||
let assistant = assistant.clone();
|
||||
@@ -632,7 +640,7 @@ impl AssistantPanel {
|
||||
)
|
||||
});
|
||||
|
||||
popover_menu("inject-context-menu")
|
||||
PopoverMenu::new("inject-context-menu")
|
||||
.trigger(IconButton::new("trigger", IconName::Quote).tooltip(|cx| {
|
||||
Tooltip::with_meta("Insert Context", None, "Type / to insert via keyboard", cx)
|
||||
}))
|
||||
@@ -873,18 +881,33 @@ impl AssistantPanel {
|
||||
context: &Model<Context>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<impl IntoElement> {
|
||||
let remaining_tokens = context.read(cx).remaining_tokens(cx)?;
|
||||
let remaining_tokens_color = if remaining_tokens <= 0 {
|
||||
let model = CompletionProvider::global(cx).model();
|
||||
let token_count = context.read(cx).token_count()?;
|
||||
let max_token_count = model.max_token_count();
|
||||
|
||||
let remaining_tokens = max_token_count as isize - token_count as isize;
|
||||
let token_count_color = if remaining_tokens <= 0 {
|
||||
Color::Error
|
||||
} else if remaining_tokens <= 500 {
|
||||
} else if token_count as f32 / max_token_count as f32 >= 0.8 {
|
||||
Color::Warning
|
||||
} else {
|
||||
Color::Muted
|
||||
};
|
||||
|
||||
Some(
|
||||
Label::new(remaining_tokens.to_string())
|
||||
.size(LabelSize::Small)
|
||||
.color(remaining_tokens_color),
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Label::new(humanize_token_count(token_count))
|
||||
.size(LabelSize::Small)
|
||||
.color(token_count_color),
|
||||
)
|
||||
.child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
|
||||
.child(
|
||||
Label::new(humanize_token_count(max_token_count))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -988,6 +1011,7 @@ impl Panel for AssistantPanel {
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for AssistantPanel {}
|
||||
impl EventEmitter<AssistantPanelEvent> for AssistantPanel {}
|
||||
|
||||
impl FocusableView for AssistantPanel {
|
||||
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
||||
@@ -1024,6 +1048,7 @@ pub struct Context {
|
||||
edit_suggestions: Vec<EditSuggestion>,
|
||||
pending_slash_commands: Vec<PendingSlashCommand>,
|
||||
edits_since_last_slash_command_parse: language::Subscription,
|
||||
slash_command_output_sections: Vec<SlashCommandOutputSection<language::Anchor>>,
|
||||
message_anchors: Vec<MessageAnchor>,
|
||||
messages_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
next_message_id: MessageId,
|
||||
@@ -1065,6 +1090,7 @@ impl Context {
|
||||
next_message_id: Default::default(),
|
||||
edit_suggestions: Vec::new(),
|
||||
pending_slash_commands: Vec::new(),
|
||||
slash_command_output_sections: Vec::new(),
|
||||
edits_since_last_slash_command_parse,
|
||||
summary: None,
|
||||
pending_summary: Task::ready(None),
|
||||
@@ -1101,11 +1127,12 @@ impl Context {
|
||||
}
|
||||
|
||||
fn serialize(&self, cx: &AppContext) -> SavedContext {
|
||||
let buffer = self.buffer.read(cx);
|
||||
SavedContext {
|
||||
id: self.id.clone(),
|
||||
zed: "context".into(),
|
||||
version: SavedContext::VERSION.into(),
|
||||
text: self.buffer.read(cx).text(),
|
||||
text: buffer.text(),
|
||||
message_metadata: self.messages_metadata.clone(),
|
||||
messages: self
|
||||
.messages(cx)
|
||||
@@ -1119,6 +1146,22 @@ impl Context {
|
||||
.as_ref()
|
||||
.map(|summary| summary.text.clone())
|
||||
.unwrap_or_default(),
|
||||
slash_command_output_sections: self
|
||||
.slash_command_output_sections
|
||||
.iter()
|
||||
.filter_map(|section| {
|
||||
let range = section.range.to_offset(buffer);
|
||||
if section.range.start.is_valid(buffer) && !range.is_empty() {
|
||||
Some(SlashCommandOutputSection {
|
||||
range,
|
||||
icon: section.icon,
|
||||
label: section.label.clone(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1170,6 +1213,19 @@ impl Context {
|
||||
next_message_id,
|
||||
edit_suggestions: Vec::new(),
|
||||
pending_slash_commands: Vec::new(),
|
||||
slash_command_output_sections: saved_context
|
||||
.slash_command_output_sections
|
||||
.into_iter()
|
||||
.map(|section| {
|
||||
let buffer = buffer.read(cx);
|
||||
SlashCommandOutputSection {
|
||||
range: buffer.anchor_after(section.range.start)
|
||||
..buffer.anchor_before(section.range.end),
|
||||
icon: section.icon,
|
||||
label: section.label,
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
edits_since_last_slash_command_parse,
|
||||
summary: Some(Summary {
|
||||
text: saved_context.summary,
|
||||
@@ -1222,6 +1278,10 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn token_count(&self) -> Option<usize> {
|
||||
self.token_count
|
||||
}
|
||||
|
||||
pub(crate) fn count_remaining_tokens(&mut self, cx: &mut ModelContext<Self>) {
|
||||
let request = self.to_completion_request(cx);
|
||||
self.pending_token_count = cx.spawn(|this, mut cx| {
|
||||
@@ -1464,10 +1524,17 @@ impl Context {
|
||||
.map(|section| SlashCommandOutputSection {
|
||||
range: buffer.anchor_after(start + section.range.start)
|
||||
..buffer.anchor_before(start + section.range.end),
|
||||
render_placeholder: section.render_placeholder,
|
||||
icon: section.icon,
|
||||
label: section.label,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
sections.sort_by(|a, b| a.range.cmp(&b.range, buffer));
|
||||
|
||||
this.slash_command_output_sections
|
||||
.extend(sections.iter().cloned());
|
||||
this.slash_command_output_sections
|
||||
.sort_by(|a, b| a.range.cmp(&b.range, buffer));
|
||||
|
||||
ContextEvent::SlashCommandFinished {
|
||||
output_range: buffer.anchor_after(start)
|
||||
..buffer.anchor_before(new_end),
|
||||
@@ -1505,11 +1572,6 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
fn remaining_tokens(&self, cx: &AppContext) -> Option<isize> {
|
||||
let model = CompletionProvider::global(cx).model();
|
||||
Some(model.max_token_count() as isize - self.token_count? as isize)
|
||||
}
|
||||
|
||||
fn completion_provider_changed(&mut self, cx: &mut ModelContext<Self>) {
|
||||
self.count_remaining_tokens(cx);
|
||||
}
|
||||
@@ -2011,7 +2073,7 @@ impl Context {
|
||||
let mut discriminant = 1;
|
||||
let mut new_path;
|
||||
loop {
|
||||
new_path = CONTEXTS_DIR.join(&format!(
|
||||
new_path = contexts_dir().join(&format!(
|
||||
"{} - {}.zed.json",
|
||||
summary.trim(),
|
||||
discriminant
|
||||
@@ -2025,7 +2087,7 @@ impl Context {
|
||||
new_path
|
||||
};
|
||||
|
||||
fs.create_dir(CONTEXTS_DIR.as_ref()).await?;
|
||||
fs.create_dir(contexts_dir().as_ref()).await?;
|
||||
fs.atomic_write(path.clone(), serde_json::to_string(&context).unwrap())
|
||||
.await?;
|
||||
this.update(&mut cx, |this, _| this.path = Some(path))?;
|
||||
@@ -2150,6 +2212,7 @@ struct PendingCompletion {
|
||||
}
|
||||
|
||||
enum ContextEditorEvent {
|
||||
Edited,
|
||||
TabContentChanged,
|
||||
}
|
||||
|
||||
@@ -2168,7 +2231,8 @@ pub struct ContextEditor {
|
||||
editor: View<Editor>,
|
||||
blocks: HashSet<BlockId>,
|
||||
scroll_position: Option<ScrollPosition>,
|
||||
pending_slash_command_flaps: HashMap<Range<language::Anchor>, FlapId>,
|
||||
pending_slash_command_creases: HashMap<Range<language::Anchor>, CreaseId>,
|
||||
pending_slash_command_blocks: HashMap<Range<language::Anchor>, BlockId>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@@ -2219,6 +2283,7 @@ impl ContextEditor {
|
||||
editor.set_show_line_numbers(false, cx);
|
||||
editor.set_show_git_diff_gutter(false, cx);
|
||||
editor.set_show_code_actions(false, cx);
|
||||
editor.set_show_runnables(false, cx);
|
||||
editor.set_show_wrap_guides(false, cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_completion_provider(Box::new(completion_provider));
|
||||
@@ -2231,6 +2296,7 @@ impl ContextEditor {
|
||||
cx.subscribe(&editor, Self::handle_editor_event),
|
||||
];
|
||||
|
||||
let sections = context.read(cx).slash_command_output_sections.clone();
|
||||
let mut this = Self {
|
||||
context,
|
||||
editor,
|
||||
@@ -2240,10 +2306,12 @@ impl ContextEditor {
|
||||
scroll_position: None,
|
||||
fs,
|
||||
workspace: workspace.downgrade(),
|
||||
pending_slash_command_flaps: HashMap::default(),
|
||||
pending_slash_command_creases: HashMap::default(),
|
||||
pending_slash_command_blocks: HashMap::default(),
|
||||
_subscriptions,
|
||||
};
|
||||
this.update_message_headers(cx);
|
||||
this.insert_slash_command_output_sections(sections, cx);
|
||||
this
|
||||
}
|
||||
|
||||
@@ -2360,7 +2428,7 @@ impl ContextEditor {
|
||||
editor.insert(&format!("/{name}"), cx);
|
||||
if command.requires_argument() {
|
||||
editor.insert(" ", cx);
|
||||
editor.show_completions(&ShowCompletions, cx);
|
||||
editor.show_completions(&ShowCompletions::default(), cx);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -2501,16 +2569,27 @@ impl ContextEditor {
|
||||
ContextEvent::PendingSlashCommandsUpdated { removed, updated } => {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||
let excerpt_id = *buffer.as_singleton().unwrap().0;
|
||||
let (excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap();
|
||||
let excerpt_id = *excerpt_id;
|
||||
|
||||
editor.remove_flaps(
|
||||
editor.remove_creases(
|
||||
removed
|
||||
.iter()
|
||||
.filter_map(|range| self.pending_slash_command_flaps.remove(range)),
|
||||
.filter_map(|range| self.pending_slash_command_creases.remove(range)),
|
||||
cx,
|
||||
);
|
||||
|
||||
let flap_ids = editor.insert_flaps(
|
||||
editor.remove_blocks(
|
||||
HashSet::from_iter(
|
||||
removed.iter().filter_map(|range| {
|
||||
self.pending_slash_command_blocks.remove(range)
|
||||
}),
|
||||
),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
|
||||
let crease_ids = editor.insert_creases(
|
||||
updated.iter().map(|command| {
|
||||
let workspace = self.workspace.clone();
|
||||
let confirm_command = Arc::new({
|
||||
@@ -2542,13 +2621,28 @@ impl ContextEditor {
|
||||
move |row, _, _, _cx: &mut WindowContext| {
|
||||
render_pending_slash_command_gutter_decoration(
|
||||
row,
|
||||
command.status.clone(),
|
||||
&command.status,
|
||||
confirm_command.clone(),
|
||||
)
|
||||
}
|
||||
};
|
||||
let render_trailer =
|
||||
|_row, _unfold, _cx: &mut WindowContext| Empty.into_any();
|
||||
let render_trailer = {
|
||||
let command = command.clone();
|
||||
move |row, _unfold, cx: &mut WindowContext| {
|
||||
// TODO: In the future we should investigate how we can expose
|
||||
// this as a hook on the `SlashCommand` trait so that we don't
|
||||
// need to special-case it here.
|
||||
if command.name == "rustdoc" {
|
||||
return render_rustdoc_slash_command_trailer(
|
||||
row,
|
||||
command.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
Empty.into_any()
|
||||
}
|
||||
};
|
||||
|
||||
let start = buffer
|
||||
.anchor_in_excerpt(excerpt_id, command.source_range.start)
|
||||
@@ -2556,16 +2650,47 @@ impl ContextEditor {
|
||||
let end = buffer
|
||||
.anchor_in_excerpt(excerpt_id, command.source_range.end)
|
||||
.unwrap();
|
||||
Flap::new(start..end, placeholder, render_toggle, render_trailer)
|
||||
Crease::new(start..end, placeholder, render_toggle, render_trailer)
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
|
||||
self.pending_slash_command_flaps.extend(
|
||||
let block_ids = editor.insert_blocks(
|
||||
updated
|
||||
.iter()
|
||||
.filter_map(|command| match &command.status {
|
||||
PendingSlashCommandStatus::Error(error) => {
|
||||
Some((command, error.clone()))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.map(|(command, error_message)| BlockProperties {
|
||||
style: BlockStyle::Fixed,
|
||||
position: Anchor {
|
||||
buffer_id: Some(buffer_id),
|
||||
excerpt_id,
|
||||
text_anchor: command.source_range.start,
|
||||
},
|
||||
height: 1,
|
||||
disposition: BlockDisposition::Below,
|
||||
render: slash_command_error_block_renderer(error_message),
|
||||
}),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
|
||||
self.pending_slash_command_creases.extend(
|
||||
updated
|
||||
.iter()
|
||||
.map(|command| command.source_range.clone())
|
||||
.zip(flap_ids),
|
||||
.zip(crease_ids),
|
||||
);
|
||||
|
||||
self.pending_slash_command_blocks.extend(
|
||||
updated
|
||||
.iter()
|
||||
.map(|command| command.source_range.clone())
|
||||
.zip(block_ids),
|
||||
);
|
||||
})
|
||||
}
|
||||
@@ -2608,7 +2733,7 @@ impl ContextEditor {
|
||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||
let excerpt_id = *buffer.as_singleton().unwrap().0;
|
||||
let mut buffer_rows_to_fold = BTreeSet::new();
|
||||
let mut flaps = Vec::new();
|
||||
let mut creases = Vec::new();
|
||||
for section in sections {
|
||||
let start = buffer
|
||||
.anchor_in_excerpt(excerpt_id, section.range.start)
|
||||
@@ -2618,26 +2743,32 @@ impl ContextEditor {
|
||||
.unwrap();
|
||||
let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
|
||||
buffer_rows_to_fold.insert(buffer_row);
|
||||
flaps.push(Flap::new(
|
||||
creases.push(Crease::new(
|
||||
start..end,
|
||||
FoldPlaceholder {
|
||||
render: Arc::new({
|
||||
let editor = cx.view().downgrade();
|
||||
let render_placeholder = section.render_placeholder.clone();
|
||||
move |fold_id, fold_range, cx| {
|
||||
let icon = section.icon;
|
||||
let label = section.label.clone();
|
||||
move |fold_id, fold_range, _cx| {
|
||||
let editor = editor.clone();
|
||||
let unfold = Arc::new(move |cx: &mut WindowContext| {
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
let buffer_start = fold_range
|
||||
.start
|
||||
.to_point(&editor.buffer().read(cx).read(cx));
|
||||
let buffer_row = MultiBufferRow(buffer_start.row);
|
||||
editor.unfold_at(&UnfoldAt { buffer_row }, cx);
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
render_placeholder(fold_id.into(), unfold, cx)
|
||||
ButtonLike::new(fold_id)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.child(Icon::new(icon))
|
||||
.child(Label::new(label.clone()).single_line())
|
||||
.on_click(move |_, cx| {
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
let buffer_start = fold_range
|
||||
.start
|
||||
.to_point(&editor.buffer().read(cx).read(cx));
|
||||
let buffer_row = MultiBufferRow(buffer_start.row);
|
||||
editor.unfold_at(&UnfoldAt { buffer_row }, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
}),
|
||||
constrain_width: false,
|
||||
@@ -2648,7 +2779,7 @@ impl ContextEditor {
|
||||
));
|
||||
}
|
||||
|
||||
editor.insert_flaps(flaps, cx);
|
||||
editor.insert_creases(creases, cx);
|
||||
|
||||
for buffer_row in buffer_rows_to_fold.into_iter().rev() {
|
||||
editor.fold_at(&FoldAt { buffer_row }, cx);
|
||||
@@ -2674,6 +2805,7 @@ impl ContextEditor {
|
||||
EditorEvent::SelectionsChanged { .. } => {
|
||||
self.scroll_position = self.cursor_scroll_position(cx);
|
||||
}
|
||||
EditorEvent::BufferEdited => cx.emit(ContextEditorEvent::Edited),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -3142,22 +3274,15 @@ fn render_slash_command_output_toggle(
|
||||
fold: ToggleFold,
|
||||
_cx: &mut WindowContext,
|
||||
) -> AnyElement {
|
||||
IconButton::new(
|
||||
("slash-command-output-fold-indicator", row.0),
|
||||
ui::IconName::ChevronDown,
|
||||
)
|
||||
.on_click(move |_e, cx| fold(!is_folded, cx))
|
||||
.icon_color(ui::Color::Muted)
|
||||
.icon_size(ui::IconSize::Small)
|
||||
.selected(is_folded)
|
||||
.selected_icon(ui::IconName::ChevronRight)
|
||||
.size(ui::ButtonSize::None)
|
||||
.into_any_element()
|
||||
Disclosure::new(("slash-command-output-fold-indicator", row.0), !is_folded)
|
||||
.selected(is_folded)
|
||||
.on_click(move |_e, cx| fold(!is_folded, cx))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_pending_slash_command_gutter_decoration(
|
||||
row: MultiBufferRow,
|
||||
status: PendingSlashCommandStatus,
|
||||
status: &PendingSlashCommandStatus,
|
||||
confirm_command: Arc<dyn Fn(&mut WindowContext)>,
|
||||
) -> AnyElement {
|
||||
let mut icon = IconButton::new(
|
||||
@@ -3175,16 +3300,43 @@ fn render_pending_slash_command_gutter_decoration(
|
||||
PendingSlashCommandStatus::Running { .. } => {
|
||||
icon = icon.selected(true);
|
||||
}
|
||||
PendingSlashCommandStatus::Error(error) => {
|
||||
icon = icon
|
||||
.icon_color(Color::Error)
|
||||
.tooltip(move |cx| Tooltip::text(format!("error: {error}"), cx));
|
||||
}
|
||||
PendingSlashCommandStatus::Error(_) => icon = icon.icon_color(Color::Error),
|
||||
}
|
||||
|
||||
icon.into_any_element()
|
||||
}
|
||||
|
||||
fn render_rustdoc_slash_command_trailer(
|
||||
row: MultiBufferRow,
|
||||
command: PendingSlashCommand,
|
||||
cx: &mut WindowContext,
|
||||
) -> AnyElement {
|
||||
let rustdoc_store = RustdocStore::global(cx);
|
||||
|
||||
let Some((crate_name, _)) = command
|
||||
.argument
|
||||
.as_ref()
|
||||
.and_then(|arg| arg.split_once(':'))
|
||||
else {
|
||||
return Empty.into_any();
|
||||
};
|
||||
|
||||
let crate_name = CrateName::from(crate_name);
|
||||
if !rustdoc_store.is_indexing(&crate_name) {
|
||||
return Empty.into_any();
|
||||
}
|
||||
|
||||
div()
|
||||
.id(("crates-being-indexed", row.0))
|
||||
.child(Icon::new(IconName::ArrowCircle).with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(4)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
))
|
||||
.tooltip(move |cx| Tooltip::text(format!("Indexing {crate_name}…"), cx))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn make_lsp_adapter_delegate(
|
||||
project: &Model<Project>,
|
||||
cx: &mut AppContext,
|
||||
@@ -3199,6 +3351,19 @@ fn make_lsp_adapter_delegate(
|
||||
})
|
||||
}
|
||||
|
||||
fn slash_command_error_block_renderer(message: String) -> RenderBlock {
|
||||
Box::new(move |_| {
|
||||
div()
|
||||
.pl_6()
|
||||
.child(
|
||||
Label::new(format!("error: {}", message))
|
||||
.single_line()
|
||||
.color(Color::Error),
|
||||
)
|
||||
.into_any()
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -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},
|
||||
@@ -23,6 +24,7 @@ pub enum CloudModel {
|
||||
Gpt4Turbo,
|
||||
#[default]
|
||||
Gpt4Omni,
|
||||
Claude3_5Sonnet,
|
||||
Claude3Opus,
|
||||
Claude3Sonnet,
|
||||
Claude3Haiku,
|
||||
@@ -104,6 +106,7 @@ impl CloudModel {
|
||||
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",
|
||||
@@ -117,6 +120,7 @@ impl CloudModel {
|
||||
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",
|
||||
@@ -129,7 +133,10 @@ impl CloudModel {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -168,6 +175,11 @@ pub enum AssistantProvider {
|
||||
api_url: String,
|
||||
low_speed_timeout_in_seconds: Option<u64>,
|
||||
},
|
||||
Ollama {
|
||||
model: OllamaModel,
|
||||
api_url: String,
|
||||
low_speed_timeout_in_seconds: Option<u64>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for AssistantProvider {
|
||||
@@ -197,6 +209,12 @@ pub enum AssistantProviderContent {
|
||||
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>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@@ -328,6 +346,13 @@ impl AssistantSettingsContent {
|
||||
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,
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -472,6 +497,27 @@ impl Settings for AssistantSettings {
|
||||
Some(low_speed_timeout_in_seconds_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,
|
||||
@@ -519,6 +565,15 @@ impl Settings for AssistantSettings {
|
||||
.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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@ mod anthropic;
|
||||
mod cloud;
|
||||
#[cfg(test)]
|
||||
mod fake;
|
||||
mod ollama;
|
||||
mod open_ai;
|
||||
|
||||
pub use anthropic::*;
|
||||
pub use cloud::*;
|
||||
#[cfg(test)]
|
||||
pub use fake::*;
|
||||
pub use ollama::*;
|
||||
pub use open_ai::*;
|
||||
|
||||
use crate::{
|
||||
@@ -50,6 +52,18 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
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);
|
||||
|
||||
@@ -87,6 +101,24 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
settings_version,
|
||||
);
|
||||
}
|
||||
|
||||
(
|
||||
CompletionProvider::Ollama(provider),
|
||||
AssistantProvider::Ollama {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
},
|
||||
) => {
|
||||
provider.update(
|
||||
model.clone(),
|
||||
api_url.clone(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
settings_version,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
(CompletionProvider::Cloud(provider), AssistantProvider::ZedDotDev { model }) => {
|
||||
provider.update(model.clone(), settings_version);
|
||||
}
|
||||
@@ -130,6 +162,23 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
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,
|
||||
));
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -142,6 +191,7 @@ pub enum CompletionProvider {
|
||||
Cloud(CloudCompletionProvider),
|
||||
#[cfg(test)]
|
||||
Fake(FakeCompletionProvider),
|
||||
Ollama(OllamaCompletionProvider),
|
||||
}
|
||||
|
||||
impl gpui::Global for CompletionProvider {}
|
||||
@@ -165,6 +215,10 @@ impl CompletionProvider {
|
||||
.available_models()
|
||||
.map(LanguageModel::Cloud)
|
||||
.collect(),
|
||||
CompletionProvider::Ollama(provider) => provider
|
||||
.available_models()
|
||||
.map(|model| LanguageModel::Ollama(model.clone()))
|
||||
.collect(),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => unimplemented!(),
|
||||
}
|
||||
@@ -175,6 +229,7 @@ impl CompletionProvider {
|
||||
CompletionProvider::OpenAi(provider) => provider.settings_version(),
|
||||
CompletionProvider::Anthropic(provider) => provider.settings_version(),
|
||||
CompletionProvider::Cloud(provider) => provider.settings_version(),
|
||||
CompletionProvider::Ollama(provider) => provider.settings_version(),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => unimplemented!(),
|
||||
}
|
||||
@@ -185,6 +240,7 @@ impl CompletionProvider {
|
||||
CompletionProvider::OpenAi(provider) => provider.is_authenticated(),
|
||||
CompletionProvider::Anthropic(provider) => provider.is_authenticated(),
|
||||
CompletionProvider::Cloud(provider) => provider.is_authenticated(),
|
||||
CompletionProvider::Ollama(provider) => provider.is_authenticated(),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => true,
|
||||
}
|
||||
@@ -195,6 +251,7 @@ impl CompletionProvider {
|
||||
CompletionProvider::OpenAi(provider) => provider.authenticate(cx),
|
||||
CompletionProvider::Anthropic(provider) => provider.authenticate(cx),
|
||||
CompletionProvider::Cloud(provider) => provider.authenticate(cx),
|
||||
CompletionProvider::Ollama(provider) => provider.authenticate(cx),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => Task::ready(Ok(())),
|
||||
}
|
||||
@@ -205,6 +262,7 @@ impl CompletionProvider {
|
||||
CompletionProvider::OpenAi(provider) => provider.authentication_prompt(cx),
|
||||
CompletionProvider::Anthropic(provider) => provider.authentication_prompt(cx),
|
||||
CompletionProvider::Cloud(provider) => provider.authentication_prompt(cx),
|
||||
CompletionProvider::Ollama(provider) => provider.authentication_prompt(cx),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => unimplemented!(),
|
||||
}
|
||||
@@ -215,6 +273,7 @@ impl CompletionProvider {
|
||||
CompletionProvider::OpenAi(provider) => provider.reset_credentials(cx),
|
||||
CompletionProvider::Anthropic(provider) => provider.reset_credentials(cx),
|
||||
CompletionProvider::Cloud(_) => Task::ready(Ok(())),
|
||||
CompletionProvider::Ollama(provider) => provider.reset_credentials(cx),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => Task::ready(Ok(())),
|
||||
}
|
||||
@@ -225,6 +284,7 @@ impl CompletionProvider {
|
||||
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(_) => LanguageModel::default(),
|
||||
}
|
||||
@@ -239,6 +299,7 @@ impl CompletionProvider {
|
||||
CompletionProvider::OpenAi(provider) => provider.count_tokens(request, cx),
|
||||
CompletionProvider::Anthropic(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))),
|
||||
}
|
||||
@@ -252,6 +313,7 @@ impl CompletionProvider {
|
||||
CompletionProvider::OpenAi(provider) => provider.complete(request),
|
||||
CompletionProvider::Anthropic(provider) => provider.complete(request),
|
||||
CompletionProvider::Cloud(provider) => provider.complete(request),
|
||||
CompletionProvider::Ollama(provider) => provider.complete(request),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(provider) => provider.complete(),
|
||||
}
|
||||
|
||||
@@ -349,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),
|
||||
),
|
||||
|
||||
@@ -101,7 +101,10 @@ impl CloudCompletionProvider {
|
||||
count_open_ai_tokens(request, cx.background_executor())
|
||||
}
|
||||
LanguageModel::Cloud(
|
||||
CloudModel::Claude3Opus | CloudModel::Claude3Sonnet | CloudModel::Claude3Haiku,
|
||||
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())
|
||||
|
||||
348
crates/assistant/src/completion_provider/ollama.rs
Normal file
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()
|
||||
}
|
||||
}
|
||||
@@ -210,6 +210,7 @@ pub fn count_open_ai_tokens(
|
||||
|
||||
match request.model {
|
||||
LanguageModel::Anthropic(_)
|
||||
| LanguageModel::Cloud(CloudModel::Claude3_5Sonnet)
|
||||
| LanguageModel::Cloud(CloudModel::Claude3Opus)
|
||||
| LanguageModel::Cloud(CloudModel::Claude3Sonnet)
|
||||
| LanguageModel::Cloud(CloudModel::Claude3Haiku) => {
|
||||
@@ -336,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),
|
||||
),
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
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::{paths::CONTEXTS_DIR, ResultExt, TryFutureExt};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedMessage {
|
||||
@@ -26,10 +28,22 @@ pub struct SavedContext {
|
||||
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.2.0";
|
||||
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)]
|
||||
@@ -62,7 +76,7 @@ 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 (mut events, _) = fs.watch(contexts_dir(), CONTEXT_WATCH_DURATION).await;
|
||||
|
||||
let this = cx.new_model(|cx: &mut ModelContext<Self>| Self {
|
||||
contexts_metadata: Vec::new(),
|
||||
@@ -99,6 +113,20 @@ impl ContextStore {
|
||||
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)?;
|
||||
@@ -110,6 +138,7 @@ impl ContextStore {
|
||||
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)),
|
||||
@@ -152,9 +181,9 @@ impl ContextStore {
|
||||
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?;
|
||||
fs.create_dir(contexts_dir()).await?;
|
||||
|
||||
let mut paths = fs.read_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?;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
use crate::{assistant_settings::AssistantSettings, CompletionProvider, ToggleModelSelector};
|
||||
use fs::Fs;
|
||||
use settings::update_settings_file;
|
||||
use ui::{popover_menu, prelude::*, ButtonLike, ContextMenu, PopoverMenuHandle, Tooltip};
|
||||
use ui::{prelude::*, ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ModelSelector {
|
||||
@@ -19,7 +19,7 @@ impl ModelSelector {
|
||||
|
||||
impl RenderOnce for ModelSelector {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
popover_menu("model-switcher")
|
||||
PopoverMenu::new("model-switcher")
|
||||
.with_handle(self.handle)
|
||||
.menu(move |cx| {
|
||||
ContextMenu::build(cx, |mut menu, cx| {
|
||||
|
||||
@@ -13,10 +13,9 @@ use futures::{
|
||||
};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
actions, percentage, point, size, Animation, AnimationExt, AnyElement, AppContext,
|
||||
BackgroundExecutor, Bounds, DevicePixels, EventEmitter, Global, PromptLevel, ReadGlobal,
|
||||
Subscription, Task, TitlebarOptions, Transformation, UpdateGlobal, View, WindowBounds,
|
||||
WindowHandle, WindowOptions,
|
||||
actions, percentage, point, size, Animation, AnimationExt, AppContext, BackgroundExecutor,
|
||||
Bounds, EventEmitter, Global, PromptLevel, ReadGlobal, Subscription, Task, TitlebarOptions,
|
||||
Transformation, UpdateGlobal, View, WindowBounds, WindowHandle, WindowOptions,
|
||||
};
|
||||
use heed::{types::SerdeBincode, Database, RoTxn};
|
||||
use language::{language_settings::SoftWrap, Buffer, LanguageRegistry};
|
||||
@@ -26,6 +25,7 @@ use rope::Rope;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
cmp::Reverse,
|
||||
future::Future,
|
||||
path::PathBuf,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
@@ -33,10 +33,10 @@ use std::{
|
||||
};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
div, prelude::*, IconButtonShape, ListHeader, ListItem, ListItemSpacing, ListSubHeader,
|
||||
ParentElement, Render, SharedString, Styled, TitleBar, Tooltip, ViewContext, VisualContext,
|
||||
div, prelude::*, IconButtonShape, ListItem, ListItemSpacing, ParentElement, Render,
|
||||
SharedString, Styled, TitleBar, Tooltip, ViewContext, VisualContext,
|
||||
};
|
||||
use util::{paths::PROMPTS_DIR, ResultExt, TryFutureExt};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use uuid::Uuid;
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -48,7 +48,7 @@ actions!(
|
||||
/// Init starts loading the PromptStore in the background and assigns
|
||||
/// a shared future to a global.
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
let db_path = PROMPTS_DIR.join("prompts-library-db.0.mdb");
|
||||
let db_path = paths::prompts_dir().join("prompts-library-db.0.mdb");
|
||||
let prompt_store_future = PromptStore::new(db_path, cx.background_executor().clone())
|
||||
.then(|result| future::ready(result.map(Arc::new).map_err(Arc::new)))
|
||||
.boxed()
|
||||
@@ -80,11 +80,7 @@ pub fn open_prompt_library(
|
||||
cx.spawn(|cx| async move {
|
||||
let store = store.await?;
|
||||
cx.update(|cx| {
|
||||
let bounds = Bounds::centered(
|
||||
None,
|
||||
size(DevicePixels::from(1024), DevicePixels::from(768)),
|
||||
cx,
|
||||
);
|
||||
let bounds = Bounds::centered(None, size(px(1024.0), px(768.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
titlebar: Some(TitlebarOptions {
|
||||
@@ -97,7 +93,7 @@ pub fn open_prompt_library(
|
||||
},
|
||||
|cx| cx.new_view(|cx| PromptLibrary::new(store, language_registry, cx)),
|
||||
)
|
||||
})
|
||||
})?
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -124,41 +120,23 @@ struct PromptEditor {
|
||||
struct PromptPickerDelegate {
|
||||
store: Arc<PromptStore>,
|
||||
selected_index: usize,
|
||||
entries: Vec<PromptPickerEntry>,
|
||||
matches: Vec<PromptMetadata>,
|
||||
}
|
||||
|
||||
enum PromptPickerEvent {
|
||||
Selected { prompt_id: Option<PromptId> },
|
||||
Selected { prompt_id: PromptId },
|
||||
Confirmed { prompt_id: PromptId },
|
||||
Deleted { prompt_id: PromptId },
|
||||
ToggledDefault { prompt_id: PromptId },
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum PromptPickerEntry {
|
||||
DefaultPromptsHeader,
|
||||
DefaultPromptsEmpty,
|
||||
AllPromptsHeader,
|
||||
AllPromptsEmpty,
|
||||
Prompt(PromptMetadata),
|
||||
}
|
||||
|
||||
impl PromptPickerEntry {
|
||||
fn prompt_id(&self) -> Option<PromptId> {
|
||||
match self {
|
||||
PromptPickerEntry::Prompt(metadata) => Some(metadata.id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PromptPickerEvent> for Picker<PromptPickerDelegate> {}
|
||||
|
||||
impl PickerDelegate for PromptPickerDelegate {
|
||||
type ListItem = AnyElement;
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.entries.len()
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
@@ -167,14 +145,11 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.selected_index = ix;
|
||||
let prompt_id = if let Some(PromptPickerEntry::Prompt(prompt)) =
|
||||
self.entries.get(self.selected_index)
|
||||
{
|
||||
Some(prompt.id)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
cx.emit(PromptPickerEvent::Selected { prompt_id });
|
||||
if let Some(prompt) = self.matches.get(self.selected_index) {
|
||||
cx.emit(PromptPickerEvent::Selected {
|
||||
prompt_id: prompt.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
|
||||
@@ -183,48 +158,24 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
let search = self.store.search(query);
|
||||
let prev_prompt_id = self
|
||||
.entries
|
||||
.get(self.selected_index)
|
||||
.and_then(|mat| mat.prompt_id());
|
||||
let prev_prompt_id = self.matches.get(self.selected_index).map(|mat| mat.id);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let (entries, selected_index) = cx
|
||||
let (matches, selected_index) = cx
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
let prompts = search.await;
|
||||
let (default_prompts, prompts) = prompts
|
||||
.into_iter()
|
||||
.partition::<Vec<_>, _>(|prompt| prompt.default);
|
||||
|
||||
let mut entries = Vec::new();
|
||||
entries.push(PromptPickerEntry::DefaultPromptsHeader);
|
||||
if default_prompts.is_empty() {
|
||||
entries.push(PromptPickerEntry::DefaultPromptsEmpty);
|
||||
} else {
|
||||
entries.extend(default_prompts.into_iter().map(PromptPickerEntry::Prompt));
|
||||
}
|
||||
|
||||
entries.push(PromptPickerEntry::AllPromptsHeader);
|
||||
if prompts.is_empty() {
|
||||
entries.push(PromptPickerEntry::AllPromptsEmpty);
|
||||
} else {
|
||||
entries.extend(prompts.into_iter().map(PromptPickerEntry::Prompt));
|
||||
}
|
||||
let matches = search.await;
|
||||
|
||||
let selected_index = prev_prompt_id
|
||||
.and_then(|prev_prompt_id| {
|
||||
entries
|
||||
.iter()
|
||||
.position(|entry| entry.prompt_id() == Some(prev_prompt_id))
|
||||
matches.iter().position(|entry| entry.id == prev_prompt_id)
|
||||
})
|
||||
.or_else(|| entries.iter().position(|entry| entry.prompt_id().is_some()))
|
||||
.unwrap_or(0);
|
||||
(entries, selected_index)
|
||||
(matches, selected_index)
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.delegate.entries = entries;
|
||||
this.delegate.matches = matches;
|
||||
this.delegate.set_selected_index(selected_index, cx);
|
||||
cx.notify();
|
||||
})
|
||||
@@ -233,7 +184,7 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
if let Some(PromptPickerEntry::Prompt(prompt)) = self.entries.get(self.selected_index) {
|
||||
if let Some(prompt) = self.matches.get(self.selected_index) {
|
||||
cx.emit(PromptPickerEvent::Confirmed {
|
||||
prompt_id: prompt.id,
|
||||
});
|
||||
@@ -248,82 +199,59 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
selected: bool,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let prompt = self.entries.get(ix)?;
|
||||
let element = match prompt {
|
||||
PromptPickerEntry::DefaultPromptsHeader => ListHeader::new("Default Prompts")
|
||||
.inset(true)
|
||||
.start_slot(
|
||||
Icon::new(IconName::Sparkle)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
.selected(selected)
|
||||
.into_any_element(),
|
||||
PromptPickerEntry::DefaultPromptsEmpty => {
|
||||
ListSubHeader::new("Star a prompt to add it to the default context")
|
||||
.inset(true)
|
||||
.selected(selected)
|
||||
.into_any_element()
|
||||
}
|
||||
PromptPickerEntry::AllPromptsHeader => ListHeader::new("All Prompts")
|
||||
.inset(true)
|
||||
.start_slot(
|
||||
Icon::new(IconName::Library)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
.selected(selected)
|
||||
.into_any_element(),
|
||||
PromptPickerEntry::AllPromptsEmpty => ListSubHeader::new("No prompts")
|
||||
.inset(true)
|
||||
.selected(selected)
|
||||
.into_any_element(),
|
||||
PromptPickerEntry::Prompt(prompt) => {
|
||||
let default = prompt.default;
|
||||
let prompt_id = prompt.id;
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.selected(selected)
|
||||
.child(h_flex().h_5().line_height(relative(1.)).child(Label::new(
|
||||
prompt.title.clone().unwrap_or("Untitled".into()),
|
||||
)))
|
||||
.end_hover_slot(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
IconButton::new("delete-prompt", IconName::Trash)
|
||||
.icon_color(Color::Muted)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| Tooltip::text("Delete Prompt", cx))
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
cx.emit(PromptPickerEvent::Deleted { prompt_id })
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("toggle-default-prompt", IconName::Sparkle)
|
||||
.selected(default)
|
||||
.selected_icon(IconName::SparkleFilled)
|
||||
.icon_color(if default { Color::Accent } else { Color::Muted })
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::text(
|
||||
if default {
|
||||
"Remove from Default Prompt"
|
||||
} else {
|
||||
"Add to Default Prompt"
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
cx.emit(PromptPickerEvent::ToggledDefault { prompt_id })
|
||||
})),
|
||||
),
|
||||
let prompt = self.matches.get(ix)?;
|
||||
let default = prompt.default;
|
||||
let prompt_id = prompt.id;
|
||||
let element = ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.selected(selected)
|
||||
.child(h_flex().h_5().line_height(relative(1.)).child(Label::new(
|
||||
prompt.title.clone().unwrap_or("Untitled".into()),
|
||||
)))
|
||||
.end_slot::<IconButton>(default.then(|| {
|
||||
IconButton::new("toggle-default-prompt", IconName::SparkleFilled)
|
||||
.selected(true)
|
||||
.icon_color(Color::Accent)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| Tooltip::text("Remove from Default Prompt", cx))
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
cx.emit(PromptPickerEvent::ToggledDefault { prompt_id })
|
||||
}))
|
||||
}))
|
||||
.end_hover_slot(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
IconButton::new("delete-prompt", IconName::Trash)
|
||||
.icon_color(Color::Muted)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| Tooltip::text("Delete Prompt", cx))
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
cx.emit(PromptPickerEvent::Deleted { prompt_id })
|
||||
})),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
};
|
||||
.child(
|
||||
IconButton::new("toggle-default-prompt", IconName::Sparkle)
|
||||
.selected(default)
|
||||
.selected_icon(IconName::SparkleFilled)
|
||||
.icon_color(if default { Color::Accent } else { Color::Muted })
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::text(
|
||||
if default {
|
||||
"Remove from Default Prompt"
|
||||
} else {
|
||||
"Add to Default Prompt"
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
cx.emit(PromptPickerEvent::ToggledDefault { prompt_id })
|
||||
})),
|
||||
),
|
||||
);
|
||||
Some(element)
|
||||
}
|
||||
|
||||
@@ -349,11 +277,13 @@ impl PromptLibrary {
|
||||
let delegate = PromptPickerDelegate {
|
||||
store: store.clone(),
|
||||
selected_index: 0,
|
||||
entries: Vec::new(),
|
||||
matches: Vec::new(),
|
||||
};
|
||||
|
||||
let picker = cx.new_view(|cx| {
|
||||
let picker = Picker::list(delegate, cx).modal(false).max_height(None);
|
||||
let picker = Picker::uniform_list(delegate, cx)
|
||||
.modal(false)
|
||||
.max_height(None);
|
||||
picker.focus(cx);
|
||||
picker
|
||||
});
|
||||
@@ -376,11 +306,7 @@ impl PromptLibrary {
|
||||
) {
|
||||
match event {
|
||||
PromptPickerEvent::Selected { prompt_id } => {
|
||||
if let Some(prompt_id) = *prompt_id {
|
||||
self.load_prompt(prompt_id, false, cx);
|
||||
} else {
|
||||
self.focus_picker(&Default::default(), cx);
|
||||
}
|
||||
self.load_prompt(*prompt_id, false, cx);
|
||||
}
|
||||
PromptPickerEvent::Confirmed { prompt_id } => {
|
||||
self.load_prompt(*prompt_id, true, cx);
|
||||
@@ -524,6 +450,7 @@ impl PromptLibrary {
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor.set_show_wrap_guides(false, cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_use_modal_editing(false);
|
||||
editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
|
||||
editor.set_completion_provider(Box::new(
|
||||
SlashCommandCompletionProvider::new(commands, None, None),
|
||||
@@ -567,21 +494,23 @@ impl PromptLibrary {
|
||||
if let Some(prompt_id) = prompt_id {
|
||||
if picker
|
||||
.delegate
|
||||
.entries
|
||||
.matches
|
||||
.get(picker.delegate.selected_index())
|
||||
.map_or(true, |old_selected_prompt| {
|
||||
old_selected_prompt.prompt_id() != Some(prompt_id)
|
||||
old_selected_prompt.id != prompt_id
|
||||
})
|
||||
{
|
||||
if let Some(ix) = picker
|
||||
.delegate
|
||||
.entries
|
||||
.matches
|
||||
.iter()
|
||||
.position(|mat| mat.prompt_id() == Some(prompt_id))
|
||||
.position(|mat| mat.id == prompt_id)
|
||||
{
|
||||
picker.set_selected_index(ix, true, cx);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
picker.focus(cx);
|
||||
}
|
||||
});
|
||||
cx.notify();
|
||||
@@ -640,7 +569,7 @@ impl PromptLibrary {
|
||||
let provider = CompletionProvider::global(cx);
|
||||
if provider.is_authenticated() {
|
||||
InlineAssistant::update_global(cx, |assistant, cx| {
|
||||
assistant.assist(&prompt_editor, None, false, cx)
|
||||
assistant.assist(&prompt_editor, None, None, cx)
|
||||
})
|
||||
} else {
|
||||
for window in cx.windows() {
|
||||
@@ -660,19 +589,6 @@ impl PromptLibrary {
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel_last_inline_assist(
|
||||
&mut self,
|
||||
_: &editor::actions::Cancel,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let canceled = InlineAssistant::update_global(cx, |assistant, cx| {
|
||||
assistant.cancel_last_inline_assist(cx)
|
||||
});
|
||||
if !canceled {
|
||||
cx.propagate();
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_prompt_editor_event(
|
||||
&mut self,
|
||||
prompt_id: PromptId,
|
||||
@@ -811,7 +727,6 @@ impl PromptLibrary {
|
||||
div()
|
||||
.on_action(cx.listener(Self::focus_picker))
|
||||
.on_action(cx.listener(Self::inline_assist))
|
||||
.on_action(cx.listener(Self::cancel_last_inline_assist))
|
||||
.flex_grow()
|
||||
.h_full()
|
||||
.pt(Spacing::XXLarge.rems(cx))
|
||||
@@ -917,13 +832,8 @@ impl PromptLibrary {
|
||||
|
||||
impl Render for PromptLibrary {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let (ui_font, ui_font_size) = {
|
||||
let theme_settings = ThemeSettings::get_global(cx);
|
||||
(theme_settings.ui_font.clone(), theme_settings.ui_font_size)
|
||||
};
|
||||
|
||||
let ui_font = theme::setup_ui_font(cx);
|
||||
let theme = cx.theme().clone();
|
||||
cx.set_rem_size(ui_font_size);
|
||||
|
||||
h_flex()
|
||||
.id("prompt-manager")
|
||||
@@ -1105,7 +1015,7 @@ impl PromptStore {
|
||||
let cached_metadata = self.metadata_cache.read().metadata.clone();
|
||||
let executor = self.executor.clone();
|
||||
self.executor.spawn(async move {
|
||||
if query.is_empty() {
|
||||
let mut matches = if query.is_empty() {
|
||||
cached_metadata
|
||||
} else {
|
||||
let candidates = cached_metadata
|
||||
@@ -1131,7 +1041,9 @@ impl PromptStore {
|
||||
.into_iter()
|
||||
.map(|mat| cached_metadata[mat.candidate_id].clone())
|
||||
.collect()
|
||||
}
|
||||
};
|
||||
matches.sort_by_key(|metadata| Reverse(metadata.default));
|
||||
matches
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -33,35 +33,32 @@ pub fn generate_content_prompt(
|
||||
)?;
|
||||
}
|
||||
|
||||
// Include file content.
|
||||
for chunk in buffer.text_for_range(0..range.start) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
|
||||
writeln!(
|
||||
prompt,
|
||||
"The user has the following file open in the editor:"
|
||||
)?;
|
||||
if range.is_empty() {
|
||||
prompt.push_str("<|START|>");
|
||||
} else {
|
||||
prompt.push_str("<|START|");
|
||||
}
|
||||
write!(prompt, "```")?;
|
||||
if let Some(language_name) = language_name {
|
||||
write!(prompt, "{language_name}")?;
|
||||
}
|
||||
|
||||
for chunk in buffer.text_for_range(range.clone()) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
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');
|
||||
|
||||
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."
|
||||
"Assume the cursor is located where the `<|CURSOR|>` span is."
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
@@ -75,11 +72,42 @@ pub fn generate_content_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();
|
||||
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,
|
||||
"Double check that you only return code and not the '<|START|' and '|END|'> spans"
|
||||
"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();
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ 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,17 +14,22 @@ use std::{
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use ui::ActiveTheme;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub mod active_command;
|
||||
pub mod auto_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 {
|
||||
commands: Arc<SlashCommandRegistry>,
|
||||
@@ -216,6 +221,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)) =
|
||||
@@ -344,3 +350,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;
|
||||
@@ -24,9 +28,9 @@ impl SlashCommand for ActiveSlashCommand {
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
&self,
|
||||
self: Arc<Self>,
|
||||
_query: String,
|
||||
_cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
@@ -57,46 +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()
|
||||
}),
|
||||
}],
|
||||
run_commands_in_text: false,
|
||||
path.as_deref(),
|
||||
false,
|
||||
None,
|
||||
)],
|
||||
run_commands_in_text: has_diagnostics,
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
193
crates/assistant/src/slash_command/auto_command.rs
Normal file
193
crates/assistant/src/slash_command/auto_command.rs
Normal file
@@ -0,0 +1,193 @@
|
||||
use super::create_label_for_command;
|
||||
use super::{SlashCommand, SlashCommandOutput};
|
||||
use crate::{CompletionProvider, LanguageModelRequest, LanguageModelRequestMessage, Role};
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures::StreamExt;
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use language::{CodeLabel, LspAdapterDelegate};
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
use ui::WindowContext;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct AutoCommand;
|
||||
|
||||
impl SlashCommand for AutoCommand {
|
||||
fn name(&self) -> String {
|
||||
"auto".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Automatically infer what context to add, based on your prompt".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
"Automatically Infer Context".into()
|
||||
}
|
||||
|
||||
fn label(&self, cx: &AppContext) -> CodeLabel {
|
||||
create_label_for_command("auto", &["--prompt"], cx)
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
_query: String,
|
||||
_cancellation_flag: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
// There's no autocomplete for a prompt, since it's arbitrary text.
|
||||
Task::ready(Ok(Vec::new()))
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
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 prompt")));
|
||||
};
|
||||
|
||||
let prompt = format!("{PROMPT_INSTRUCTIONS_BEFORE_SUMMARY}\n{SUMMARY}\n{PROMPT_INSTRUCTIONS_AFTER_SUMMARY}\n{argument}");
|
||||
let request = LanguageModelRequest {
|
||||
model: CompletionProvider::global(cx).model(),
|
||||
messages: vec![LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: prompt,
|
||||
}],
|
||||
stop: vec![],
|
||||
temperature: 1.0,
|
||||
};
|
||||
|
||||
let stream = CompletionProvider::global(cx).complete(request);
|
||||
let mut wip_action: String = String::new();
|
||||
let task: Task<Result<String>> = cx.spawn(|_cx| async move {
|
||||
let mut actions_text = String::new();
|
||||
let stream_completion = async {
|
||||
let mut messages = stream.await?;
|
||||
|
||||
while let Some(message) = messages.next().await {
|
||||
let text = message?;
|
||||
|
||||
chunked_line(&mut wip_action, &text, |line| {
|
||||
actions_text.push('/');
|
||||
actions_text.push_str(line);
|
||||
actions_text.push('\n');
|
||||
});
|
||||
|
||||
smol::future::yield_now().await;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
};
|
||||
|
||||
stream_completion.await?;
|
||||
|
||||
Ok(actions_text)
|
||||
});
|
||||
|
||||
// As a convenience, append /auto's argument to the end of the prompt so you don't have to write it again.
|
||||
let argument = argument.to_string();
|
||||
|
||||
cx.background_executor().spawn(async move {
|
||||
let mut text = task.await?;
|
||||
|
||||
text.push_str(&argument);
|
||||
|
||||
Ok(SlashCommandOutput {
|
||||
text,
|
||||
sections: Vec::new(),
|
||||
run_commands_in_text: true,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
const PROMPT_INSTRUCTIONS_BEFORE_SUMMARY: &str = r#"
|
||||
I'm going to give you a prompt. I don't want you to respond
|
||||
to the prompt itself. I want you to figure out which of the following
|
||||
actions on my project, if any, would help you answer the prompt.
|
||||
|
||||
Here are the actions:
|
||||
|
||||
## file
|
||||
|
||||
This action's parameter is a file path to one of the files
|
||||
in the project. If you ask for this action, I will tell you
|
||||
the full contents of the file, so you can learn all the
|
||||
details of the file.
|
||||
|
||||
## search
|
||||
|
||||
This action's parameter is a string to search for across
|
||||
the project. It will tell you which files this string
|
||||
(or similar strings; it is a semantic search) appear in,
|
||||
as well as some context of the lines surrounding each result.
|
||||
|
||||
---
|
||||
|
||||
That was the end of the list of actions.
|
||||
|
||||
Here is an XML summary of each of the files in my project:
|
||||
"#;
|
||||
|
||||
const PROMPT_INSTRUCTIONS_AFTER_SUMMARY: &str = r#"
|
||||
Actions have a cost, so only include actions that you think
|
||||
will be helpful to you in doing a great job answering the
|
||||
prompt in the future.
|
||||
|
||||
You must respond ONLY with a list of actions you would like to
|
||||
perform. Each action should be on its own line, and followed by a space and then its parameter.
|
||||
|
||||
Actions can be performed more than once with different parameters.
|
||||
Here is an example valid response:
|
||||
|
||||
```
|
||||
file path/to/my/file.txt
|
||||
file path/to/another/file.txt
|
||||
search something to search for
|
||||
search something else to search for
|
||||
```
|
||||
|
||||
Once again, do not forget: you must respond ONLY in the format of
|
||||
one action per line, and the action name should be followed by
|
||||
its parameter. Your response must not include anything other
|
||||
than a list of actions, with one action per line, in this format.
|
||||
It is extremely important that you do not deviate from this format even slightly!
|
||||
|
||||
This is the end of my instructions for how to respond. The rest is the prompt:
|
||||
"#;
|
||||
|
||||
const SUMMARY: &str = "";
|
||||
|
||||
fn chunked_line(wip: &mut String, chunk: &str, mut on_line_end: impl FnMut(&str)) {
|
||||
// The first iteration of the loop should just push to wip
|
||||
// and nothing else. We only push what we encountered in
|
||||
// previous iterations of the loop.
|
||||
//
|
||||
// This correctly handles both the scenario where no
|
||||
// newlines are encountered (the loop will only run once,
|
||||
// and so will only push to wip), as well as the scenario
|
||||
// where the chunk contains at least one newline but
|
||||
// does not end in a newline (the last iteration of the
|
||||
// loop will update wip but will not run anything).
|
||||
let mut is_first_iteration = true;
|
||||
|
||||
for line in chunk.split('\n') {
|
||||
if is_first_iteration {
|
||||
is_first_iteration = false;
|
||||
} else {
|
||||
// Since this isn't the first iteration of the loop, we definitely hit a newline
|
||||
// at the end of the previous iteration! Run the function on whatever wip we have.
|
||||
on_line_end(wip);
|
||||
wip.clear();
|
||||
}
|
||||
|
||||
wip.push_str(line);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{prompt_command::PromptPlaceholder, SlashCommand, SlashCommandOutput};
|
||||
use super::{SlashCommand, SlashCommandOutput};
|
||||
use crate::prompt_library::PromptStore;
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::SlashCommandOutputSection;
|
||||
@@ -31,7 +31,7 @@ impl SlashCommand for DefaultSlashCommand {
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
&self,
|
||||
self: Arc<Self>,
|
||||
_query: String,
|
||||
_cancellation_flag: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
@@ -53,7 +53,7 @@ impl SlashCommand for DefaultSlashCommand {
|
||||
let prompts = store.default_prompt_metadata();
|
||||
|
||||
let mut text = String::new();
|
||||
writeln!(text, "Default Prompt:").unwrap();
|
||||
text.push('\n');
|
||||
for prompt in prompts {
|
||||
if let Some(title) = prompt.title {
|
||||
writeln!(text, "/prompt {}", title).unwrap();
|
||||
@@ -61,17 +61,15 @@ impl SlashCommand for DefaultSlashCommand {
|
||||
}
|
||||
text.pop();
|
||||
|
||||
if text.is_empty() {
|
||||
text.push('\n');
|
||||
}
|
||||
|
||||
Ok(SlashCommandOutput {
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range: 0..text.len(),
|
||||
render_placeholder: Arc::new(move |id, unfold, _cx| {
|
||||
PromptPlaceholder {
|
||||
title: "Default".into(),
|
||||
id,
|
||||
unfold,
|
||||
}
|
||||
.into_any_element()
|
||||
}),
|
||||
icon: IconName::Library,
|
||||
label: "Default".into(),
|
||||
}],
|
||||
text,
|
||||
run_commands_in_text: true,
|
||||
|
||||
490
crates/assistant/src/slash_command/diagnostics_command.rs
Normal file
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",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -5,12 +7,19 @@ 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;
|
||||
use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler};
|
||||
use http::{AsyncBody, HttpClient, HttpClientWithUrl};
|
||||
use language::LspAdapterDelegate;
|
||||
use ui::{prelude::*, ButtonLike, ElevationIndex};
|
||||
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 {
|
||||
@@ -37,7 +46,53 @@ impl FetchSlashCommand {
|
||||
);
|
||||
}
|
||||
|
||||
convert_html_to_markdown(&body[..])
|
||||
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)?
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +114,7 @@ impl SlashCommand for FetchSlashCommand {
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
&self,
|
||||
self: Arc<Self>,
|
||||
_query: String,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
@@ -98,37 +153,11 @@ impl SlashCommand for FetchSlashCommand {
|
||||
text,
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range,
|
||||
render_placeholder: Arc::new(move |id, unfold, _cx| {
|
||||
FetchPlaceholder {
|
||||
id,
|
||||
unfold,
|
||||
url: url.clone(),
|
||||
}
|
||||
.into_any_element()
|
||||
}),
|
||||
icon: IconName::AtSign,
|
||||
label: format!("fetch {}", url).into(),
|
||||
}],
|
||||
run_commands_in_text: false,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct FetchPlaceholder {
|
||||
pub id: ElementId,
|
||||
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
|
||||
pub url: SharedString,
|
||||
}
|
||||
|
||||
impl RenderOnce for FetchPlaceholder {
|
||||
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::AtSign))
|
||||
.child(Label::new(format!("fetch {url}", url = self.url)))
|
||||
.on_click(move |_, cx| unfold(cx))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<_>>();
|
||||
@@ -98,7 +100,7 @@ impl SlashCommand for FileSlashCommand {
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
&self,
|
||||
self: Arc<Self>,
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
@@ -139,88 +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()
|
||||
}),
|
||||
}],
|
||||
run_commands_in_text: false,
|
||||
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
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;
|
||||
@@ -102,7 +102,7 @@ impl SlashCommand for ProjectSlashCommand {
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
&self,
|
||||
self: Arc<Self>,
|
||||
_query: String,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
@@ -138,15 +138,8 @@ 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,
|
||||
})
|
||||
|
||||
@@ -5,7 +5,7 @@ use assistant_slash_command::SlashCommandOutputSection;
|
||||
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;
|
||||
@@ -28,7 +28,7 @@ impl SlashCommand for PromptSlashCommand {
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
&self,
|
||||
self: Arc<Self>,
|
||||
query: String,
|
||||
_cancellation_flag: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
@@ -69,42 +69,20 @@ impl SlashCommand for PromptSlashCommand {
|
||||
}
|
||||
});
|
||||
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| {
|
||||
PromptPlaceholder {
|
||||
id,
|
||||
unfold,
|
||||
title: title.clone(),
|
||||
}
|
||||
.into_any_element()
|
||||
}),
|
||||
icon: IconName::Library,
|
||||
label: title,
|
||||
}],
|
||||
run_commands_in_text: true,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct PromptPlaceholder {
|
||||
pub title: SharedString,
|
||||
pub id: ElementId,
|
||||
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
|
||||
}
|
||||
|
||||
impl RenderOnce for PromptPlaceholder {
|
||||
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::Library))
|
||||
.child(Label::new(self.title))
|
||||
.on_click(move |_, cx| unfold(cx))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,45 +7,37 @@ use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutp
|
||||
use fs::Fs;
|
||||
use futures::AsyncReadExt;
|
||||
use gpui::{AppContext, Model, Task, WeakView};
|
||||
use html_to_markdown::convert_rustdoc_to_markdown;
|
||||
use http::{AsyncBody, HttpClient, HttpClientWithUrl};
|
||||
use language::LspAdapterDelegate;
|
||||
use project::{Project, ProjectPath};
|
||||
use ui::{prelude::*, ButtonLike, ElevationIndex};
|
||||
use rustdoc::{convert_rustdoc_to_markdown, CrateName, LocalProvider, RustdocSource, RustdocStore};
|
||||
use ui::prelude::*;
|
||||
use util::{maybe, ResultExt};
|
||||
use workspace::Workspace;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum RustdocSource {
|
||||
/// The docs were sourced from local `cargo doc` output.
|
||||
Local,
|
||||
/// The docs were sourced from `docs.rs`.
|
||||
DocsDotRs,
|
||||
}
|
||||
|
||||
pub(crate) struct RustdocSlashCommand;
|
||||
|
||||
impl RustdocSlashCommand {
|
||||
async fn build_message(
|
||||
fs: Arc<dyn Fs>,
|
||||
http_client: Arc<HttpClientWithUrl>,
|
||||
crate_name: 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);
|
||||
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 {
|
||||
return Ok((
|
||||
RustdocSource::Local,
|
||||
convert_rustdoc_to_markdown(contents.as_bytes())?,
|
||||
));
|
||||
let (markdown, _items) = convert_rustdoc_to_markdown(contents.as_bytes())?;
|
||||
|
||||
return Ok((RustdocSource::Local, markdown));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,10 +70,9 @@ impl RustdocSlashCommand {
|
||||
);
|
||||
}
|
||||
|
||||
Ok((
|
||||
RustdocSource::DocsDotRs,
|
||||
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>> {
|
||||
@@ -116,13 +107,42 @@ impl SlashCommand for RustdocSlashCommand {
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
&self,
|
||||
_query: String,
|
||||
self: Arc<Self>,
|
||||
query: String,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<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(
|
||||
@@ -142,91 +162,77 @@ impl SlashCommand for RustdocSlashCommand {
|
||||
let project = workspace.read(cx).project().clone();
|
||||
let fs = project.read(cx).fs().clone();
|
||||
let http_client = workspace.read(cx).client().http_client();
|
||||
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) => crate_name.to_string(),
|
||||
Ok(crate_name) => CrateName::from(crate_name),
|
||||
Err(err) => return Task::ready(Err(err)),
|
||||
};
|
||||
let module_path = path_components.map(ToString::to_string).collect::<Vec<_>>();
|
||||
let path_to_cargo_toml = Self::path_to_cargo_toml(project, cx);
|
||||
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();
|
||||
let module_path = module_path.clone();
|
||||
let item_path = item_path.clone();
|
||||
async move {
|
||||
Self::build_message(
|
||||
fs,
|
||||
http_client,
|
||||
crate_name,
|
||||
module_path,
|
||||
path_to_cargo_toml.as_deref(),
|
||||
)
|
||||
.await
|
||||
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 module_path.is_empty() {
|
||||
let module_path = if item_path.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(SharedString::from(module_path.join("::")))
|
||||
Some(SharedString::from(item_path.join("::")))
|
||||
};
|
||||
cx.foreground_executor().spawn(async move {
|
||||
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,
|
||||
source,
|
||||
crate_name: crate_name.clone(),
|
||||
module_path: module_path.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 source: RustdocSource,
|
||||
pub crate_name: SharedString,
|
||||
pub module_path: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl RenderOnce for RustdocPlaceholder {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
let unfold = self.unfold;
|
||||
|
||||
let crate_path = self
|
||||
.module_path
|
||||
.map(|module_path| format!("{crate_name}::{module_path}", crate_name = self.crate_name))
|
||||
.unwrap_or(self.crate_name.to_string());
|
||||
|
||||
ButtonLike::new(self.id)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.child(Icon::new(IconName::FileRust))
|
||||
.child(Label::new(format!(
|
||||
"rustdoc ({source}): {crate_path}",
|
||||
source = match self.source {
|
||||
RustdocSource::Local => "local",
|
||||
RustdocSource::DocsDotRs => "docs.rs",
|
||||
}
|
||||
)))
|
||||
.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,14 +25,7 @@ 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 {
|
||||
@@ -44,7 +41,7 @@ impl SlashCommand for SearchSlashCommand {
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
&self,
|
||||
self: Arc<Self>,
|
||||
_query: String,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
@@ -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,47 +134,30 @@ 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 {
|
||||
|
||||
@@ -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;
|
||||
@@ -29,7 +32,7 @@ impl SlashCommand for TabsSlashCommand {
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
&self,
|
||||
self: Arc<Self>,
|
||||
_query: String,
|
||||
_cancel: Arc<std::sync::atomic::AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
@@ -75,44 +78,37 @@ 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,
|
||||
run_commands_in_text: false,
|
||||
run_commands_in_text: has_diagnostics,
|
||||
})
|
||||
}),
|
||||
Err(error) => Task::ready(Err(error)),
|
||||
|
||||
105
crates/assistant/src/slash_command/term_command.rs
Normal file
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);
|
||||
@@ -22,7 +23,7 @@ pub trait SlashCommand: 'static + Send + Sync {
|
||||
fn description(&self) -> String;
|
||||
fn menu_text(&self) -> String;
|
||||
fn complete_argument(
|
||||
&self,
|
||||
self: Arc<Self>,
|
||||
query: String,
|
||||
cancel: Arc<AtomicBool>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
@@ -49,14 +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,
|
||||
}
|
||||
|
||||
@@ -141,8 +141,13 @@ 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 poll_for_updates = ReleaseChannel::try_global(cx)
|
||||
.map(|channel| channel.poll_for_updates())
|
||||
.unwrap_or(false);
|
||||
|
||||
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
|
||||
@@ -186,6 +191,13 @@ pub fn check(_: &Check, cx: &mut WindowContext) {
|
||||
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 {
|
||||
|
||||
@@ -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, _| {
|
||||
|
||||
@@ -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())?;
|
||||
|
||||
@@ -888,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, _| {
|
||||
@@ -902,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| {
|
||||
|
||||
@@ -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,
|
||||
@@ -223,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())?;
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -548,6 +548,9 @@ impl Server {
|
||||
.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>)
|
||||
@@ -2580,14 +2583,13 @@ async fn rejoin_dev_server_projects(
|
||||
)
|
||||
.await?
|
||||
};
|
||||
notify_rejoined_projects(&mut rejoined_projects, &session)?;
|
||||
|
||||
response.send(proto::RejoinRemoteProjectsResponse {
|
||||
rejoined_projects: rejoined_projects
|
||||
.into_iter()
|
||||
.iter()
|
||||
.map(|project| project.to_proto())
|
||||
.collect(),
|
||||
})
|
||||
})?;
|
||||
notify_rejoined_projects(&mut rejoined_projects, &session)
|
||||
}
|
||||
|
||||
async fn reconnect_dev_server(
|
||||
@@ -4500,6 +4502,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
|
||||
|
||||
@@ -73,6 +73,7 @@ impl ConnectionPool {
|
||||
pub fn reset(&mut self) {
|
||||
self.connections.clear();
|
||||
self.connected_users.clear();
|
||||
self.connected_dev_servers.clear();
|
||||
self.channels.clear();
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
@@ -501,6 +504,29 @@ async fn test_dev_server_reconnect(
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_dev_server_restart(cx1: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppContext) {
|
||||
let (server, client1) = TestServer::start1(cx1).await;
|
||||
|
||||
let (_dev_server, remote_workspace) =
|
||||
create_dev_server_project(&server, client1.app_state.clone(), cx1, cx2).await;
|
||||
let cx = VisualTestContext::from_window(remote_workspace.into(), cx1).as_mut();
|
||||
|
||||
server.reset().await;
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.simulate_keystrokes("cmd-p 1 enter");
|
||||
remote_workspace
|
||||
.update(cx, |ws, cx| {
|
||||
ws.active_item_as::<Editor>(cx)
|
||||
.unwrap()
|
||||
.update(cx, |ed, cx| {
|
||||
assert_eq!(ed.text(cx).to_string(), "remote\nremote\nremote");
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_create_dev_server_project_path_validation(
|
||||
cx1: &mut gpui::TestAppContext,
|
||||
@@ -572,7 +598,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();
|
||||
|
||||
|
||||
@@ -28,8 +28,9 @@ use language::{
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use project::{
|
||||
project_settings::{InlineBlameSettings, ProjectSettings},
|
||||
SERVER_PROGRESS_DEBOUNCE_TIMEOUT,
|
||||
SERVER_PROGRESS_THROTTLE_TIMEOUT,
|
||||
};
|
||||
use recent_projects::disconnected_overlay::DisconnectedOverlay;
|
||||
use rpc::RECEIVE_TIMEOUT;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
@@ -59,6 +60,7 @@ async fn test_host_disconnect(
|
||||
.await;
|
||||
|
||||
cx_b.update(editor::init);
|
||||
cx_b.update(recent_projects::init);
|
||||
|
||||
client_a
|
||||
.fs()
|
||||
@@ -83,7 +85,7 @@ async fn test_host_disconnect(
|
||||
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
|
||||
cx_a.background_executor.run_until_parked();
|
||||
|
||||
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
|
||||
assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer()));
|
||||
|
||||
let workspace_b = cx_b
|
||||
.add_window(|cx| Workspace::new(None, project_b.clone(), client_b.app_state.clone(), cx));
|
||||
@@ -120,14 +122,13 @@ async fn test_host_disconnect(
|
||||
|
||||
project_b.read_with(cx_b, |project, _| project.is_read_only());
|
||||
|
||||
assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
|
||||
assert!(worktree_a.read_with(cx_a, |tree, _| !tree.has_update_observer()));
|
||||
|
||||
// Ensure client B's edited state is reset and that the whole window is blurred.
|
||||
|
||||
workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
assert_eq!(cx.focused(), None);
|
||||
assert!(!workspace.is_edited())
|
||||
assert!(workspace.active_modal::<DisconnectedOverlay>(cx).is_some());
|
||||
assert!(!workspace.is_edited());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
@@ -1005,6 +1006,8 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
fake_language_server.start_progress("the-token").await;
|
||||
|
||||
executor.advance_clock(SERVER_PROGRESS_THROTTLE_TIMEOUT);
|
||||
fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
|
||||
token: lsp::NumberOrString::String("the-token".to_string()),
|
||||
value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
|
||||
@@ -1014,11 +1017,10 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
},
|
||||
)),
|
||||
});
|
||||
executor.advance_clock(SERVER_PROGRESS_DEBOUNCE_TIMEOUT);
|
||||
executor.run_until_parked();
|
||||
|
||||
project_a.read_with(cx_a, |project, _| {
|
||||
let status = project.language_server_statuses().next().unwrap();
|
||||
let status = project.language_server_statuses().next().unwrap().1;
|
||||
assert_eq!(status.name, "the-language-server");
|
||||
assert_eq!(status.pending_work.len(), 1);
|
||||
assert_eq!(
|
||||
@@ -1035,10 +1037,11 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
|
||||
|
||||
project_b.read_with(cx_b, |project, _| {
|
||||
let status = project.language_server_statuses().next().unwrap();
|
||||
let status = project.language_server_statuses().next().unwrap().1;
|
||||
assert_eq!(status.name, "the-language-server");
|
||||
});
|
||||
|
||||
executor.advance_clock(SERVER_PROGRESS_THROTTLE_TIMEOUT);
|
||||
fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
|
||||
token: lsp::NumberOrString::String("the-token".to_string()),
|
||||
value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
|
||||
@@ -1048,11 +1051,10 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
},
|
||||
)),
|
||||
});
|
||||
executor.advance_clock(SERVER_PROGRESS_DEBOUNCE_TIMEOUT);
|
||||
executor.run_until_parked();
|
||||
|
||||
project_a.read_with(cx_a, |project, _| {
|
||||
let status = project.language_server_statuses().next().unwrap();
|
||||
let status = project.language_server_statuses().next().unwrap().1;
|
||||
assert_eq!(status.name, "the-language-server");
|
||||
assert_eq!(status.pending_work.len(), 1);
|
||||
assert_eq!(
|
||||
@@ -1062,7 +1064,7 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
});
|
||||
|
||||
project_b.read_with(cx_b, |project, _| {
|
||||
let status = project.language_server_statuses().next().unwrap();
|
||||
let status = project.language_server_statuses().next().unwrap().1;
|
||||
assert_eq!(status.name, "the-language-server");
|
||||
assert_eq!(status.pending_work.len(), 1);
|
||||
assert_eq!(
|
||||
@@ -1202,7 +1204,7 @@ async fn test_share_project(
|
||||
buffer_a.read_with(cx_a, |buffer, _| {
|
||||
buffer
|
||||
.snapshot()
|
||||
.remote_selections_in_range(text::Anchor::MIN..text::Anchor::MAX)
|
||||
.selections_in_range(text::Anchor::MIN..text::Anchor::MAX, false)
|
||||
.count()
|
||||
== 1
|
||||
});
|
||||
@@ -1243,7 +1245,7 @@ async fn test_share_project(
|
||||
buffer_a.read_with(cx_a, |buffer, _| {
|
||||
buffer
|
||||
.snapshot()
|
||||
.remote_selections_in_range(text::Anchor::MIN..text::Anchor::MAX)
|
||||
.selections_in_range(text::Anchor::MIN..text::Anchor::MAX, false)
|
||||
.count()
|
||||
== 0
|
||||
});
|
||||
|
||||
@@ -1378,7 +1378,7 @@ async fn test_unshare_project(
|
||||
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
|
||||
executor.run_until_parked();
|
||||
|
||||
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
|
||||
assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer()));
|
||||
|
||||
project_b
|
||||
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
|
||||
@@ -1403,7 +1403,7 @@ async fn test_unshare_project(
|
||||
.unwrap();
|
||||
executor.run_until_parked();
|
||||
|
||||
assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
|
||||
assert!(worktree_a.read_with(cx_a, |tree, _| !tree.has_update_observer()));
|
||||
|
||||
assert!(project_c.read_with(cx_c, |project, _| project.is_disconnected()));
|
||||
|
||||
@@ -1415,7 +1415,7 @@ async fn test_unshare_project(
|
||||
let project_c2 = client_c.build_dev_server_project(project_id, cx_c).await;
|
||||
executor.run_until_parked();
|
||||
|
||||
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
|
||||
assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer()));
|
||||
project_c2
|
||||
.update(cx_c, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
|
||||
.await
|
||||
@@ -1522,7 +1522,7 @@ async fn test_project_reconnect(
|
||||
executor.run_until_parked();
|
||||
|
||||
let worktree1_id = worktree_a1.read_with(cx_a, |worktree, _| {
|
||||
assert!(worktree.as_local().unwrap().is_shared());
|
||||
assert!(worktree.has_update_observer());
|
||||
worktree.id()
|
||||
});
|
||||
let (worktree_a2, _) = project_a1
|
||||
@@ -1534,7 +1534,7 @@ async fn test_project_reconnect(
|
||||
executor.run_until_parked();
|
||||
|
||||
let worktree2_id = worktree_a2.read_with(cx_a, |tree, _| {
|
||||
assert!(tree.as_local().unwrap().is_shared());
|
||||
assert!(tree.has_update_observer());
|
||||
tree.id()
|
||||
});
|
||||
executor.run_until_parked();
|
||||
@@ -1567,9 +1567,7 @@ async fn test_project_reconnect(
|
||||
assert_eq!(project.collaborators().len(), 1);
|
||||
});
|
||||
|
||||
worktree_a1.read_with(cx_a, |tree, _| {
|
||||
assert!(tree.as_local().unwrap().is_shared())
|
||||
});
|
||||
worktree_a1.read_with(cx_a, |tree, _| assert!(tree.has_update_observer()));
|
||||
|
||||
// While client A is disconnected, add and remove files from client A's project.
|
||||
client_a
|
||||
@@ -1611,7 +1609,7 @@ async fn test_project_reconnect(
|
||||
.await;
|
||||
|
||||
let worktree3_id = worktree_a3.read_with(cx_a, |tree, _| {
|
||||
assert!(!tree.as_local().unwrap().is_shared());
|
||||
assert!(!tree.has_update_observer());
|
||||
tree.id()
|
||||
});
|
||||
executor.run_until_parked();
|
||||
@@ -1634,7 +1632,7 @@ async fn test_project_reconnect(
|
||||
|
||||
project_a1.read_with(cx_a, |project, cx| {
|
||||
assert!(project.is_shared());
|
||||
assert!(worktree_a1.read(cx).as_local().unwrap().is_shared());
|
||||
assert!(worktree_a1.read(cx).has_update_observer());
|
||||
assert_eq!(
|
||||
worktree_a1
|
||||
.read(cx)
|
||||
@@ -1652,7 +1650,7 @@ async fn test_project_reconnect(
|
||||
"subdir2/i.txt"
|
||||
]
|
||||
);
|
||||
assert!(worktree_a3.read(cx).as_local().unwrap().is_shared());
|
||||
assert!(worktree_a3.read(cx).has_update_observer());
|
||||
assert_eq!(
|
||||
worktree_a3
|
||||
.read(cx)
|
||||
@@ -1733,7 +1731,7 @@ async fn test_project_reconnect(
|
||||
executor.run_until_parked();
|
||||
|
||||
let worktree4_id = worktree_a4.read_with(cx_a, |tree, _| {
|
||||
assert!(tree.as_local().unwrap().is_shared());
|
||||
assert!(tree.has_update_observer());
|
||||
tree.id()
|
||||
});
|
||||
project_a1.update(cx_a, |project, cx| {
|
||||
@@ -4774,7 +4772,7 @@ async fn test_references(
|
||||
// User is informed that a request is pending.
|
||||
executor.run_until_parked();
|
||||
project_b.read_with(cx_b, |project, _| {
|
||||
let status = project.language_server_statuses().next().cloned().unwrap();
|
||||
let status = project.language_server_statuses().next().unwrap().1;
|
||||
assert_eq!(status.name, "my-fake-lsp-adapter");
|
||||
assert_eq!(
|
||||
status.pending_work.values().next().unwrap().message,
|
||||
@@ -4804,7 +4802,7 @@ async fn test_references(
|
||||
executor.run_until_parked();
|
||||
project_b.read_with(cx_b, |project, cx| {
|
||||
// User is informed that a request is no longer pending.
|
||||
let status = project.language_server_statuses().next().unwrap();
|
||||
let status = project.language_server_statuses().next().unwrap().1;
|
||||
assert!(status.pending_work.is_empty());
|
||||
|
||||
assert_eq!(references.len(), 3);
|
||||
@@ -4832,7 +4830,7 @@ async fn test_references(
|
||||
// User is informed that a request is pending.
|
||||
executor.run_until_parked();
|
||||
project_b.read_with(cx_b, |project, _| {
|
||||
let status = project.language_server_statuses().next().cloned().unwrap();
|
||||
let status = project.language_server_statuses().next().unwrap().1;
|
||||
assert_eq!(status.name, "my-fake-lsp-adapter");
|
||||
assert_eq!(
|
||||
status.pending_work.values().next().unwrap().message,
|
||||
@@ -4849,7 +4847,7 @@ async fn test_references(
|
||||
// User is informed that the request is no longer pending.
|
||||
executor.run_until_parked();
|
||||
project_b.read_with(cx_b, |project, _| {
|
||||
let status = project.language_server_statuses().next().unwrap();
|
||||
let status = project.language_server_statuses().next().unwrap().1;
|
||||
assert!(status.pending_work.is_empty());
|
||||
});
|
||||
}
|
||||
@@ -4906,7 +4904,15 @@ async fn test_project_search(
|
||||
let mut results = HashMap::default();
|
||||
let mut search_rx = project_b.update(cx_b, |project, cx| {
|
||||
project.search(
|
||||
SearchQuery::text("world", false, false, false, Vec::new(), Vec::new()).unwrap(),
|
||||
SearchQuery::text(
|
||||
"world",
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
)
|
||||
.unwrap(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
@@ -14,7 +14,9 @@ use language::{
|
||||
};
|
||||
use lsp::FakeLanguageServer;
|
||||
use pretty_assertions::assert_eq;
|
||||
use project::{search::SearchQuery, Project, ProjectPath, SearchResult};
|
||||
use project::{
|
||||
search::SearchQuery, Project, ProjectPath, SearchResult, DEFAULT_COMPLETION_CONTEXT,
|
||||
};
|
||||
use rand::{
|
||||
distributions::{Alphanumeric, DistString},
|
||||
prelude::*,
|
||||
@@ -303,7 +305,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
.filter(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
worktree.is_visible()
|
||||
&& worktree.entries(false).any(|e| e.is_file())
|
||||
&& worktree.entries(false, 0).any(|e| e.is_file())
|
||||
&& worktree.root_entry().map_or(false, |e| e.is_dir())
|
||||
})
|
||||
.choose(rng)
|
||||
@@ -425,14 +427,14 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
.filter(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
worktree.is_visible()
|
||||
&& worktree.entries(false).any(|e| e.is_file())
|
||||
&& worktree.entries(false, 0).any(|e| e.is_file())
|
||||
})
|
||||
.choose(rng)
|
||||
});
|
||||
let Some(worktree) = worktree else { continue };
|
||||
let full_path = worktree.read_with(cx, |worktree, _| {
|
||||
let entry = worktree
|
||||
.entries(false)
|
||||
.entries(false, 0)
|
||||
.filter(|e| e.is_file())
|
||||
.choose(rng)
|
||||
.unwrap();
|
||||
@@ -829,7 +831,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
.map_ok(|_| ())
|
||||
.boxed(),
|
||||
LspRequestKind::Completion => project
|
||||
.completions(&buffer, offset, cx)
|
||||
.completions(&buffer, offset, DEFAULT_COMPLETION_CONTEXT, cx)
|
||||
.map_ok(|_| ())
|
||||
.boxed(),
|
||||
LspRequestKind::CodeAction => project
|
||||
@@ -873,8 +875,15 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
|
||||
let mut search = project.update(cx, |project, cx| {
|
||||
project.search(
|
||||
SearchQuery::text(query, false, false, false, Vec::new(), Vec::new())
|
||||
.unwrap(),
|
||||
SearchQuery::text(
|
||||
query,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
)
|
||||
.unwrap(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -1204,8 +1213,8 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
guest_project.remote_id(),
|
||||
);
|
||||
assert_eq!(
|
||||
guest_snapshot.entries(false).collect::<Vec<_>>(),
|
||||
host_snapshot.entries(false).collect::<Vec<_>>(),
|
||||
guest_snapshot.entries(false, 0).collect::<Vec<_>>(),
|
||||
host_snapshot.entries(false, 0).collect::<Vec<_>>(),
|
||||
"{} has different snapshot than the host for worktree {:?} ({:?}) and project {:?}",
|
||||
client.username,
|
||||
host_snapshot.abs_path(),
|
||||
|
||||
@@ -161,7 +161,7 @@ impl TestServer {
|
||||
}
|
||||
let settings = SettingsStore::test(cx);
|
||||
cx.set_global(settings);
|
||||
release_channel::init("0.0.0", cx);
|
||||
release_channel::init(SemanticVersion::default(), cx);
|
||||
client::init_settings(cx);
|
||||
});
|
||||
|
||||
@@ -277,11 +277,7 @@ impl TestServer {
|
||||
node_runtime: FakeNodeRuntime::new(),
|
||||
});
|
||||
|
||||
let os_keymap = if cfg!(target_os = "linux") {
|
||||
"keymaps/default-linux.json"
|
||||
} else {
|
||||
"keymaps/default-macos.json"
|
||||
};
|
||||
let os_keymap = "keymaps/default-macos.json";
|
||||
|
||||
cx.update(|cx| {
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
@@ -327,7 +323,7 @@ impl TestServer {
|
||||
}
|
||||
let settings = SettingsStore::test(cx);
|
||||
cx.set_global(settings);
|
||||
release_channel::init("0.0.0", cx);
|
||||
release_channel::init(SemanticVersion::default(), cx);
|
||||
client::init_settings(cx);
|
||||
});
|
||||
let (dev_server_id, _) = split_dev_server_token(&access_token).unwrap();
|
||||
|
||||
@@ -35,10 +35,12 @@ call.workspace = true
|
||||
channel.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
emojis.workspace = true
|
||||
extensions_ui.workspace = true
|
||||
feedback.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
|
||||
@@ -228,11 +228,11 @@ impl ChannelView {
|
||||
&self.editor,
|
||||
move |this, _, e: &EditorEvent, cx| {
|
||||
match e {
|
||||
EditorEvent::Reparsed => {
|
||||
EditorEvent::Reparsed(_) => {
|
||||
this.focus_position_from_link(position.clone(), false, cx);
|
||||
this._reparse_subscription.take();
|
||||
}
|
||||
EditorEvent::Edited | EditorEvent::SelectionsChanged { local: true } => {
|
||||
EditorEvent::Edited { .. } | EditorEvent::SelectionsChanged { local: true } => {
|
||||
this._reparse_subscription.take();
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -22,7 +22,7 @@ use settings::Settings;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
use ui::{
|
||||
popover_menu, prelude::*, Avatar, Button, ContextMenu, IconButton, IconName, KeyBinding, Label,
|
||||
prelude::*, Avatar, Button, ContextMenu, IconButton, IconName, KeyBinding, Label, PopoverMenu,
|
||||
TabBar, Tooltip,
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
@@ -679,7 +679,7 @@ impl ChatPanel {
|
||||
cx,
|
||||
div()
|
||||
.child(
|
||||
popover_menu(("menu", message_id))
|
||||
PopoverMenu::new(("menu", message_id))
|
||||
.trigger(IconButton::new(
|
||||
("trigger", message_id),
|
||||
IconName::Ellipsis,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context, Result};
|
||||
use channel::{ChannelChat, ChannelStore, MessageParams};
|
||||
use client::{UserId, UserStore};
|
||||
use collections::HashSet;
|
||||
@@ -25,8 +25,15 @@ use crate::panel_settings::MessageEditorSettings;
|
||||
const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
|
||||
|
||||
lazy_static! {
|
||||
static ref MENTIONS_SEARCH: SearchQuery =
|
||||
SearchQuery::regex("@[-_\\w]+", false, false, false, Vec::new(), Vec::new()).unwrap();
|
||||
static ref MENTIONS_SEARCH: SearchQuery = SearchQuery::regex(
|
||||
"@[-_\\w]+",
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
Default::default(),
|
||||
Default::default()
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub struct MessageEditor {
|
||||
@@ -46,6 +53,7 @@ impl CompletionProvider for MessageEditorCompletionProvider {
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
buffer_position: language::Anchor,
|
||||
_: editor::CompletionContext,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Task<anyhow::Result<Vec<Completion>>> {
|
||||
let Some(handle) = self.0.upgrade() else {
|
||||
@@ -132,7 +140,7 @@ impl MessageEditor {
|
||||
|
||||
let markdown = language_registry.language_for_name("Markdown");
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let markdown = markdown.await?;
|
||||
let markdown = markdown.await.context("failed to load Markdown language")?;
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.set_language(Some(markdown), cx)
|
||||
})
|
||||
|
||||
@@ -10,11 +10,12 @@ use gpui::{
|
||||
use project::{Project, RepositoryEntry};
|
||||
use recent_projects::RecentProjects;
|
||||
use rpc::proto::{self, DevServerStatus};
|
||||
use settings::Settings;
|
||||
use std::sync::Arc;
|
||||
use theme::ActiveTheme;
|
||||
use theme::{ActiveTheme, ThemeSettings};
|
||||
use ui::{
|
||||
h_flex, popover_menu, prelude::*, Avatar, AvatarAudioStatusIndicator, Button, ButtonLike,
|
||||
ButtonStyle, ContextMenu, Icon, IconButton, IconName, Indicator, TintColor, TitleBar, Tooltip,
|
||||
h_flex, prelude::*, Avatar, AvatarAudioStatusIndicator, Button, ButtonLike, ButtonStyle,
|
||||
ContextMenu, Icon, IconButton, IconName, Indicator, PopoverMenu, TintColor, TitleBar, Tooltip,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use vcs_menu::{BranchList, OpenRecent as ToggleVcsMenu};
|
||||
@@ -58,6 +59,8 @@ impl Render for CollabTitlebarItem {
|
||||
let project_id = self.project.read(cx).remote_id();
|
||||
let workspace = self.workspace.upgrade();
|
||||
|
||||
let platform_supported = cfg!(target_os = "macos");
|
||||
|
||||
TitleBar::new("collab-titlebar", Box::new(workspace::CloseWindow))
|
||||
// note: on windows titlebar behaviour is handled by the platform implementation
|
||||
.when(cfg!(not(windows)), |this| {
|
||||
@@ -71,6 +74,7 @@ impl Render for CollabTitlebarItem {
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.children(self.render_application_menu(cx))
|
||||
.children(self.render_project_host(cx))
|
||||
.child(self.render_project_name(cx))
|
||||
.children(self.render_project_branch(cx))
|
||||
@@ -243,7 +247,9 @@ impl Render for CollabTitlebarItem {
|
||||
)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::text(
|
||||
if is_muted {
|
||||
if !platform_supported {
|
||||
"Cannot share microphone"
|
||||
} else if is_muted {
|
||||
"Unmute microphone"
|
||||
} else {
|
||||
"Mute microphone"
|
||||
@@ -253,7 +259,8 @@ impl Render for CollabTitlebarItem {
|
||||
})
|
||||
.style(ButtonStyle::Subtle)
|
||||
.icon_size(IconSize::Small)
|
||||
.selected(is_muted)
|
||||
.selected(platform_supported && is_muted)
|
||||
.disabled(!platform_supported)
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Negative))
|
||||
.on_click(move |_, cx| crate::toggle_mute(&Default::default(), cx)),
|
||||
)
|
||||
@@ -271,8 +278,11 @@ impl Render for CollabTitlebarItem {
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Negative))
|
||||
.icon_size(IconSize::Small)
|
||||
.selected(is_deafened)
|
||||
.disabled(!platform_supported)
|
||||
.tooltip(move |cx| {
|
||||
if can_use_microphone {
|
||||
if !platform_supported {
|
||||
Tooltip::text("Cannot share microphone", cx)
|
||||
} else if can_use_microphone {
|
||||
Tooltip::with_meta(
|
||||
"Deafen Audio",
|
||||
None,
|
||||
@@ -291,10 +301,13 @@ impl Render for CollabTitlebarItem {
|
||||
.style(ButtonStyle::Subtle)
|
||||
.icon_size(IconSize::Small)
|
||||
.selected(is_screen_sharing)
|
||||
.disabled(!platform_supported)
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::text(
|
||||
if is_screen_sharing {
|
||||
if !platform_supported {
|
||||
"Cannot share screen"
|
||||
} else if is_screen_sharing {
|
||||
"Stop Sharing Screen"
|
||||
} else {
|
||||
"Share Screen"
|
||||
@@ -375,8 +388,173 @@ impl CollabTitlebarItem {
|
||||
}
|
||||
}
|
||||
|
||||
// resolve if you are in a room -> render_project_owner
|
||||
// render_project_owner -> resolve if you are in a room -> Option<foo>
|
||||
pub fn render_application_menu(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
|
||||
cfg!(not(target_os = "macos")).then(|| {
|
||||
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size;
|
||||
let font = cx.text_style().font();
|
||||
let font_id = cx.text_system().resolve_font(&font);
|
||||
let width = cx
|
||||
.text_system()
|
||||
.typographic_bounds(font_id, ui_font_size, 'm')
|
||||
.unwrap()
|
||||
.size
|
||||
.width
|
||||
* 3.0;
|
||||
|
||||
PopoverMenu::new("application-menu")
|
||||
.menu(move |cx| {
|
||||
let width = width;
|
||||
ContextMenu::build(cx, move |menu, _cx| {
|
||||
let width = width;
|
||||
menu.header("Workspace")
|
||||
.action("Open Command Palette", Box::new(command_palette::Toggle))
|
||||
.custom_row(move |cx| {
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.justify_between()
|
||||
.cursor(gpui::CursorStyle::Arrow)
|
||||
.child(Label::new("Buffer Font Size"))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.child(div().w(px(16.0)))
|
||||
.child(
|
||||
IconButton::new(
|
||||
"reset-buffer-zoom",
|
||||
IconName::RotateCcw,
|
||||
)
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(
|
||||
zed_actions::ResetBufferFontSize,
|
||||
))
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("--buffer-zoom", IconName::Dash)
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(
|
||||
zed_actions::DecreaseBufferFontSize,
|
||||
))
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w(width)
|
||||
.flex()
|
||||
.flex_row()
|
||||
.justify_around()
|
||||
.child(Label::new(
|
||||
theme::get_buffer_font_size(cx).to_string(),
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("+-buffer-zoom", IconName::Plus)
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(
|
||||
zed_actions::IncreaseBufferFontSize,
|
||||
))
|
||||
}),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
.custom_row(move |cx| {
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.justify_between()
|
||||
.cursor(gpui::CursorStyle::Arrow)
|
||||
.child(Label::new("UI Font Size"))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.child(
|
||||
IconButton::new(
|
||||
"reset-ui-zoom",
|
||||
IconName::RotateCcw,
|
||||
)
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(
|
||||
zed_actions::ResetUiFontSize,
|
||||
))
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("--ui-zoom", IconName::Dash)
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(
|
||||
zed_actions::DecreaseUiFontSize,
|
||||
))
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w(width)
|
||||
.flex()
|
||||
.flex_row()
|
||||
.justify_around()
|
||||
.child(Label::new(
|
||||
theme::get_ui_font_size(cx).to_string(),
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("+-ui-zoom", IconName::Plus)
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(
|
||||
zed_actions::IncreaseUiFontSize,
|
||||
))
|
||||
}),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
.header("Project")
|
||||
.action(
|
||||
"Add Folder to Project...",
|
||||
Box::new(workspace::AddFolderToProject),
|
||||
)
|
||||
.action("Open a new Project...", Box::new(workspace::Open))
|
||||
.action(
|
||||
"Open Recent Projects...",
|
||||
Box::new(recent_projects::OpenRecent {
|
||||
create_new_window: false,
|
||||
}),
|
||||
)
|
||||
.header("Help")
|
||||
.action("About Zed", Box::new(zed_actions::About))
|
||||
.action("Welcome", Box::new(workspace::Welcome))
|
||||
.link(
|
||||
"Documentation",
|
||||
Box::new(zed_actions::OpenBrowser {
|
||||
url: "https://zed.dev/docs".into(),
|
||||
}),
|
||||
)
|
||||
.action("Give Feedback", Box::new(feedback::GiveFeedback))
|
||||
.action("Check for Updates", Box::new(auto_update::Check))
|
||||
.action("View Telemetry", Box::new(zed_actions::OpenTelemetryLog))
|
||||
.action(
|
||||
"View Dependency Licenses",
|
||||
Box::new(zed_actions::OpenLicenses),
|
||||
)
|
||||
.separator()
|
||||
.action("Quit", Box::new(zed_actions::Quit))
|
||||
})
|
||||
.into()
|
||||
})
|
||||
.trigger(
|
||||
IconButton::new("application-menu", ui::IconName::Menu)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(|cx| Tooltip::text("Open Application Menu", cx))
|
||||
.icon_size(IconSize::Small),
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn render_project_host(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
|
||||
if let Some(dev_server) =
|
||||
@@ -413,6 +591,17 @@ impl CollabTitlebarItem {
|
||||
);
|
||||
}
|
||||
|
||||
if self.project.read(cx).is_disconnected() {
|
||||
return Some(
|
||||
Button::new("disconnected", "Disconnected")
|
||||
.disabled(true)
|
||||
.color(Color::Disabled)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.label_size(LabelSize::Small)
|
||||
.into_any_element(),
|
||||
);
|
||||
}
|
||||
|
||||
let host = self.project.read(cx).host()?;
|
||||
let host_user = self.user_store.read(cx).get_cached_user(host.user_id)?;
|
||||
let participant_index = self
|
||||
@@ -686,7 +875,7 @@ impl CollabTitlebarItem {
|
||||
.on_click(|_, cx| {
|
||||
if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
|
||||
if auto_updater.read(cx).status().is_updated() {
|
||||
workspace::restart(&Default::default(), cx);
|
||||
workspace::reload(&Default::default(), cx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -717,12 +906,13 @@ impl CollabTitlebarItem {
|
||||
|
||||
pub fn render_user_menu_button(&mut self, cx: &mut ViewContext<Self>) -> impl Element {
|
||||
if let Some(user) = self.user_store.read(cx).current_user() {
|
||||
popover_menu("user-menu")
|
||||
PopoverMenu::new("user-menu")
|
||||
.menu(|cx| {
|
||||
ContextMenu::build(cx, |menu, _| {
|
||||
menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
|
||||
.action("Extensions", extensions_ui::Extensions.boxed_clone())
|
||||
.action("Key Bindings", Box::new(zed_actions::OpenKeymap))
|
||||
.action("Themes…", theme_selector::Toggle::default().boxed_clone())
|
||||
.action("Extensions", extensions_ui::Extensions.boxed_clone())
|
||||
.separator()
|
||||
.action("Sign Out", client::SignOut.boxed_clone())
|
||||
})
|
||||
@@ -745,12 +935,13 @@ impl CollabTitlebarItem {
|
||||
)
|
||||
.anchor(gpui::AnchorCorner::TopRight)
|
||||
} else {
|
||||
popover_menu("user-menu")
|
||||
PopoverMenu::new("user-menu")
|
||||
.menu(|cx| {
|
||||
ContextMenu::build(cx, |menu, _| {
|
||||
menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
|
||||
.action("Extensions", extensions_ui::Extensions.boxed_clone())
|
||||
.action("Key Bindings", Box::new(zed_actions::OpenKeymap))
|
||||
.action("Themes…", theme_selector::Toggle::default().boxed_clone())
|
||||
.action("Extensions", extensions_ui::Extensions.boxed_clone())
|
||||
})
|
||||
.into()
|
||||
})
|
||||
|
||||
@@ -13,8 +13,8 @@ use call::{report_call_event_for_room, ActiveCall};
|
||||
pub use collab_panel::CollabPanel;
|
||||
pub use collab_titlebar_item::CollabTitlebarItem;
|
||||
use gpui::{
|
||||
actions, point, AppContext, DevicePixels, Pixels, PlatformDisplay, Size, Task,
|
||||
WindowBackgroundAppearance, WindowBounds, WindowContext, WindowKind, WindowOptions,
|
||||
actions, point, AppContext, Pixels, PlatformDisplay, Size, Task, WindowBackgroundAppearance,
|
||||
WindowBounds, WindowContext, WindowKind, WindowOptions,
|
||||
};
|
||||
use panel_settings::MessageEditorSettings;
|
||||
pub use panel_settings::{
|
||||
@@ -22,6 +22,7 @@ pub use panel_settings::{
|
||||
};
|
||||
use release_channel::ReleaseChannel;
|
||||
use settings::Settings;
|
||||
use ui::px;
|
||||
use workspace::{notifications::DetachAndPromptErr, AppState};
|
||||
|
||||
actions!(
|
||||
@@ -96,22 +97,19 @@ pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
|
||||
|
||||
fn notification_window_options(
|
||||
screen: Rc<dyn PlatformDisplay>,
|
||||
window_size: Size<Pixels>,
|
||||
size: Size<Pixels>,
|
||||
cx: &AppContext,
|
||||
) -> WindowOptions {
|
||||
let notification_margin_width = DevicePixels::from(16);
|
||||
let notification_margin_height = DevicePixels::from(-0) - DevicePixels::from(48);
|
||||
let notification_margin_width = px(16.);
|
||||
let notification_margin_height = px(-48.);
|
||||
|
||||
let screen_bounds = screen.bounds();
|
||||
let size: Size<DevicePixels> = window_size.into();
|
||||
|
||||
let bounds = gpui::Bounds::<DevicePixels> {
|
||||
origin: screen_bounds.upper_right()
|
||||
let bounds = gpui::Bounds::<Pixels> {
|
||||
origin: screen.bounds().upper_right()
|
||||
- point(
|
||||
size.width + notification_margin_width,
|
||||
notification_margin_height,
|
||||
),
|
||||
size: window_size.into(),
|
||||
size,
|
||||
};
|
||||
|
||||
let app_id = ReleaseChannel::global(cx).app_id();
|
||||
@@ -126,5 +124,6 @@ fn notification_window_options(
|
||||
display_id: Some(screen.id()),
|
||||
window_background: WindowBackgroundAppearance::default(),
|
||||
app_id: Some(app_id.to_owned()),
|
||||
window_min_size: Size::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,8 @@ use crate::notifications::collab_notification::CollabNotification;
|
||||
use call::{ActiveCall, IncomingCall};
|
||||
use futures::StreamExt;
|
||||
use gpui::{prelude::*, AppContext, WindowHandle};
|
||||
use settings::Settings;
|
||||
|
||||
use std::sync::{Arc, Weak};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, Button, Label};
|
||||
use util::ResultExt;
|
||||
use workspace::AppState;
|
||||
@@ -113,13 +112,7 @@ impl IncomingCallNotification {
|
||||
|
||||
impl Render for IncomingCallNotification {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
// TODO: Is there a better place for us to initialize the font?
|
||||
let (ui_font, ui_font_size) = {
|
||||
let theme_settings = ThemeSettings::get_global(cx);
|
||||
(theme_settings.ui_font.clone(), theme_settings.ui_font_size)
|
||||
};
|
||||
|
||||
cx.set_rem_size(ui_font_size);
|
||||
let ui_font = theme::setup_ui_font(cx);
|
||||
|
||||
div().size_full().font(ui_font).child(
|
||||
CollabNotification::new(
|
||||
|
||||
@@ -4,10 +4,10 @@ use call::{room, ActiveCall};
|
||||
use client::User;
|
||||
use collections::HashMap;
|
||||
use gpui::{AppContext, Size};
|
||||
use settings::Settings;
|
||||
use std::sync::{Arc, Weak};
|
||||
use theme::ThemeSettings;
|
||||
|
||||
use ui::{prelude::*, Button, Label};
|
||||
use util::ResultExt;
|
||||
use workspace::AppState;
|
||||
|
||||
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
@@ -27,16 +27,21 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
|
||||
for screen in cx.displays() {
|
||||
let options = notification_window_options(screen, window_size, cx);
|
||||
let window = cx.open_window(options, |cx| {
|
||||
cx.new_view(|_| {
|
||||
ProjectSharedNotification::new(
|
||||
owner.clone(),
|
||||
*project_id,
|
||||
worktree_root_names.clone(),
|
||||
app_state.clone(),
|
||||
)
|
||||
let Some(window) = cx
|
||||
.open_window(options, |cx| {
|
||||
cx.new_view(|_| {
|
||||
ProjectSharedNotification::new(
|
||||
owner.clone(),
|
||||
*project_id,
|
||||
worktree_root_names.clone(),
|
||||
app_state.clone(),
|
||||
)
|
||||
})
|
||||
})
|
||||
});
|
||||
.log_err()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
notification_windows
|
||||
.entry(*project_id)
|
||||
.or_insert(Vec::new())
|
||||
@@ -118,13 +123,7 @@ impl ProjectSharedNotification {
|
||||
|
||||
impl Render for ProjectSharedNotification {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
// TODO: Is there a better place for us to initialize the font?
|
||||
let (ui_font, ui_font_size) = {
|
||||
let theme_settings = ThemeSettings::get_global(cx);
|
||||
(theme_settings.ui_font.clone(), theme_settings.ui_font_size)
|
||||
};
|
||||
|
||||
cx.set_rem_size(ui_font_size);
|
||||
let ui_font = theme::setup_ui_font(cx);
|
||||
|
||||
div().size_full().font(ui_font).child(
|
||||
CollabNotification::new(
|
||||
|
||||
@@ -1,227 +0,0 @@
|
||||
//! # Color
|
||||
//!
|
||||
//! The `color` crate provides a set utilities for working with colors. It is a wrapper around the [`palette`](https://docs.rs/palette) crate with some additional functionality.
|
||||
//!
|
||||
//! It is used to create a manipulate colors when building themes.
|
||||
//!
|
||||
//! === In development note ===
|
||||
//!
|
||||
//! This crate is meant to sit between gpui and the theme/ui for all the color related stuff.
|
||||
//!
|
||||
//! It could be folded into gpui, ui or theme potentially but for now we'll continue
|
||||
//! to develop it in isolation.
|
||||
//!
|
||||
//! Once we have a good idea of the needs of the theme system and color in gpui in general I see 3 paths:
|
||||
//! 1. Use `palette` (or another color library) directly in gpui and everywhere else, rather than rolling our own color system.
|
||||
//! 2. Keep this crate as a thin wrapper around `palette` and use it everywhere except gpui, and convert to gpui's color system when needed.
|
||||
//! 3. Build the needed functionality into gpui and keep using its color system everywhere.
|
||||
//!
|
||||
//! I'm leaning towards 2 in the short term and 1 in the long term, but we'll need to discuss it more.
|
||||
//!
|
||||
//! === End development note ===
|
||||
use palette::{
|
||||
blend::Blend, convert::FromColorUnclamped, encoding, rgb::Rgb, Clamp, Mix, Srgb, WithAlpha,
|
||||
};
|
||||
|
||||
/// The types of blend modes supported
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum BlendMode {
|
||||
/// Multiplies the colors, resulting in a darker color. This mode is useful for creating shadows.
|
||||
Multiply,
|
||||
/// Lightens the color by adding the source and destination colors. It results in a lighter color.
|
||||
Screen,
|
||||
/// Combines Multiply and Screen blend modes. Parts of the image that are lighter than 50% gray are lightened, and parts that are darker are darkened.
|
||||
Overlay,
|
||||
/// Selects the darker of the base or blend color as the resulting color. Useful for darkening images without affecting the overall contrast.
|
||||
Darken,
|
||||
/// Selects the lighter of the base or blend color as the resulting color. Useful for lightening images without affecting the overall contrast.
|
||||
Lighten,
|
||||
/// Brightens the base color to reflect the blend color. The result is a lightened image.
|
||||
Dodge,
|
||||
/// Darkens the base color to reflect the blend color. The result is a darkened image.
|
||||
Burn,
|
||||
/// Similar to Overlay, but with a stronger effect. Hard Light can either multiply or screen colors, depending on the blend color.
|
||||
HardLight,
|
||||
/// A softer version of Hard Light. Soft Light either darkens or lightens colors, depending on the blend color.
|
||||
SoftLight,
|
||||
/// Subtracts the darker of the two constituent colors from the lighter color. Difference mode is useful for creating more vivid colors.
|
||||
Difference,
|
||||
/// Similar to Difference, but with a lower contrast. Exclusion mode produces an effect similar to Difference but with less intensity.
|
||||
Exclusion,
|
||||
}
|
||||
|
||||
/// Converts a hexadecimal color string to a `palette::Hsla` color.
|
||||
///
|
||||
/// This function supports the following hex formats:
|
||||
/// `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`.
|
||||
pub fn hex_to_hsla(s: &str) -> Result<RGBAColor, String> {
|
||||
let hex = s.trim_start_matches('#');
|
||||
|
||||
// Expand shorthand formats #RGB and #RGBA to #RRGGBB and #RRGGBBAA
|
||||
let h = hex.as_bytes();
|
||||
let arr: [u8; 8] = match h.len() {
|
||||
// #RGB => #RRGGBBAA
|
||||
3 => [h[0], h[0], h[1], h[1], h[2], h[2], b'f', b'f'],
|
||||
// #RGBA => #RRGGBBAA
|
||||
4 => [h[0], h[0], h[1], h[1], h[2], h[2], h[3], h[3]],
|
||||
// #RRGGBB => #RRGGBBAA
|
||||
6 => [h[0], h[1], h[2], h[3], h[4], h[5], b'f', b'f'],
|
||||
// Already in #RRGGBBAA
|
||||
8 => h.try_into().unwrap(),
|
||||
_ => return Err("Invalid hexadecimal string length".to_string()),
|
||||
};
|
||||
|
||||
let hex =
|
||||
std::str::from_utf8(&arr).map_err(|_| format!("Invalid hexadecimal string: {}", s))?;
|
||||
let hex_val =
|
||||
u32::from_str_radix(hex, 16).map_err(|_| format!("Invalid hexadecimal string: {}", s))?;
|
||||
|
||||
Ok(RGBAColor {
|
||||
r: ((hex_val >> 24) & 0xFF) as f32 / 255.0,
|
||||
g: ((hex_val >> 16) & 0xFF) as f32 / 255.0,
|
||||
b: ((hex_val >> 8) & 0xFF) as f32 / 255.0,
|
||||
a: (hex_val & 0xFF) as f32 / 255.0,
|
||||
})
|
||||
}
|
||||
|
||||
// These derives implement to and from palette's color types.
|
||||
#[derive(FromColorUnclamped, WithAlpha, Debug, Clone)]
|
||||
#[palette(skip_derives(Rgb), rgb_standard = "encoding::Srgb")]
|
||||
pub struct RGBAColor {
|
||||
r: f32,
|
||||
g: f32,
|
||||
b: f32,
|
||||
// Let Palette know this is our alpha channel.
|
||||
#[palette(alpha)]
|
||||
a: f32,
|
||||
}
|
||||
|
||||
impl FromColorUnclamped<RGBAColor> for RGBAColor {
|
||||
fn from_color_unclamped(color: RGBAColor) -> RGBAColor {
|
||||
color
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> FromColorUnclamped<Rgb<S, f32>> for RGBAColor
|
||||
where
|
||||
Srgb: FromColorUnclamped<Rgb<S, f32>>,
|
||||
{
|
||||
fn from_color_unclamped(color: Rgb<S, f32>) -> RGBAColor {
|
||||
let srgb = Srgb::from_color_unclamped(color);
|
||||
RGBAColor {
|
||||
r: srgb.red,
|
||||
g: srgb.green,
|
||||
b: srgb.blue,
|
||||
a: 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> FromColorUnclamped<RGBAColor> for Rgb<S, f32>
|
||||
where
|
||||
Rgb<S, f32>: FromColorUnclamped<Srgb>,
|
||||
{
|
||||
fn from_color_unclamped(color: RGBAColor) -> Self {
|
||||
Self::from_color_unclamped(Srgb::new(color.r, color.g, color.b))
|
||||
}
|
||||
}
|
||||
|
||||
impl Clamp for RGBAColor {
|
||||
fn clamp(self) -> Self {
|
||||
RGBAColor {
|
||||
r: self.r.clamp(0., 1.),
|
||||
g: self.g.clamp(0., 1.),
|
||||
b: self.b.clamp(0., 1.),
|
||||
a: self.a.clamp(0., 1.),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RGBAColor {
|
||||
/// Creates a new color from the given RGBA values.
|
||||
///
|
||||
/// This color can be used to convert to any [`palette::Color`] type.
|
||||
pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
|
||||
RGBAColor { r, g, b, a }
|
||||
}
|
||||
|
||||
/// Returns a set of states for this color.
|
||||
pub fn states(self, is_light: bool) -> ColorStates {
|
||||
states_for_color(self, is_light)
|
||||
}
|
||||
|
||||
/// Mixes this color with another [`palette::Hsl`] color at the given `mix_ratio`.
|
||||
pub fn mixed(&self, other: RGBAColor, mix_ratio: f32) -> Self {
|
||||
let srgb_self = Srgb::new(self.r, self.g, self.b);
|
||||
let srgb_other = Srgb::new(other.r, other.g, other.b);
|
||||
|
||||
// Directly mix the colors as sRGB values
|
||||
let mixed = srgb_self.mix(srgb_other, mix_ratio);
|
||||
RGBAColor::from_color_unclamped(mixed)
|
||||
}
|
||||
|
||||
pub fn blend(&self, other: RGBAColor, blend_mode: BlendMode) -> Self {
|
||||
let srgb_self = Srgb::new(self.r, self.g, self.b);
|
||||
let srgb_other = Srgb::new(other.r, other.g, other.b);
|
||||
|
||||
let blended = match blend_mode {
|
||||
// replace hsl methods with the respective sRGB methods
|
||||
BlendMode::Multiply => srgb_self.multiply(srgb_other),
|
||||
_ => unimplemented!(),
|
||||
};
|
||||
|
||||
Self {
|
||||
r: blended.red,
|
||||
g: blended.green,
|
||||
b: blended.blue,
|
||||
a: self.a,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A set of colors for different states of an element.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ColorStates {
|
||||
/// The default color.
|
||||
pub default: RGBAColor,
|
||||
/// The color when the mouse is hovering over the element.
|
||||
pub hover: RGBAColor,
|
||||
/// The color when the mouse button is held down on the element.
|
||||
pub active: RGBAColor,
|
||||
/// The color when the element is focused with the keyboard.
|
||||
pub focused: RGBAColor,
|
||||
/// The color when the element is disabled.
|
||||
pub disabled: RGBAColor,
|
||||
}
|
||||
|
||||
/// Returns a set of colors for different states of an element.
|
||||
///
|
||||
/// todo("This should take a theme and use appropriate colors from it")
|
||||
pub fn states_for_color(color: RGBAColor, is_light: bool) -> ColorStates {
|
||||
let adjustment_factor = if is_light { 0.1 } else { -0.1 };
|
||||
let hover_adjustment = 1.0 - adjustment_factor;
|
||||
let active_adjustment = 1.0 - 2.0 * adjustment_factor;
|
||||
let focused_adjustment = 1.0 - 3.0 * adjustment_factor;
|
||||
let disabled_adjustment = 1.0 - 4.0 * adjustment_factor;
|
||||
|
||||
let make_adjustment = |color: RGBAColor, adjustment: f32| -> RGBAColor {
|
||||
// Adjust lightness for each state
|
||||
// Note: Adjustment logic may differ; simplify as needed for sRGB
|
||||
RGBAColor::new(
|
||||
color.r * adjustment,
|
||||
color.g * adjustment,
|
||||
color.b * adjustment,
|
||||
color.a,
|
||||
)
|
||||
};
|
||||
|
||||
let color = color.clamp();
|
||||
|
||||
ColorStates {
|
||||
default: color.clone(),
|
||||
hover: make_adjustment(color.clone(), hover_adjustment),
|
||||
active: make_adjustment(color.clone(), active_adjustment),
|
||||
focused: make_adjustment(color.clone(), focused_adjustment),
|
||||
disabled: make_adjustment(color.clone(), disabled_adjustment),
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ lsp.workspace = true
|
||||
menu.workspace = true
|
||||
node_runtime.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
project.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
|
||||
@@ -33,7 +33,7 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use util::{fs::remove_matching, maybe, paths, ResultExt};
|
||||
use util::{fs::remove_matching, maybe, ResultExt};
|
||||
|
||||
pub use copilot_completion_provider::CopilotCompletionProvider;
|
||||
pub use sign_in::CopilotCodeVerification;
|
||||
@@ -968,7 +968,7 @@ fn uri_for_buffer(buffer: &Model<Buffer>, cx: &AppContext) -> lsp::Url {
|
||||
}
|
||||
|
||||
async fn clear_copilot_dir() {
|
||||
remove_matching(&paths::COPILOT_DIR, |_| true).await
|
||||
remove_matching(paths::copilot_dir(), |_| true).await
|
||||
}
|
||||
|
||||
async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
|
||||
@@ -979,7 +979,7 @@ async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
|
||||
let release =
|
||||
latest_github_release("zed-industries/copilot", true, false, http.clone()).await?;
|
||||
|
||||
let version_dir = &*paths::COPILOT_DIR.join(format!("copilot-{}", release.tag_name));
|
||||
let version_dir = &paths::copilot_dir().join(format!("copilot-{}", release.tag_name));
|
||||
|
||||
fs::create_dir_all(version_dir).await?;
|
||||
let server_path = version_dir.join(SERVER_PATH);
|
||||
@@ -1003,7 +1003,7 @@ async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
|
||||
let archive = Archive::new(decompressed_bytes);
|
||||
archive.unpack(dist_dir).await?;
|
||||
|
||||
remove_matching(&paths::COPILOT_DIR, |entry| entry != version_dir).await;
|
||||
remove_matching(paths::copilot_dir(), |entry| entry != version_dir).await;
|
||||
}
|
||||
|
||||
Ok(server_path)
|
||||
@@ -1016,7 +1016,7 @@ async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
|
||||
// Fetch a cached binary, if it exists
|
||||
maybe!(async {
|
||||
let mut last_version_dir = None;
|
||||
let mut entries = fs::read_dir(paths::COPILOT_DIR.as_path()).await?;
|
||||
let mut entries = fs::read_dir(paths::copilot_dir()).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
let entry = entry?;
|
||||
if entry.file_type().await?.is_dir() {
|
||||
@@ -1044,7 +1044,6 @@ async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use language::BufferId;
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_buffer_management(cx: &mut TestAppContext) {
|
||||
@@ -1258,16 +1257,5 @@ mod tests {
|
||||
fn load(&self, _: &AppContext) -> Task<Result<String>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn buffer_reloaded(
|
||||
&self,
|
||||
_: BufferId,
|
||||
_: &clock::Global,
|
||||
_: language::LineEnding,
|
||||
_: Option<std::time::SystemTime>,
|
||||
_: &mut AppContext,
|
||||
) {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ gpui.workspace = true
|
||||
indoc.workspace = true
|
||||
lazy_static.workspace = true
|
||||
log.workspace = true
|
||||
paths.workspace = true
|
||||
release_channel.workspace = true
|
||||
smol.workspace = true
|
||||
sqlez.workspace = true
|
||||
|
||||
@@ -7,10 +7,10 @@ use anyhow::Context;
|
||||
use gpui::AppContext;
|
||||
pub use indoc::indoc;
|
||||
pub use lazy_static;
|
||||
pub use paths::database_dir;
|
||||
pub use smol;
|
||||
pub use sqlez;
|
||||
pub use sqlez_macros;
|
||||
pub use util::paths::DB_DIR;
|
||||
|
||||
use release_channel::ReleaseChannel;
|
||||
pub use release_channel::RELEASE_CHANNEL;
|
||||
@@ -145,7 +145,7 @@ macro_rules! define_connection {
|
||||
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
$crate::lazy_static::lazy_static! {
|
||||
pub static ref $id: $t = $t($crate::smol::block_on($crate::open_db(&$crate::DB_DIR, &$crate::RELEASE_CHANNEL)));
|
||||
pub static ref $id: $t = $t($crate::smol::block_on($crate::open_db($crate::database_dir(), &$crate::RELEASE_CHANNEL)));
|
||||
}
|
||||
};
|
||||
(pub static ref $id:ident: $t:ident<$($d:ty),+> = $migrations:expr;) => {
|
||||
@@ -176,7 +176,7 @@ macro_rules! define_connection {
|
||||
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
$crate::lazy_static::lazy_static! {
|
||||
pub static ref $id: $t = $t($crate::smol::block_on($crate::open_db(&$crate::DB_DIR, &$crate::RELEASE_CHANNEL)));
|
||||
pub static ref $id: $t = $t($crate::smol::block_on($crate::open_db($crate::database_dir(), &$crate::RELEASE_CHANNEL)));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user