Compare commits
251 Commits
quick-comm
...
drop-image
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1cb301a827 | ||
|
|
859092d722 | ||
|
|
c34320c2ad | ||
|
|
efe795242f | ||
|
|
04398619f7 | ||
|
|
773a3b335e | ||
|
|
b5c38e9a09 | ||
|
|
273173ec8a | ||
|
|
4084ba36f9 | ||
|
|
770886880f | ||
|
|
ea44c510a3 | ||
|
|
c8f1969916 | ||
|
|
a960344301 | ||
|
|
af9e7f1f96 | ||
|
|
c04c439d23 | ||
|
|
d3cd8f8f14 | ||
|
|
cd8d776fe1 | ||
|
|
6de2330253 | ||
|
|
0e264b5a68 | ||
|
|
1af5304074 | ||
|
|
dde692eb88 | ||
|
|
cec72b837e | ||
|
|
95842c7987 | ||
|
|
183e3664cc | ||
|
|
08b124c8d4 | ||
|
|
ea08026cd0 | ||
|
|
daa9939c03 | ||
|
|
f757e5a6c3 | ||
|
|
ecb874db62 | ||
|
|
75f1862268 | ||
|
|
f8ab86f930 | ||
|
|
155854d9a9 | ||
|
|
5b6578247f | ||
|
|
b87c4a1e13 | ||
|
|
a0988508f0 | ||
|
|
a347c4def7 | ||
|
|
9c77bcc827 | ||
|
|
8d1f377bf0 | ||
|
|
f766f6ceae | ||
|
|
9dad897d49 | ||
|
|
5b6401519b | ||
|
|
293e080f03 | ||
|
|
633b665379 | ||
|
|
7fd334fddb | ||
|
|
10226a3992 | ||
|
|
383e868af0 | ||
|
|
40802d91d4 | ||
|
|
6d5784daa6 | ||
|
|
f80eb264fb | ||
|
|
3d956ca68b | ||
|
|
7ce131aaf8 | ||
|
|
60be47d115 | ||
|
|
bd187883da | ||
|
|
4f9217bca0 | ||
|
|
ce5222f1df | ||
|
|
cf7b0c8971 | ||
|
|
7bc4cb9868 | ||
|
|
f84f3ffeb7 | ||
|
|
c564a4a26c | ||
|
|
515fd7b75f | ||
|
|
662a4440cc | ||
|
|
5dee43b05c | ||
|
|
c8003c0697 | ||
|
|
83e2889d63 | ||
|
|
d49cd0019f | ||
|
|
0ba40bdfb8 | ||
|
|
f6cd97f6fd | ||
|
|
774a8bf039 | ||
|
|
4431ef1870 | ||
|
|
b3f0ba1430 | ||
|
|
a5f52f0f04 | ||
|
|
63524a2354 | ||
|
|
90edb7189f | ||
|
|
518f6b529b | ||
|
|
fb97e462de | ||
|
|
5b7fa05a87 | ||
|
|
d310a1269f | ||
|
|
9818835c9d | ||
|
|
f3b7f5944d | ||
|
|
fc5cde9434 | ||
|
|
6ea4662326 | ||
|
|
9d12308d06 | ||
|
|
21137d2ba7 | ||
|
|
273cb1921f | ||
|
|
cfa20ff221 | ||
|
|
759d136fe6 | ||
|
|
322aa41ad6 | ||
|
|
3e2f1d733c | ||
|
|
3fed738d2f | ||
|
|
5893e85708 | ||
|
|
1356665ed3 | ||
|
|
9739da8de3 | ||
|
|
249c8a4d96 | ||
|
|
f919fa92de | ||
|
|
21b58643fa | ||
|
|
6a0bcca9ec | ||
|
|
84328c303b | ||
|
|
f7b2b41df9 | ||
|
|
7a6b6435c4 | ||
|
|
bdb54decdc | ||
|
|
b5c41eeb98 | ||
|
|
719a7f7890 | ||
|
|
1b84fee708 | ||
|
|
58e5d4ff02 | ||
|
|
85ff03cde0 | ||
|
|
a3f0bb4547 | ||
|
|
93b20008e0 | ||
|
|
188a893fd0 | ||
|
|
052b746fbd | ||
|
|
80f89059aa | ||
|
|
826d83edfe | ||
|
|
f5d5fab2c8 | ||
|
|
fab2f22a89 | ||
|
|
a451bcc3c4 | ||
|
|
5e9ff3e313 | ||
|
|
cc81f19c68 | ||
|
|
5e89fba681 | ||
|
|
67eb652bf1 | ||
|
|
e0ea9a9ab5 | ||
|
|
ff29a34298 | ||
|
|
6686f66949 | ||
|
|
8a96ea25c4 | ||
|
|
cdddb4d360 | ||
|
|
03bd95405b | ||
|
|
177dfdf900 | ||
|
|
2ab0b3b819 | ||
|
|
888fec9299 | ||
|
|
e86b096b92 | ||
|
|
ffe36c9beb | ||
|
|
2d16d2d036 | ||
|
|
c69da2df70 | ||
|
|
5506669b06 | ||
|
|
b13940720a | ||
|
|
db61711753 | ||
|
|
c12a9f2673 | ||
|
|
2e32f1c8a1 | ||
|
|
03a1c8d2b8 | ||
|
|
d7a277607b | ||
|
|
fc8a72cdd8 | ||
|
|
1acebb3c47 | ||
|
|
78ed0c9312 | ||
|
|
98d2e5fe73 | ||
|
|
4325819075 | ||
|
|
c19c89e6df | ||
|
|
507929cb79 | ||
|
|
7d0a7aff44 | ||
|
|
92ba18342c | ||
|
|
6de5ace116 | ||
|
|
c9db1b9a7b | ||
|
|
24cb694494 | ||
|
|
85bdd9329b | ||
|
|
d40ea8fc81 | ||
|
|
5f9a1482f1 | ||
|
|
5c2238c7a5 | ||
|
|
5769065f27 | ||
|
|
0173479d18 | ||
|
|
08a3c54bac | ||
|
|
3617873431 | ||
|
|
6eb6788201 | ||
|
|
ebc3031fd9 | ||
|
|
42a7402cc5 | ||
|
|
6cd5c9e32f | ||
|
|
d45b830412 | ||
|
|
3a9c071e6e | ||
|
|
ca861bb1bb | ||
|
|
454d3dd52b | ||
|
|
3ec015b325 | ||
|
|
02718284ef | ||
|
|
b5f816dde5 | ||
|
|
499e1459eb | ||
|
|
b5aea548a8 | ||
|
|
3c6a505166 | ||
|
|
efc4d3efdf | ||
|
|
4214ed927f | ||
|
|
e040b200bc | ||
|
|
1dba50f42f | ||
|
|
0ffc92ab65 | ||
|
|
d30361537e | ||
|
|
510c71d41b | ||
|
|
013d2d52fd | ||
|
|
eee91f3f1b | ||
|
|
e87d5e145d | ||
|
|
291af664e1 | ||
|
|
9c0dba4ce1 | ||
|
|
8bfd27b00b | ||
|
|
c6f08dea89 | ||
|
|
9dfe4a30bb | ||
|
|
69b12f4e33 | ||
|
|
c3860804ff | ||
|
|
6a0c19fcf9 | ||
|
|
622c266160 | ||
|
|
dc4396b79c | ||
|
|
1a59b6413b | ||
|
|
992155c60c | ||
|
|
e633f62eaf | ||
|
|
b85af0e533 | ||
|
|
bce1b7a10a | ||
|
|
c292bdd2ca | ||
|
|
1cb9f64917 | ||
|
|
1bded42b2a | ||
|
|
7f64f0454d | ||
|
|
375bc88f95 | ||
|
|
d53a86b01d | ||
|
|
6c7e79eff6 | ||
|
|
d0bc84eb33 | ||
|
|
fabc14355c | ||
|
|
07e086b41e | ||
|
|
48674ec54c | ||
|
|
fc7874e64e | ||
|
|
dcb0da0a7d | ||
|
|
a9f48bd9d1 | ||
|
|
33197608ed | ||
|
|
f951410ef0 | ||
|
|
47ade2f9f9 | ||
|
|
263e143d1b | ||
|
|
21a44d74bd | ||
|
|
efd485cbb8 | ||
|
|
7a6550c1d1 | ||
|
|
23ad470daf | ||
|
|
6dcec47235 | ||
|
|
e93d62680d | ||
|
|
5dbf68ddc4 | ||
|
|
d8d84bf5d4 | ||
|
|
6f6893a93a | ||
|
|
7ae25d10c8 | ||
|
|
d80683f2bf | ||
|
|
ab98d4889b | ||
|
|
ce11ca9d49 | ||
|
|
edda149d75 | ||
|
|
291ca2c32c | ||
|
|
3ba2af289b | ||
|
|
d8d8c908ed | ||
|
|
680b3dd80b | ||
|
|
270e13bb9a | ||
|
|
b3aa7055e4 | ||
|
|
f16461d7d0 | ||
|
|
970f8db5c4 | ||
|
|
bc9086c9af | ||
|
|
a367c6de6e | ||
|
|
27d1a566d0 | ||
|
|
4f52077d97 | ||
|
|
6e485453d0 | ||
|
|
9bae93cd39 | ||
|
|
1a4b253ee5 | ||
|
|
89f6b65ee6 | ||
|
|
a2c6b4ad2f | ||
|
|
6b7d85b769 | ||
|
|
cb3eb75712 | ||
|
|
bae85d858e | ||
|
|
755fd695f5 | ||
|
|
74e25c11f1 |
2
.github/actions/run_tests/action.yml
vendored
@@ -10,7 +10,7 @@ runs:
|
|||||||
cargo install cargo-nextest
|
cargo install cargo-nextest
|
||||||
|
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4
|
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
|
||||||
with:
|
with:
|
||||||
node-version: "18"
|
node-version: "18"
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/bump_collab_staging.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
4
.github/workflows/bump_patch_version.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
|||||||
- buildjet-16vcpu-ubuntu-2204
|
- buildjet-16vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.inputs.branch }}
|
ref: ${{ github.event.inputs.branch }}
|
||||||
ssh-key: ${{ secrets.ZED_BOT_DEPLOY_KEY }}
|
ssh-key: ${{ secrets.ZED_BOT_DEPLOY_KEY }}
|
||||||
@@ -43,6 +43,8 @@ jobs:
|
|||||||
esac
|
esac
|
||||||
which cargo-set-version > /dev/null || cargo install cargo-edit
|
which cargo-set-version > /dev/null || cargo install cargo-edit
|
||||||
output=$(cargo set-version -p zed --bump patch 2>&1 | sed 's/.* //')
|
output=$(cargo set-version -p zed --bump patch 2>&1 | sed 's/.* //')
|
||||||
|
export GIT_COMMITTER_NAME="Zed Bot"
|
||||||
|
export GIT_COMMITTER_EMAIL="hi@zed.dev"
|
||||||
git commit -am "Bump to $output for @$GITHUB_ACTOR" --author "Zed Bot <hi@zed.dev>"
|
git commit -am "Bump to $output for @$GITHUB_ACTOR" --author "Zed Bot <hi@zed.dev>"
|
||||||
git tag v${output}${tag_suffix}
|
git tag v${output}${tag_suffix}
|
||||||
git push origin HEAD v${output}${tag_suffix}
|
git push origin HEAD v${output}${tag_suffix}
|
||||||
|
|||||||
43
.github/workflows/ci.yml
vendored
@@ -25,6 +25,7 @@ env:
|
|||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
CARGO_INCREMENTAL: 0
|
CARGO_INCREMENTAL: 0
|
||||||
RUST_BACKTRACE: 1
|
RUST_BACKTRACE: 1
|
||||||
|
RUSTFLAGS: "-D warnings"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
migration_checks:
|
migration_checks:
|
||||||
@@ -36,7 +37,7 @@ jobs:
|
|||||||
- test
|
- test
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
clean: false
|
clean: false
|
||||||
fetch-depth: 0 # fetch full history
|
fetch-depth: 0 # fetch full history
|
||||||
@@ -78,25 +79,26 @@ jobs:
|
|||||||
- buildjet-8vcpu-ubuntu-2204
|
- buildjet-8vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
- name: Run style checks
|
- name: Run style checks
|
||||||
uses: ./.github/actions/check_style
|
uses: ./.github/actions/check_style
|
||||||
|
|
||||||
- name: Check for typos
|
- name: Check for typos
|
||||||
uses: crate-ci/typos@v1.24.6
|
uses: crate-ci/typos@8e6a4285bcbde632c5d79900a7779746e8b7ea3f # v1.24.6
|
||||||
with:
|
with:
|
||||||
config: ./typos.toml
|
config: ./typos.toml
|
||||||
|
|
||||||
macos_tests:
|
macos_tests:
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
name: (macOS) Run Clippy and tests
|
name: (macOS) Run Clippy and tests
|
||||||
|
if: github.repository_owner == 'zed-industries'
|
||||||
runs-on:
|
runs-on:
|
||||||
- self-hosted
|
- self-hosted
|
||||||
- test
|
- test
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
clean: false
|
clean: false
|
||||||
|
|
||||||
@@ -115,17 +117,18 @@ jobs:
|
|||||||
uses: ./.github/actions/run_tests
|
uses: ./.github/actions/run_tests
|
||||||
|
|
||||||
- name: Build collab
|
- name: Build collab
|
||||||
run: RUSTFLAGS="-D warnings" cargo build -p collab
|
run: cargo build -p collab
|
||||||
|
|
||||||
- name: Build other binaries and features
|
- name: Build other binaries and features
|
||||||
run: |
|
run: |
|
||||||
RUSTFLAGS="-D warnings" cargo build --workspace --bins --all-features
|
cargo build --workspace --bins --all-features
|
||||||
cargo check -p gpui --features "macos-blade"
|
cargo check -p gpui --features "macos-blade"
|
||||||
RUSTFLAGS="-D warnings" cargo build -p remote_server
|
cargo build -p remote_server
|
||||||
|
|
||||||
linux_tests:
|
linux_tests:
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
name: (Linux) Run Clippy and tests
|
name: (Linux) Run Clippy and tests
|
||||||
|
if: github.repository_owner == 'zed-industries'
|
||||||
runs-on:
|
runs-on:
|
||||||
- buildjet-16vcpu-ubuntu-2204
|
- buildjet-16vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
@@ -133,7 +136,7 @@ jobs:
|
|||||||
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
clean: false
|
clean: false
|
||||||
|
|
||||||
@@ -153,11 +156,12 @@ jobs:
|
|||||||
uses: ./.github/actions/run_tests
|
uses: ./.github/actions/run_tests
|
||||||
|
|
||||||
- name: Build Zed
|
- name: Build Zed
|
||||||
run: RUSTFLAGS="-D warnings" cargo build -p zed
|
run: cargo build -p zed
|
||||||
|
|
||||||
build_remote_server:
|
build_remote_server:
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
name: (Linux) Build Remote Server
|
name: (Linux) Build Remote Server
|
||||||
|
if: github.repository_owner == 'zed-industries'
|
||||||
runs-on:
|
runs-on:
|
||||||
- buildjet-16vcpu-ubuntu-2204
|
- buildjet-16vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
@@ -165,7 +169,7 @@ jobs:
|
|||||||
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
clean: false
|
clean: false
|
||||||
|
|
||||||
@@ -179,16 +183,20 @@ jobs:
|
|||||||
run: ./script/remote-server && ./script/install-mold 2.34.0
|
run: ./script/remote-server && ./script/install-mold 2.34.0
|
||||||
|
|
||||||
- name: Build Remote Server
|
- name: Build Remote Server
|
||||||
run: RUSTFLAGS="-D warnings" cargo build -p remote_server
|
run: cargo build -p remote_server
|
||||||
|
|
||||||
# todo(windows): Actually run the tests
|
# todo(windows): Actually run the tests
|
||||||
windows_tests:
|
windows_tests:
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
name: (Windows) Run Clippy and tests
|
name: (Windows) Run Clippy and tests
|
||||||
|
if: github.repository_owner == 'zed-industries'
|
||||||
runs-on: hosted-windows-1
|
runs-on: hosted-windows-1
|
||||||
steps:
|
steps:
|
||||||
|
# more info here:- https://github.com/rust-lang/cargo/issues/13020
|
||||||
|
- name: Enable longer pathnames for git
|
||||||
|
run: git config --system core.longpaths true
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
clean: false
|
clean: false
|
||||||
|
|
||||||
@@ -203,7 +211,7 @@ jobs:
|
|||||||
run: cargo xtask clippy
|
run: cargo xtask clippy
|
||||||
|
|
||||||
- name: Build Zed
|
- name: Build Zed
|
||||||
run: $env:RUSTFLAGS="-D warnings"; cargo build
|
run: cargo build
|
||||||
|
|
||||||
bundle-mac:
|
bundle-mac:
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
@@ -224,12 +232,12 @@ jobs:
|
|||||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||||
steps:
|
steps:
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4
|
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
|
||||||
with:
|
with:
|
||||||
node-version: "18"
|
node-version: "18"
|
||||||
|
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
# We need to fetch more than one commit so that `script/draft-release-notes`
|
# We need to fetch more than one commit so that `script/draft-release-notes`
|
||||||
# is able to diff between the current and previous tag.
|
# is able to diff between the current and previous tag.
|
||||||
@@ -314,7 +322,7 @@ jobs:
|
|||||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
clean: false
|
clean: false
|
||||||
|
|
||||||
@@ -361,7 +369,7 @@ jobs:
|
|||||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
clean: false
|
clean: false
|
||||||
|
|
||||||
@@ -386,7 +394,6 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload app bundle to release
|
- name: Upload app bundle to release
|
||||||
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||||
if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }}
|
|
||||||
with:
|
with:
|
||||||
draft: true
|
draft: true
|
||||||
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ jobs:
|
|||||||
stale-issue-message: >
|
stale-issue-message: >
|
||||||
Hi there! 👋
|
Hi there! 👋
|
||||||
|
|
||||||
We're working to clean up our issue tracker by closing older issues that might not be relevant anymore. Are you able to reproduce this issue in the latest version of Zed? If so, please let us know by commenting on this issue and we will keep it open; otherwise, we'll close it in 7 days. Feel free to open a new issue if you're seeing this message after the issue has been closed.
|
We're working to clean up our issue tracker by closing older issues that might not be relevant anymore. If you are able to reproduce this issue in the latest version of Zed, please let us know by commenting on this issue, and we will keep it open. If you can't reproduce it, feel free to close the issue yourself. Otherwise, we'll close it in 7 days.
|
||||||
|
|
||||||
Thanks for your help!
|
Thanks for your help!
|
||||||
close-issue-message: "This issue was closed due to inactivity. If you're still experiencing this problem, please open a new issue with a link to this issue."
|
close-issue-message: "This issue was closed due to inactivity. If you're still experiencing this problem, please open a new issue with a link to this issue."
|
||||||
# We will increase `days-before-stale` to 365 on or after Jan 24th,
|
# We will increase `days-before-stale` to 365 on or after Jan 24th,
|
||||||
# 2024. This date marks one year since migrating issues from
|
# 2024. This date marks one year since migrating issues from
|
||||||
# 'community' to 'zed' repository. The migration added activity to all
|
# 'community' to 'zed' repository. The migration added activity to all
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.repository_owner == 'zed-industries'
|
if: github.repository_owner == 'zed-industries'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
- name: Set up uv
|
- name: Set up uv
|
||||||
uses: astral-sh/setup-uv@f3bcaebff5eace81a1c062af9f9011aae482ca9d # v3
|
uses: astral-sh/setup-uv@f3bcaebff5eace81a1c062af9f9011aae482ca9d # v3
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.repository_owner == 'zed-industries'
|
if: github.repository_owner == 'zed-industries'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
- name: Set up uv
|
- name: Set up uv
|
||||||
uses: astral-sh/setup-uv@f3bcaebff5eace81a1c062af9f9011aae482ca9d # v3
|
uses: astral-sh/setup-uv@f3bcaebff5eace81a1c062af9f9011aae482ca9d # v3
|
||||||
with:
|
with:
|
||||||
|
|||||||
4
.github/workflows/danger.yml
vendored
@@ -14,14 +14,14 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
|
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
|
||||||
with:
|
with:
|
||||||
version: 9
|
version: 9
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4
|
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
|
||||||
with:
|
with:
|
||||||
node-version: "20"
|
node-version: "20"
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
|
|||||||
10
.github/workflows/deploy_cloudflare.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
clean: false
|
clean: false
|
||||||
|
|
||||||
@@ -37,28 +37,28 @@ jobs:
|
|||||||
mdbook build ./docs --dest-dir=../target/deploy/docs/
|
mdbook build ./docs --dest-dir=../target/deploy/docs/
|
||||||
|
|
||||||
- name: Deploy Docs
|
- name: Deploy Docs
|
||||||
uses: cloudflare/wrangler-action@9681c2997648301493e78cacbfb790a9f19c833f # v3
|
uses: cloudflare/wrangler-action@b2a0191ce60d21388e1a8dcc968b4e9966f938e1 # v3
|
||||||
with:
|
with:
|
||||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
command: pages deploy target/deploy --project-name=docs
|
command: pages deploy target/deploy --project-name=docs
|
||||||
|
|
||||||
- name: Deploy Install
|
- name: Deploy Install
|
||||||
uses: cloudflare/wrangler-action@9681c2997648301493e78cacbfb790a9f19c833f # v3
|
uses: cloudflare/wrangler-action@b2a0191ce60d21388e1a8dcc968b4e9966f938e1 # v3
|
||||||
with:
|
with:
|
||||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
command: r2 object put -f script/install.sh zed-open-source-website-assets/install.sh
|
command: r2 object put -f script/install.sh zed-open-source-website-assets/install.sh
|
||||||
|
|
||||||
- name: Deploy Docs Workers
|
- name: Deploy Docs Workers
|
||||||
uses: cloudflare/wrangler-action@9681c2997648301493e78cacbfb790a9f19c833f # v3
|
uses: cloudflare/wrangler-action@b2a0191ce60d21388e1a8dcc968b4e9966f938e1 # v3
|
||||||
with:
|
with:
|
||||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
command: deploy .cloudflare/docs-proxy/src/worker.js
|
command: deploy .cloudflare/docs-proxy/src/worker.js
|
||||||
|
|
||||||
- name: Deploy Install Workers
|
- name: Deploy Install Workers
|
||||||
uses: cloudflare/wrangler-action@9681c2997648301493e78cacbfb790a9f19c833f # v3
|
uses: cloudflare/wrangler-action@b2a0191ce60d21388e1a8dcc968b4e9966f938e1 # v3
|
||||||
with:
|
with:
|
||||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
|
|||||||
8
.github/workflows/deploy_collab.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
- test
|
- test
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
clean: false
|
clean: false
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -36,7 +36,7 @@ jobs:
|
|||||||
needs: style
|
needs: style
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
clean: false
|
clean: false
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -71,7 +71,7 @@ jobs:
|
|||||||
run: doctl registry login
|
run: doctl registry login
|
||||||
|
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
clean: false
|
clean: false
|
||||||
|
|
||||||
@@ -97,7 +97,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
clean: false
|
clean: false
|
||||||
|
|
||||||
|
|||||||
4
.github/workflows/docs.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
|
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
|
||||||
with:
|
with:
|
||||||
@@ -31,7 +31,7 @@ jobs:
|
|||||||
}
|
}
|
||||||
|
|
||||||
- name: Check for Typos with Typos-CLI
|
- name: Check for Typos with Typos-CLI
|
||||||
uses: crate-ci/typos@v1.24.6
|
uses: crate-ci/typos@8e6a4285bcbde632c5d79900a7779746e8b7ea3f # v1.24.6
|
||||||
with:
|
with:
|
||||||
config: ./typos.toml
|
config: ./typos.toml
|
||||||
files: ./docs/
|
files: ./docs/
|
||||||
|
|||||||
2
.github/workflows/publish_extension_cli.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
- ubuntu-latest
|
- ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
clean: false
|
clean: false
|
||||||
|
|
||||||
|
|||||||
4
.github/workflows/randomized_tests.yml
vendored
@@ -22,12 +22,12 @@ jobs:
|
|||||||
- buildjet-16vcpu-ubuntu-2204
|
- buildjet-16vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4
|
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
|
||||||
with:
|
with:
|
||||||
node-version: "18"
|
node-version: "18"
|
||||||
|
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
clean: false
|
clean: false
|
||||||
|
|
||||||
|
|||||||
14
.github/workflows/release_nightly.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
|||||||
- test
|
- test
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
clean: false
|
clean: false
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -44,7 +44,7 @@ jobs:
|
|||||||
needs: style
|
needs: style
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
clean: false
|
clean: false
|
||||||
|
|
||||||
@@ -70,12 +70,12 @@ jobs:
|
|||||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||||
steps:
|
steps:
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4
|
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
|
||||||
with:
|
with:
|
||||||
node-version: "18"
|
node-version: "18"
|
||||||
|
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
clean: false
|
clean: false
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ jobs:
|
|||||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
clean: false
|
clean: false
|
||||||
|
|
||||||
@@ -149,7 +149,7 @@ jobs:
|
|||||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
clean: false
|
clean: false
|
||||||
|
|
||||||
@@ -182,7 +182,7 @@ jobs:
|
|||||||
- bundle-linux-arm
|
- bundle-linux-arm
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
# Code of Conduct
|
# Code of Conduct
|
||||||
|
|
||||||
The Code of Conduct for this repository can be found online at [zed.dev/docs/code-of-conduct](https://zed.dev/docs/code-of-conduct).
|
The Code of Conduct for this repository can be found online at [zed.dev/code-of-conduct](https://zed.dev/code-of-conduct).
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Thanks for your interest in contributing to Zed, the collaborative platform that is also a code editor!
|
Thanks for your interest in contributing to Zed, the collaborative platform that is also a code editor!
|
||||||
|
|
||||||
All activity in Zed forums is subject to our [Code of Conduct](https://zed.dev/docs/code-of-conduct). Additionally, contributors must sign our [Contributor License Agreement](https://zed.dev/cla) before their contributions can be merged.
|
All activity in Zed forums is subject to our [Code of Conduct](https://zed.dev/code-of-conduct). Additionally, contributors must sign our [Contributor License Agreement](https://zed.dev/cla) before their contributions can be merged.
|
||||||
|
|
||||||
## Contribution ideas
|
## Contribution ideas
|
||||||
|
|
||||||
|
|||||||
775
Cargo.lock
generated
20
Cargo.toml
@@ -23,7 +23,6 @@ members = [
|
|||||||
"crates/context_servers",
|
"crates/context_servers",
|
||||||
"crates/copilot",
|
"crates/copilot",
|
||||||
"crates/db",
|
"crates/db",
|
||||||
"crates/dev_server_projects",
|
|
||||||
"crates/diagnostics",
|
"crates/diagnostics",
|
||||||
"crates/docs_preprocessor",
|
"crates/docs_preprocessor",
|
||||||
"crates/editor",
|
"crates/editor",
|
||||||
@@ -31,6 +30,7 @@ members = [
|
|||||||
"crates/extension",
|
"crates/extension",
|
||||||
"crates/extension_api",
|
"crates/extension_api",
|
||||||
"crates/extension_cli",
|
"crates/extension_cli",
|
||||||
|
"crates/extension_host",
|
||||||
"crates/extensions_ui",
|
"crates/extensions_ui",
|
||||||
"crates/feature_flags",
|
"crates/feature_flags",
|
||||||
"crates/feedback",
|
"crates/feedback",
|
||||||
@@ -45,7 +45,6 @@ members = [
|
|||||||
"crates/google_ai",
|
"crates/google_ai",
|
||||||
"crates/gpui",
|
"crates/gpui",
|
||||||
"crates/gpui_macros",
|
"crates/gpui_macros",
|
||||||
"crates/headless",
|
|
||||||
"crates/html_to_markdown",
|
"crates/html_to_markdown",
|
||||||
"crates/http_client",
|
"crates/http_client",
|
||||||
"crates/image_viewer",
|
"crates/image_viewer",
|
||||||
@@ -119,6 +118,7 @@ members = [
|
|||||||
"crates/theme_selector",
|
"crates/theme_selector",
|
||||||
"crates/time_format",
|
"crates/time_format",
|
||||||
"crates/title_bar",
|
"crates/title_bar",
|
||||||
|
"crates/toolchain_selector",
|
||||||
"crates/ui",
|
"crates/ui",
|
||||||
"crates/ui_input",
|
"crates/ui_input",
|
||||||
"crates/ui_macros",
|
"crates/ui_macros",
|
||||||
@@ -139,7 +139,6 @@ members = [
|
|||||||
"extensions/astro",
|
"extensions/astro",
|
||||||
"extensions/clojure",
|
"extensions/clojure",
|
||||||
"extensions/csharp",
|
"extensions/csharp",
|
||||||
"extensions/dart",
|
|
||||||
"extensions/deno",
|
"extensions/deno",
|
||||||
"extensions/elixir",
|
"extensions/elixir",
|
||||||
"extensions/elm",
|
"extensions/elm",
|
||||||
@@ -201,10 +200,10 @@ command_palette_hooks = { path = "crates/command_palette_hooks" }
|
|||||||
context_servers = { path = "crates/context_servers" }
|
context_servers = { path = "crates/context_servers" }
|
||||||
copilot = { path = "crates/copilot" }
|
copilot = { path = "crates/copilot" }
|
||||||
db = { path = "crates/db" }
|
db = { path = "crates/db" }
|
||||||
dev_server_projects = { path = "crates/dev_server_projects" }
|
|
||||||
diagnostics = { path = "crates/diagnostics" }
|
diagnostics = { path = "crates/diagnostics" }
|
||||||
editor = { path = "crates/editor" }
|
editor = { path = "crates/editor" }
|
||||||
extension = { path = "crates/extension" }
|
extension = { path = "crates/extension" }
|
||||||
|
extension_host = { path = "crates/extension_host" }
|
||||||
extensions_ui = { path = "crates/extensions_ui" }
|
extensions_ui = { path = "crates/extensions_ui" }
|
||||||
feature_flags = { path = "crates/feature_flags" }
|
feature_flags = { path = "crates/feature_flags" }
|
||||||
feedback = { path = "crates/feedback" }
|
feedback = { path = "crates/feedback" }
|
||||||
@@ -219,7 +218,6 @@ go_to_line = { path = "crates/go_to_line" }
|
|||||||
google_ai = { path = "crates/google_ai" }
|
google_ai = { path = "crates/google_ai" }
|
||||||
gpui = { path = "crates/gpui", default-features = false, features = ["http_client"]}
|
gpui = { path = "crates/gpui", default-features = false, features = ["http_client"]}
|
||||||
gpui_macros = { path = "crates/gpui_macros" }
|
gpui_macros = { path = "crates/gpui_macros" }
|
||||||
headless = { path = "crates/headless" }
|
|
||||||
html_to_markdown = { path = "crates/html_to_markdown" }
|
html_to_markdown = { path = "crates/html_to_markdown" }
|
||||||
http_client = { path = "crates/http_client" }
|
http_client = { path = "crates/http_client" }
|
||||||
image_viewer = { path = "crates/image_viewer" }
|
image_viewer = { path = "crates/image_viewer" }
|
||||||
@@ -294,6 +292,7 @@ theme_importer = { path = "crates/theme_importer" }
|
|||||||
theme_selector = { path = "crates/theme_selector" }
|
theme_selector = { path = "crates/theme_selector" }
|
||||||
time_format = { path = "crates/time_format" }
|
time_format = { path = "crates/time_format" }
|
||||||
title_bar = { path = "crates/title_bar" }
|
title_bar = { path = "crates/title_bar" }
|
||||||
|
toolchain_selector = { path = "crates/toolchain_selector" }
|
||||||
ui = { path = "crates/ui" }
|
ui = { path = "crates/ui" }
|
||||||
ui_input = { path = "crates/ui_input" }
|
ui_input = { path = "crates/ui_input" }
|
||||||
ui_macros = { path = "crates/ui_macros" }
|
ui_macros = { path = "crates/ui_macros" }
|
||||||
@@ -373,6 +372,7 @@ linkify = "0.10.0"
|
|||||||
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
|
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
|
||||||
markup5ever_rcdom = "0.3.0"
|
markup5ever_rcdom = "0.3.0"
|
||||||
nanoid = "0.4"
|
nanoid = "0.4"
|
||||||
|
nbformat = "0.3.2"
|
||||||
nix = "0.29"
|
nix = "0.29"
|
||||||
num-format = "0.4.4"
|
num-format = "0.4.4"
|
||||||
once_cell = "1.19.0"
|
once_cell = "1.19.0"
|
||||||
@@ -380,6 +380,11 @@ ordered-float = "2.1.1"
|
|||||||
palette = { version = "0.7.5", default-features = false, features = ["std"] }
|
palette = { version = "0.7.5", default-features = false, features = ["std"] }
|
||||||
parking_lot = "0.12.1"
|
parking_lot = "0.12.1"
|
||||||
pathdiff = "0.2"
|
pathdiff = "0.2"
|
||||||
|
pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
||||||
|
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
||||||
|
pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
||||||
|
pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
||||||
|
pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
|
||||||
postage = { version = "0.5", features = ["futures-traits"] }
|
postage = { version = "0.5", features = ["futures-traits"] }
|
||||||
pretty_assertions = "1.3.0"
|
pretty_assertions = "1.3.0"
|
||||||
profiling = "1"
|
profiling = "1"
|
||||||
@@ -388,6 +393,7 @@ prost-build = "0.9"
|
|||||||
prost-types = "0.9"
|
prost-types = "0.9"
|
||||||
pulldown-cmark = { version = "0.12.0", default-features = false }
|
pulldown-cmark = { version = "0.12.0", default-features = false }
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
|
rayon = "1.8"
|
||||||
regex = "1.5"
|
regex = "1.5"
|
||||||
repair_json = "0.1.0"
|
repair_json = "0.1.0"
|
||||||
reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "fd110f6998da16bbca97b6dddda9be7827c50e29", default-features = false, features = [
|
reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "fd110f6998da16bbca97b6dddda9be7827c50e29", default-features = false, features = [
|
||||||
@@ -399,7 +405,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "fd110f
|
|||||||
"stream",
|
"stream",
|
||||||
] }
|
] }
|
||||||
rsa = "0.9.6"
|
rsa = "0.9.6"
|
||||||
runtimelib = { version = "0.15", default-features = false, features = [
|
runtimelib = { version = "0.16.1", default-features = false, features = [
|
||||||
"async-dispatcher-runtime",
|
"async-dispatcher-runtime",
|
||||||
] }
|
] }
|
||||||
rustc-demangle = "0.1.23"
|
rustc-demangle = "0.1.23"
|
||||||
@@ -459,7 +465,7 @@ tree-sitter-diff = "0.1.0"
|
|||||||
tree-sitter-html = "0.20"
|
tree-sitter-html = "0.20"
|
||||||
tree-sitter-jsdoc = "0.23"
|
tree-sitter-jsdoc = "0.23"
|
||||||
tree-sitter-json = "0.23"
|
tree-sitter-json = "0.23"
|
||||||
tree-sitter-md = { git = "https://github.com/zed-industries/tree-sitter-markdown", rev = "4cfa6aad6b75052a5077c80fd934757d9267d81b" }
|
tree-sitter-md = { git = "https://github.com/tree-sitter-grammars/tree-sitter-markdown", rev = "9a23c1a96c0513d8fc6520972beedd419a973539" }
|
||||||
tree-sitter-python = "0.23"
|
tree-sitter-python = "0.23"
|
||||||
tree-sitter-regex = "0.23"
|
tree-sitter-regex = "0.23"
|
||||||
tree-sitter-ruby = "0.23"
|
tree-sitter-ruby = "0.23"
|
||||||
|
|||||||
@@ -58,6 +58,7 @@
|
|||||||
"gitignore": "vcs",
|
"gitignore": "vcs",
|
||||||
"gitkeep": "vcs",
|
"gitkeep": "vcs",
|
||||||
"gitmodules": "vcs",
|
"gitmodules": "vcs",
|
||||||
|
"gleam": "gleam",
|
||||||
"go": "go",
|
"go": "go",
|
||||||
"gql": "graphql",
|
"gql": "graphql",
|
||||||
"graphql": "graphql",
|
"graphql": "graphql",
|
||||||
@@ -65,6 +66,7 @@
|
|||||||
"h": "c",
|
"h": "c",
|
||||||
"handlebars": "code",
|
"handlebars": "code",
|
||||||
"hbs": "template",
|
"hbs": "template",
|
||||||
|
"hcl": "hcl",
|
||||||
"heex": "elixir",
|
"heex": "elixir",
|
||||||
"heic": "image",
|
"heic": "image",
|
||||||
"heif": "image",
|
"heif": "image",
|
||||||
@@ -82,6 +84,7 @@
|
|||||||
"j2k": "image",
|
"j2k": "image",
|
||||||
"java": "java",
|
"java": "java",
|
||||||
"jfif": "image",
|
"jfif": "image",
|
||||||
|
"jl": "julia",
|
||||||
"jp2": "image",
|
"jp2": "image",
|
||||||
"jpeg": "image",
|
"jpeg": "image",
|
||||||
"jpg": "image",
|
"jpg": "image",
|
||||||
@@ -116,6 +119,7 @@
|
|||||||
"myd": "storage",
|
"myd": "storage",
|
||||||
"myi": "storage",
|
"myi": "storage",
|
||||||
"nim": "nim",
|
"nim": "nim",
|
||||||
|
"nix": "nix",
|
||||||
"nu": "terminal",
|
"nu": "terminal",
|
||||||
"odp": "document",
|
"odp": "document",
|
||||||
"ods": "document",
|
"ods": "document",
|
||||||
@@ -143,12 +147,15 @@
|
|||||||
"rb": "ruby",
|
"rb": "ruby",
|
||||||
"rebar.config": "erlang",
|
"rebar.config": "erlang",
|
||||||
"rkt": "code",
|
"rkt": "code",
|
||||||
|
"roc": "roc",
|
||||||
"rs": "rust",
|
"rs": "rust",
|
||||||
"rtf": "document",
|
"rtf": "document",
|
||||||
|
"sass": "sass",
|
||||||
"sav": "storage",
|
"sav": "storage",
|
||||||
"sc": "scala",
|
"sc": "scala",
|
||||||
"scala": "scala",
|
"scala": "scala",
|
||||||
"scm": "code",
|
"scm": "code",
|
||||||
|
"scss": "sass",
|
||||||
"sdf": "storage",
|
"sdf": "storage",
|
||||||
"sh": "terminal",
|
"sh": "terminal",
|
||||||
"sql": "storage",
|
"sql": "storage",
|
||||||
@@ -182,6 +189,7 @@
|
|||||||
"yaml": "settings",
|
"yaml": "settings",
|
||||||
"yml": "settings",
|
"yml": "settings",
|
||||||
"yrl": "erlang",
|
"yrl": "erlang",
|
||||||
|
"zig": "zig",
|
||||||
"zlogin": "terminal",
|
"zlogin": "terminal",
|
||||||
"zsh": "terminal",
|
"zsh": "terminal",
|
||||||
"zsh_aliases": "terminal",
|
"zsh_aliases": "terminal",
|
||||||
@@ -257,6 +265,9 @@
|
|||||||
"fsharp": {
|
"fsharp": {
|
||||||
"icon": "icons/file_icons/fsharp.svg"
|
"icon": "icons/file_icons/fsharp.svg"
|
||||||
},
|
},
|
||||||
|
"gleam": {
|
||||||
|
"icon": "icons/file_icons/gleam.svg"
|
||||||
|
},
|
||||||
"go": {
|
"go": {
|
||||||
"icon": "icons/file_icons/go.svg"
|
"icon": "icons/file_icons/go.svg"
|
||||||
},
|
},
|
||||||
@@ -266,6 +277,9 @@
|
|||||||
"haskell": {
|
"haskell": {
|
||||||
"icon": "icons/file_icons/haskell.svg"
|
"icon": "icons/file_icons/haskell.svg"
|
||||||
},
|
},
|
||||||
|
"hcl": {
|
||||||
|
"icon": "icons/file_icons/hcl.svg"
|
||||||
|
},
|
||||||
"heroku": {
|
"heroku": {
|
||||||
"icon": "icons/file_icons/heroku.svg"
|
"icon": "icons/file_icons/heroku.svg"
|
||||||
},
|
},
|
||||||
@@ -278,6 +292,9 @@
|
|||||||
"javascript": {
|
"javascript": {
|
||||||
"icon": "icons/file_icons/javascript.svg"
|
"icon": "icons/file_icons/javascript.svg"
|
||||||
},
|
},
|
||||||
|
"julia": {
|
||||||
|
"icon": "icons/file_icons/julia.svg"
|
||||||
|
},
|
||||||
"kotlin": {
|
"kotlin": {
|
||||||
"icon": "icons/file_icons/kotlin.svg"
|
"icon": "icons/file_icons/kotlin.svg"
|
||||||
},
|
},
|
||||||
@@ -293,6 +310,9 @@
|
|||||||
"nim": {
|
"nim": {
|
||||||
"icon": "icons/file_icons/nim.svg"
|
"icon": "icons/file_icons/nim.svg"
|
||||||
},
|
},
|
||||||
|
"nix": {
|
||||||
|
"icon": "icons/file_icons/nix.svg"
|
||||||
|
},
|
||||||
"ocaml": {
|
"ocaml": {
|
||||||
"icon": "icons/file_icons/ocaml.svg"
|
"icon": "icons/file_icons/ocaml.svg"
|
||||||
},
|
},
|
||||||
@@ -317,12 +337,18 @@
|
|||||||
"react": {
|
"react": {
|
||||||
"icon": "icons/file_icons/react.svg"
|
"icon": "icons/file_icons/react.svg"
|
||||||
},
|
},
|
||||||
|
"roc": {
|
||||||
|
"icon": "icons/file_icons/roc.svg"
|
||||||
|
},
|
||||||
"ruby": {
|
"ruby": {
|
||||||
"icon": "icons/file_icons/ruby.svg"
|
"icon": "icons/file_icons/ruby.svg"
|
||||||
},
|
},
|
||||||
"rust": {
|
"rust": {
|
||||||
"icon": "icons/file_icons/rust.svg"
|
"icon": "icons/file_icons/rust.svg"
|
||||||
},
|
},
|
||||||
|
"sass": {
|
||||||
|
"icon": "icons/file_icons/sass.svg"
|
||||||
|
},
|
||||||
"scala": {
|
"scala": {
|
||||||
"icon": "icons/file_icons/scala.svg"
|
"icon": "icons/file_icons/scala.svg"
|
||||||
},
|
},
|
||||||
@@ -361,6 +387,9 @@
|
|||||||
},
|
},
|
||||||
"vue": {
|
"vue": {
|
||||||
"icon": "icons/file_icons/vue.svg"
|
"icon": "icons/file_icons/vue.svg"
|
||||||
|
},
|
||||||
|
"zig": {
|
||||||
|
"icon": "icons/file_icons/zig.svg"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
assets/icons/file_icons/gleam.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7.3848 9.30444C7.3848 9.30444 7.53254 10.2646 8.53248 10.0882C9.53242 9.91193 9.36378 8.95549 9.36378 8.95549" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M5.54155 5.54157C6.12355 4.90104 6.01688 2.62541 7.22875 2.3985C8.44063 2.17158 9.19097 4.33148 9.91982 4.6814C10.6487 5.03133 12.8517 4.3028 13.4381 5.38734C14.0244 6.47188 12.1395 7.95973 12.026 8.64088C11.9126 9.32203 13.3614 11.2416 12.4675 12.1701C11.5736 13.0986 9.73005 11.7545 8.90486 11.8834C8.07966 12.0123 6.79244 13.9095 5.67367 13.3502C4.55491 12.7909 5.16702 10.5455 4.82437 9.87612C4.48171 9.20673 2.34028 8.54978 2.4525 7.35049C2.56471 6.15121 4.95956 6.1821 5.54155 5.54157Z" stroke="#FF7676" stroke-opacity="0.52" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M5.54155 5.54157C6.12355 4.90104 6.01688 2.62541 7.22875 2.3985C8.44063 2.17158 9.19097 4.33148 9.91982 4.6814C10.6487 5.03133 12.8517 4.3028 13.4381 5.38734C14.0244 6.47188 12.1395 7.95973 12.026 8.64088C11.9126 9.32203 13.3614 11.2416 12.4675 12.1701C11.5736 13.0986 9.73005 11.7545 8.90486 11.8834C8.07966 12.0123 6.79244 13.9095 5.67367 13.3502C4.55491 12.7909 5.16702 10.5455 4.82437 9.87612C4.48171 9.20673 2.34028 8.54978 2.4525 7.35049C2.56471 6.15121 4.95956 6.1821 5.54155 5.54157Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<circle cx="6.25098" cy="7.75" r="0.75" fill="black"/>
|
||||||
|
<circle cx="10.1035" cy="7.25" r="0.75" fill="black"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
3
assets/icons/file_icons/hcl.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.11466 3.11809C7.21859 3.37393 7.09545 3.66558 6.83961 3.76952L4.31181 4.79643C4.1233 4.87302 4 5.05619 4 5.25967V11.5C4 11.7761 3.77614 12 3.5 12H2.5C2.22386 12 2 11.7761 2 11.5V4.41827C2 3.90959 2.30825 3.45164 2.77953 3.26018L6.08686 1.91658C6.34269 1.81265 6.63434 1.93579 6.73828 2.19163L7.11466 3.11809ZM10.5 1.99999C10.7761 1.99999 11 2.22384 11 2.49999V10.5C11 10.7761 10.7761 11 10.5 11H9.5C9.22386 11 9 10.7761 9 10.5V9.49999C9 9.22384 8.77614 8.99999 8.5 8.99999H7.5C7.22386 8.99999 7 9.22384 7 9.49999V13.5C7 13.7761 6.77614 14 6.5 14H5.5C5.22386 14 5 13.7761 5 13.5V5.53124C5 5.25509 5.22386 5.03124 5.5 5.03124H6.5C6.77614 5.03124 7 5.25509 7 5.53124V6.49999C7 6.77613 7.22386 6.99999 7.5 6.99999H8.5C8.77614 6.99999 9 6.77613 9 6.49999V2.49999C9 2.22384 9.22386 1.99999 9.5 1.99999H10.5ZM13.5 4.03124C13.7761 4.03124 14 4.2551 14 4.53124L14 11.5847C14 12.0859 13.7006 12.5386 13.2394 12.7349L9.99399 14.1159C9.7399 14.224 9.44626 14.1057 9.33813 13.8516L8.94658 12.9315C8.83845 12.6774 8.95678 12.3837 9.21087 12.2756L11.6958 11.2182C11.8802 11.1397 12 10.9586 12 10.7581L12 4.53124C12 4.2551 12.2238 4.03124 12.5 4.03124L13.5 4.03124Z" fill="black"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
5
assets/icons/file_icons/julia.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="8" cy="5" r="2.75" fill="black"/>
|
||||||
|
<circle cx="4.75" cy="11" r="2.75" fill="black" fill-opacity="0.5"/>
|
||||||
|
<circle cx="11.25" cy="11" r="2.75" fill="black" fill-opacity="0.75"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 289 B |
8
assets/icons/file_icons/nix.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M6.00005 4.76556L4.76569 2.74996M6.00005 4.76556L3.75 4.76563M6.00005 4.76556L7.25006 4.7656" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
<path d="M10.0232 11.2311L11.2675 13.2406M10.0232 11.2311L12.2732 11.2199M10.0232 11.2311L8.7732 11.2373" stroke="black" stroke-opacity="0.5" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
<path d="M9.99025 4.91551L10.9985 2.77781M9.99025 4.91551L8.75599 3.03419M9.99025 4.91551L10.6759 5.9607" stroke="black" stroke-opacity="0.5" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
<path d="M6.0323 11.1009L5.03465 13.2436M6.0323 11.1009L7.27585 12.9761M6.0323 11.1009L5.34151 10.0592" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
<path d="M11.883 8.19023L14.2466 8.19287M11.883 8.19023L13.0602 6.27268M11.883 8.19023L11.229 9.25547" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
<path d="M4.12354 7.8356L1.76002 7.84465M4.12354 7.8356L2.95585 9.75894M4.12354 7.8356L4.7723 6.76713" stroke="black" stroke-opacity="0.5" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
7
assets/icons/file_icons/roc.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M5.51497 2.02702L1.92042 1.95067C1.69543 1.94589 1.57917 2.21756 1.73796 2.37702L6.24865 6.9068C6.42388 7.08277 6.72071 6.92326 6.67067 6.68002L5.75454 2.22659C5.73103 2.11231 5.63161 2.02949 5.51497 2.02702Z" fill="black" fill-opacity="0.5"/>
|
||||||
|
<path d="M8.05816 7.38492L12.1366 8.02844C12.3704 8.06532 12.5198 7.78697 12.3599 7.61255L7.30439 2.09814C7.13336 1.91159 6.82522 2.06811 6.87499 2.31624L7.852 7.18714C7.87257 7.28971 7.95483 7.36862 8.05816 7.38492Z" fill="black"/>
|
||||||
|
<path d="M9.0952 10.9797L11.3824 9.35081C11.564 9.22151 11.4983 8.93722 11.2785 8.90058L8.496 8.43683C8.31974 8.40746 8.17047 8.56712 8.21162 8.74101L8.70689 10.8337C8.74777 11.0064 8.95062 11.0827 9.0952 10.9797Z" fill="black" fill-opacity="0.5"/>
|
||||||
|
<path d="M5.10282 13.9632L7.59108 12.4532C7.68331 12.3972 7.72923 12.2884 7.70498 12.1832L6.75736 8.07484C6.699 7.8218 6.34133 7.81448 6.27266 8.06491L4.73201 13.6834C4.67223 13.9014 4.90954 14.0805 5.10282 13.9632Z" fill="black"/>
|
||||||
|
<path d="M11.3183 4.89351L13.1588 7.03149L15.535 6.14302C15.7099 6.07761 15.754 5.85043 15.6161 5.72438L13.7222 3.99219L11.4546 4.48614C11.2695 4.52645 11.1947 4.74995 11.3183 4.89351Z" fill="black"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
3
assets/icons/file_icons/sass.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.92096 7.00668C7.87408 7.83549 10.0987 7.48203 10.9376 7.06254C12.8751 6.09381 13.9407 4.39379 12.6407 2.90629C11.0157 1.04692 6.24221 2.49998 4.89844 3.40625C3.55467 4.31252 2.67972 5.53126 2.89071 7.1719C3.1017 8.81254 4.68758 9.7422 6.03128 10.3203C5.38786 10.5616 3.8517 11.0388 3.3125 11.7188C2.71341 12.4742 3.04343 14 4.51577 14C7.15639 14 7.59539 11.1486 7.14847 10.4375C7.88773 10.1295 8.49597 9.96169 9.40138 9.77081C9.63831 9.72087 9.65457 9.46395 9.41295 9.44827C8.80252 9.40864 7.30567 9.8489 6.92096 9.97657C5.78909 9.35157 4.51016 7.93818 4.59378 6.87501C4.68676 5.6928 5.27676 5.07603 6.84508 4.21876C8.01705 3.57813 10.258 3.10695 11.25 3.62501C12.6563 4.35936 10.7875 5.75599 9.92969 6.32031C9.28179 6.74656 8.21971 6.77513 7.22979 6.61435C6.99371 6.576 6.74048 6.84974 6.92096 7.00668ZM5.6719 12.4643C6.35508 11.9894 6.45471 11.1076 6.29955 10.8844C5.76663 11.0874 4.36593 11.9102 4.75111 12.4643C4.90628 12.6875 5.31358 12.7134 5.6719 12.4643Z" fill="black"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
5
assets/icons/file_icons/zig.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M14.25 12H11C10.794 12 10.6764 11.7648 10.8 11.6L11.925 10.1C11.9722 10.037 12.0463 10 12.125 10H12.75C12.8881 10 13 9.88807 13 9.75V6.25C13 6.11193 12.8881 6 12.75 6H12.4045C12.2187 6 12.0978 5.80442 12.1809 5.6382L12.9309 4.1382C12.9732 4.0535 13.0598 4 13.1545 4H14.25C14.3881 4 14.5 4.11193 14.5 4.25V11.75C14.5 11.8881 14.3881 12 14.25 12Z" fill="black"/>
|
||||||
|
<path d="M1.75 4H5C5.20601 4 5.32361 4.23519 5.2 4.4L4.075 5.9C4.02779 5.96295 3.95369 6 3.875 6H3.25C3.11193 6 3 6.11193 3 6.25V9.75C3 9.88807 3.11193 10 3.25 10H3.59549C3.78134 10 3.90221 10.1956 3.8191 10.3618L3.0691 11.8618C3.02675 11.9465 2.94018 12 2.84549 12H1.75C1.61193 12 1.5 11.8881 1.5 11.75V4.25C1.5 4.11193 1.61193 4 1.75 4Z" fill="black"/>
|
||||||
|
<path d="M7.55748 6H5.95006C5.74177 6 5.62482 5.76022 5.75306 5.59609L6.92493 4.09609C6.97231 4.03544 7.04498 4 7.12194 4H9.93075C9.97607 4 10.0205 3.98769 10.0594 3.96437L11.6408 3.0155C11.8641 2.88154 12.1179 3.13555 11.9837 3.3587L8.22612 9.6083C8.12629 9.77433 8.24508 9.98591 8.43881 9.98712L10.0039 9.9969C10.2092 9.99818 10.3255 10.2327 10.2023 10.3969L9.075 11.9C9.02779 11.963 8.95369 12 8.875 12H6.55383C6.51835 12 6.48328 12.0076 6.45094 12.0222L4.32473 12.9824C4.10122 13.0833 3.88113 12.8356 4.00771 12.6255L7.77161 6.37903C7.87201 6.2124 7.75202 6 7.55748 6Z" fill="black"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
7
assets/icons/list_x.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M8.33333 8H3" stroke="#FBF1C7" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M11.6667 4H3" stroke="#FBF1C7" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M11.6667 12H3" stroke="#FBF1C7" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M13.6667 6.66663L11 9.33329" stroke="#FBF1C7" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M11 6.66663L13.6667 9.33329" stroke="#FBF1C7" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 579 B |
1
assets/icons/wand.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-wand"><path d="M15 4V2"/><path d="M15 16v-2"/><path d="M8 9h2"/><path d="M20 9h2"/><path d="M17.8 11.8 19 13"/><path d="M15 9h.01"/><path d="M17.8 6.2 19 5"/><path d="m3 21 9-9"/><path d="M12.2 6.2 11 5"/></svg>
|
||||||
|
After Width: | Height: | Size: 414 B |
@@ -313,6 +313,15 @@
|
|||||||
"ctrl-k ctrl-l": "editor::ToggleFold",
|
"ctrl-k ctrl-l": "editor::ToggleFold",
|
||||||
"ctrl-k ctrl-[": "editor::FoldRecursive",
|
"ctrl-k ctrl-[": "editor::FoldRecursive",
|
||||||
"ctrl-k ctrl-]": "editor::UnfoldRecursive",
|
"ctrl-k ctrl-]": "editor::UnfoldRecursive",
|
||||||
|
"ctrl-k ctrl-1": ["editor::FoldAtLevel", { "level": 1 }],
|
||||||
|
"ctrl-k ctrl-2": ["editor::FoldAtLevel", { "level": 2 }],
|
||||||
|
"ctrl-k ctrl-3": ["editor::FoldAtLevel", { "level": 3 }],
|
||||||
|
"ctrl-k ctrl-4": ["editor::FoldAtLevel", { "level": 4 }],
|
||||||
|
"ctrl-k ctrl-5": ["editor::FoldAtLevel", { "level": 5 }],
|
||||||
|
"ctrl-k ctrl-6": ["editor::FoldAtLevel", { "level": 6 }],
|
||||||
|
"ctrl-k ctrl-7": ["editor::FoldAtLevel", { "level": 7 }],
|
||||||
|
"ctrl-k ctrl-8": ["editor::FoldAtLevel", { "level": 8 }],
|
||||||
|
"ctrl-k ctrl-9": ["editor::FoldAtLevel", { "level": 9 }],
|
||||||
"ctrl-k ctrl-0": "editor::FoldAll",
|
"ctrl-k ctrl-0": "editor::FoldAll",
|
||||||
"ctrl-k ctrl-j": "editor::UnfoldAll",
|
"ctrl-k ctrl-j": "editor::UnfoldAll",
|
||||||
"ctrl-space": "editor::ShowCompletions",
|
"ctrl-space": "editor::ShowCompletions",
|
||||||
@@ -505,6 +514,13 @@
|
|||||||
"ctrl-enter": "assistant::InlineAssist"
|
"ctrl-enter": "assistant::InlineAssist"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"context": "ProposedChangesEditor",
|
||||||
|
"bindings": {
|
||||||
|
"ctrl-shift-y": "editor::ApplyDiffHunk",
|
||||||
|
"ctrl-alt-a": "editor::ApplyAllDiffHunks"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && jupyter && !ContextEditor",
|
"context": "Editor && jupyter && !ContextEditor",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
@@ -516,6 +532,7 @@
|
|||||||
"context": "ContextEditor > Editor",
|
"context": "ContextEditor > Editor",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-enter": "assistant::Assist",
|
"ctrl-enter": "assistant::Assist",
|
||||||
|
"ctrl-shift-enter": "assistant::Edit",
|
||||||
"ctrl-s": "workspace::Save",
|
"ctrl-s": "workspace::Save",
|
||||||
"ctrl->": "assistant::QuoteSelection",
|
"ctrl->": "assistant::QuoteSelection",
|
||||||
"ctrl-<": "assistant::InsertIntoEditor",
|
"ctrl-<": "assistant::InsertIntoEditor",
|
||||||
|
|||||||
@@ -201,6 +201,7 @@
|
|||||||
"context": "ContextEditor > Editor",
|
"context": "ContextEditor > Editor",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"cmd-enter": "assistant::Assist",
|
"cmd-enter": "assistant::Assist",
|
||||||
|
"cmd-shift-enter": "assistant::Edit",
|
||||||
"cmd-s": "workspace::Save",
|
"cmd-s": "workspace::Save",
|
||||||
"cmd->": "assistant::QuoteSelection",
|
"cmd->": "assistant::QuoteSelection",
|
||||||
"cmd-<": "assistant::InsertIntoEditor",
|
"cmd-<": "assistant::InsertIntoEditor",
|
||||||
@@ -350,6 +351,15 @@
|
|||||||
"cmd-k cmd-l": "editor::ToggleFold",
|
"cmd-k cmd-l": "editor::ToggleFold",
|
||||||
"cmd-k cmd-[": "editor::FoldRecursive",
|
"cmd-k cmd-[": "editor::FoldRecursive",
|
||||||
"cmd-k cmd-]": "editor::UnfoldRecursive",
|
"cmd-k cmd-]": "editor::UnfoldRecursive",
|
||||||
|
"cmd-k cmd-1": ["editor::FoldAtLevel", { "level": 1 }],
|
||||||
|
"cmd-k cmd-2": ["editor::FoldAtLevel", { "level": 2 }],
|
||||||
|
"cmd-k cmd-3": ["editor::FoldAtLevel", { "level": 3 }],
|
||||||
|
"cmd-k cmd-4": ["editor::FoldAtLevel", { "level": 4 }],
|
||||||
|
"cmd-k cmd-5": ["editor::FoldAtLevel", { "level": 5 }],
|
||||||
|
"cmd-k cmd-6": ["editor::FoldAtLevel", { "level": 6 }],
|
||||||
|
"cmd-k cmd-7": ["editor::FoldAtLevel", { "level": 7 }],
|
||||||
|
"cmd-k cmd-8": ["editor::FoldAtLevel", { "level": 8 }],
|
||||||
|
"cmd-k cmd-9": ["editor::FoldAtLevel", { "level": 9 }],
|
||||||
"cmd-k cmd-0": "editor::FoldAll",
|
"cmd-k cmd-0": "editor::FoldAll",
|
||||||
"cmd-k cmd-j": "editor::UnfoldAll",
|
"cmd-k cmd-j": "editor::UnfoldAll",
|
||||||
"ctrl-space": "editor::ShowCompletions",
|
"ctrl-space": "editor::ShowCompletions",
|
||||||
@@ -538,6 +548,13 @@
|
|||||||
"ctrl-enter": "assistant::InlineAssist"
|
"ctrl-enter": "assistant::InlineAssist"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"context": "ProposedChangesEditor",
|
||||||
|
"bindings": {
|
||||||
|
"cmd-shift-y": "editor::ApplyDiffHunk",
|
||||||
|
"cmd-shift-a": "editor::ApplyAllDiffHunks"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"context": "PromptEditor",
|
"context": "PromptEditor",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
|
|||||||
@@ -157,51 +157,6 @@
|
|||||||
"7": ["vim::Number", 7],
|
"7": ["vim::Number", 7],
|
||||||
"8": ["vim::Number", 8],
|
"8": ["vim::Number", 8],
|
||||||
"9": ["vim::Number", 9],
|
"9": ["vim::Number", 9],
|
||||||
// window related commands (ctrl-w X)
|
|
||||||
"ctrl-w": null,
|
|
||||||
"ctrl-w left": ["workspace::ActivatePaneInDirection", "Left"],
|
|
||||||
"ctrl-w right": ["workspace::ActivatePaneInDirection", "Right"],
|
|
||||||
"ctrl-w up": ["workspace::ActivatePaneInDirection", "Up"],
|
|
||||||
"ctrl-w down": ["workspace::ActivatePaneInDirection", "Down"],
|
|
||||||
"ctrl-w h": ["workspace::ActivatePaneInDirection", "Left"],
|
|
||||||
"ctrl-w l": ["workspace::ActivatePaneInDirection", "Right"],
|
|
||||||
"ctrl-w k": ["workspace::ActivatePaneInDirection", "Up"],
|
|
||||||
"ctrl-w j": ["workspace::ActivatePaneInDirection", "Down"],
|
|
||||||
"ctrl-w ctrl-h": ["workspace::ActivatePaneInDirection", "Left"],
|
|
||||||
"ctrl-w ctrl-l": ["workspace::ActivatePaneInDirection", "Right"],
|
|
||||||
"ctrl-w ctrl-k": ["workspace::ActivatePaneInDirection", "Up"],
|
|
||||||
"ctrl-w ctrl-j": ["workspace::ActivatePaneInDirection", "Down"],
|
|
||||||
"ctrl-w shift-left": ["workspace::SwapPaneInDirection", "Left"],
|
|
||||||
"ctrl-w shift-right": ["workspace::SwapPaneInDirection", "Right"],
|
|
||||||
"ctrl-w shift-up": ["workspace::SwapPaneInDirection", "Up"],
|
|
||||||
"ctrl-w shift-down": ["workspace::SwapPaneInDirection", "Down"],
|
|
||||||
"ctrl-w shift-h": ["workspace::SwapPaneInDirection", "Left"],
|
|
||||||
"ctrl-w shift-l": ["workspace::SwapPaneInDirection", "Right"],
|
|
||||||
"ctrl-w shift-k": ["workspace::SwapPaneInDirection", "Up"],
|
|
||||||
"ctrl-w shift-j": ["workspace::SwapPaneInDirection", "Down"],
|
|
||||||
"ctrl-w g t": "pane::ActivateNextItem",
|
|
||||||
"ctrl-w ctrl-g t": "pane::ActivateNextItem",
|
|
||||||
"ctrl-w g shift-t": "pane::ActivatePrevItem",
|
|
||||||
"ctrl-w ctrl-g shift-t": "pane::ActivatePrevItem",
|
|
||||||
"ctrl-w w": "workspace::ActivateNextPane",
|
|
||||||
"ctrl-w ctrl-w": "workspace::ActivateNextPane",
|
|
||||||
"ctrl-w p": "workspace::ActivatePreviousPane",
|
|
||||||
"ctrl-w ctrl-p": "workspace::ActivatePreviousPane",
|
|
||||||
"ctrl-w shift-w": "workspace::ActivatePreviousPane",
|
|
||||||
"ctrl-w ctrl-shift-w": "workspace::ActivatePreviousPane",
|
|
||||||
"ctrl-w v": "pane::SplitVertical",
|
|
||||||
"ctrl-w ctrl-v": "pane::SplitVertical",
|
|
||||||
"ctrl-w s": "pane::SplitHorizontal",
|
|
||||||
"ctrl-w shift-s": "pane::SplitHorizontal",
|
|
||||||
"ctrl-w ctrl-s": "pane::SplitHorizontal",
|
|
||||||
"ctrl-w c": "pane::CloseAllItems",
|
|
||||||
"ctrl-w ctrl-c": "pane::CloseAllItems",
|
|
||||||
"ctrl-w q": "pane::CloseAllItems",
|
|
||||||
"ctrl-w ctrl-q": "pane::CloseAllItems",
|
|
||||||
"ctrl-w o": "workspace::CloseInactiveTabsAndPanes",
|
|
||||||
"ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes",
|
|
||||||
"ctrl-w n": "workspace::NewFileSplitHorizontal",
|
|
||||||
"ctrl-w ctrl-n": "workspace::NewFileSplitHorizontal",
|
|
||||||
"ctrl-w d": "editor::GoToDefinitionSplit",
|
"ctrl-w d": "editor::GoToDefinitionSplit",
|
||||||
"ctrl-w g d": "editor::GoToDefinitionSplit",
|
"ctrl-w g d": "editor::GoToDefinitionSplit",
|
||||||
"ctrl-w shift-d": "editor::GoToTypeDefinitionSplit",
|
"ctrl-w shift-d": "editor::GoToTypeDefinitionSplit",
|
||||||
@@ -339,6 +294,10 @@
|
|||||||
"ctrl-t": "vim::Indent",
|
"ctrl-t": "vim::Indent",
|
||||||
"ctrl-d": "vim::Outdent",
|
"ctrl-d": "vim::Outdent",
|
||||||
"ctrl-k": ["vim::PushOperator", { "Digraph": {} }],
|
"ctrl-k": ["vim::PushOperator", { "Digraph": {} }],
|
||||||
|
"ctrl-v": ["vim::PushOperator", { "Literal": {} }],
|
||||||
|
"ctrl-shift-v": "editor::Paste", // note: this is *very* similar to ctrl-v in vim, but ctrl-shift-v on linux is the typical shortcut for paste when ctrl-v is already in use.
|
||||||
|
"ctrl-q": ["vim::PushOperator", { "Literal": {} }],
|
||||||
|
"ctrl-shift-q": ["vim::PushOperator", { "Literal": {} }],
|
||||||
"ctrl-r": ["vim::PushOperator", "Register"],
|
"ctrl-r": ["vim::PushOperator", "Register"],
|
||||||
"insert": "vim::ToggleReplace"
|
"insert": "vim::ToggleReplace"
|
||||||
}
|
}
|
||||||
@@ -357,6 +316,10 @@
|
|||||||
"ctrl-c": "vim::NormalBefore",
|
"ctrl-c": "vim::NormalBefore",
|
||||||
"ctrl-[": "vim::NormalBefore",
|
"ctrl-[": "vim::NormalBefore",
|
||||||
"ctrl-k": ["vim::PushOperator", { "Digraph": {} }],
|
"ctrl-k": ["vim::PushOperator", { "Digraph": {} }],
|
||||||
|
"ctrl-v": ["vim::PushOperator", { "Literal": {} }],
|
||||||
|
"ctrl-shift-v": "editor::Paste", // note: this is *very* similar to ctrl-v in vim, but ctrl-shift-v on linux is the typical shortcut for paste when ctrl-v is already in use.
|
||||||
|
"ctrl-q": ["vim::PushOperator", { "Literal": {} }],
|
||||||
|
"ctrl-shift-q": ["vim::PushOperator", { "Literal": {} }],
|
||||||
"backspace": "vim::UndoReplace",
|
"backspace": "vim::UndoReplace",
|
||||||
"tab": "vim::Tab",
|
"tab": "vim::Tab",
|
||||||
"enter": "vim::Enter",
|
"enter": "vim::Enter",
|
||||||
@@ -371,7 +334,9 @@
|
|||||||
"escape": "vim::ClearOperators",
|
"escape": "vim::ClearOperators",
|
||||||
"ctrl-c": "vim::ClearOperators",
|
"ctrl-c": "vim::ClearOperators",
|
||||||
"ctrl-[": "vim::ClearOperators",
|
"ctrl-[": "vim::ClearOperators",
|
||||||
"ctrl-k": ["vim::PushOperator", { "Digraph": {} }]
|
"ctrl-k": ["vim::PushOperator", { "Digraph": {} }],
|
||||||
|
"ctrl-v": ["vim::PushOperator", { "Literal": {} }],
|
||||||
|
"ctrl-q": ["vim::PushOperator", { "Literal": {} }]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -485,6 +450,49 @@
|
|||||||
"c": "vim::CurrentLine"
|
"c": "vim::CurrentLine"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"context": "vim_mode == literal",
|
||||||
|
"bindings": {
|
||||||
|
"ctrl-@": ["vim::Literal", ["ctrl-@", "\u0000"]],
|
||||||
|
"ctrl-a": ["vim::Literal", ["ctrl-a", "\u0001"]],
|
||||||
|
"ctrl-b": ["vim::Literal", ["ctrl-b", "\u0002"]],
|
||||||
|
"ctrl-c": ["vim::Literal", ["ctrl-c", "\u0003"]],
|
||||||
|
"ctrl-d": ["vim::Literal", ["ctrl-d", "\u0004"]],
|
||||||
|
"ctrl-e": ["vim::Literal", ["ctrl-e", "\u0005"]],
|
||||||
|
"ctrl-f": ["vim::Literal", ["ctrl-f", "\u0006"]],
|
||||||
|
"ctrl-g": ["vim::Literal", ["ctrl-g", "\u0007"]],
|
||||||
|
"ctrl-h": ["vim::Literal", ["ctrl-h", "\u0008"]],
|
||||||
|
"ctrl-i": ["vim::Literal", ["ctrl-i", "\u0009"]],
|
||||||
|
"ctrl-j": ["vim::Literal", ["ctrl-j", "\u000A"]],
|
||||||
|
"ctrl-k": ["vim::Literal", ["ctrl-k", "\u000B"]],
|
||||||
|
"ctrl-l": ["vim::Literal", ["ctrl-l", "\u000C"]],
|
||||||
|
"ctrl-m": ["vim::Literal", ["ctrl-m", "\u000D"]],
|
||||||
|
"ctrl-n": ["vim::Literal", ["ctrl-n", "\u000E"]],
|
||||||
|
"ctrl-o": ["vim::Literal", ["ctrl-o", "\u000F"]],
|
||||||
|
"ctrl-p": ["vim::Literal", ["ctrl-p", "\u0010"]],
|
||||||
|
"ctrl-q": ["vim::Literal", ["ctrl-q", "\u0011"]],
|
||||||
|
"ctrl-r": ["vim::Literal", ["ctrl-r", "\u0012"]],
|
||||||
|
"ctrl-s": ["vim::Literal", ["ctrl-s", "\u0013"]],
|
||||||
|
"ctrl-t": ["vim::Literal", ["ctrl-t", "\u0014"]],
|
||||||
|
"ctrl-u": ["vim::Literal", ["ctrl-u", "\u0015"]],
|
||||||
|
"ctrl-v": ["vim::Literal", ["ctrl-v", "\u0016"]],
|
||||||
|
"ctrl-w": ["vim::Literal", ["ctrl-w", "\u0017"]],
|
||||||
|
"ctrl-x": ["vim::Literal", ["ctrl-x", "\u0018"]],
|
||||||
|
"ctrl-y": ["vim::Literal", ["ctrl-y", "\u0019"]],
|
||||||
|
"ctrl-z": ["vim::Literal", ["ctrl-z", "\u001A"]],
|
||||||
|
"ctrl-[": ["vim::Literal", ["ctrl-[", "\u001B"]],
|
||||||
|
"ctrl-\\": ["vim::Literal", ["ctrl-\\", "\u001C"]],
|
||||||
|
"ctrl-]": ["vim::Literal", ["ctrl-]", "\u001D"]],
|
||||||
|
"ctrl-^": ["vim::Literal", ["ctrl-^", "\u001E"]],
|
||||||
|
"ctrl-_": ["vim::Literal", ["ctrl-_", "\u001F"]],
|
||||||
|
"escape": ["vim::Literal", ["escape", "\u001B"]],
|
||||||
|
"enter": ["vim::Literal", ["enter", "\u000D"]],
|
||||||
|
"tab": ["vim::Literal", ["tab", "\u0009"]],
|
||||||
|
// zed extensions:
|
||||||
|
"backspace": ["vim::Literal", ["backspace", "\u0008"]],
|
||||||
|
"delete": ["vim::Literal", ["delete", "\u007F"]]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"context": "BufferSearchBar && !in_replace",
|
"context": "BufferSearchBar && !in_replace",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
@@ -493,7 +501,57 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "EmptyPane || SharedScreen",
|
"context": "ProjectPanel || CollabPanel || OutlinePanel || ChatPanel || VimControl || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView",
|
||||||
|
"bindings": {
|
||||||
|
// window related commands (ctrl-w X)
|
||||||
|
"ctrl-w": null,
|
||||||
|
"ctrl-w left": ["workspace::ActivatePaneInDirection", "Left"],
|
||||||
|
"ctrl-w right": ["workspace::ActivatePaneInDirection", "Right"],
|
||||||
|
"ctrl-w up": ["workspace::ActivatePaneInDirection", "Up"],
|
||||||
|
"ctrl-w down": ["workspace::ActivatePaneInDirection", "Down"],
|
||||||
|
"ctrl-w h": ["workspace::ActivatePaneInDirection", "Left"],
|
||||||
|
"ctrl-w l": ["workspace::ActivatePaneInDirection", "Right"],
|
||||||
|
"ctrl-w k": ["workspace::ActivatePaneInDirection", "Up"],
|
||||||
|
"ctrl-w j": ["workspace::ActivatePaneInDirection", "Down"],
|
||||||
|
"ctrl-w ctrl-h": ["workspace::ActivatePaneInDirection", "Left"],
|
||||||
|
"ctrl-w ctrl-l": ["workspace::ActivatePaneInDirection", "Right"],
|
||||||
|
"ctrl-w ctrl-k": ["workspace::ActivatePaneInDirection", "Up"],
|
||||||
|
"ctrl-w ctrl-j": ["workspace::ActivatePaneInDirection", "Down"],
|
||||||
|
"ctrl-w shift-left": ["workspace::SwapPaneInDirection", "Left"],
|
||||||
|
"ctrl-w shift-right": ["workspace::SwapPaneInDirection", "Right"],
|
||||||
|
"ctrl-w shift-up": ["workspace::SwapPaneInDirection", "Up"],
|
||||||
|
"ctrl-w shift-down": ["workspace::SwapPaneInDirection", "Down"],
|
||||||
|
"ctrl-w shift-h": ["workspace::SwapPaneInDirection", "Left"],
|
||||||
|
"ctrl-w shift-l": ["workspace::SwapPaneInDirection", "Right"],
|
||||||
|
"ctrl-w shift-k": ["workspace::SwapPaneInDirection", "Up"],
|
||||||
|
"ctrl-w shift-j": ["workspace::SwapPaneInDirection", "Down"],
|
||||||
|
"ctrl-w g t": "pane::ActivateNextItem",
|
||||||
|
"ctrl-w ctrl-g t": "pane::ActivateNextItem",
|
||||||
|
"ctrl-w g shift-t": "pane::ActivatePrevItem",
|
||||||
|
"ctrl-w ctrl-g shift-t": "pane::ActivatePrevItem",
|
||||||
|
"ctrl-w w": "workspace::ActivateNextPane",
|
||||||
|
"ctrl-w ctrl-w": "workspace::ActivateNextPane",
|
||||||
|
"ctrl-w p": "workspace::ActivatePreviousPane",
|
||||||
|
"ctrl-w ctrl-p": "workspace::ActivatePreviousPane",
|
||||||
|
"ctrl-w shift-w": "workspace::ActivatePreviousPane",
|
||||||
|
"ctrl-w ctrl-shift-w": "workspace::ActivatePreviousPane",
|
||||||
|
"ctrl-w v": "pane::SplitVertical",
|
||||||
|
"ctrl-w ctrl-v": "pane::SplitVertical",
|
||||||
|
"ctrl-w s": "pane::SplitHorizontal",
|
||||||
|
"ctrl-w shift-s": "pane::SplitHorizontal",
|
||||||
|
"ctrl-w ctrl-s": "pane::SplitHorizontal",
|
||||||
|
"ctrl-w c": "pane::CloseAllItems",
|
||||||
|
"ctrl-w ctrl-c": "pane::CloseAllItems",
|
||||||
|
"ctrl-w q": "pane::CloseAllItems",
|
||||||
|
"ctrl-w ctrl-q": "pane::CloseAllItems",
|
||||||
|
"ctrl-w o": "workspace::CloseInactiveTabsAndPanes",
|
||||||
|
"ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes",
|
||||||
|
"ctrl-w n": "workspace::NewFileSplitHorizontal",
|
||||||
|
"ctrl-w ctrl-n": "workspace::NewFileSplitHorizontal"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "EmptyPane || SharedScreen || MarkdownPreview || KeyContextView",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
":": "command_palette::Toggle",
|
":": "command_palette::Toggle",
|
||||||
"g /": "pane::DeploySearch"
|
"g /": "pane::DeploySearch"
|
||||||
|
|||||||
@@ -88,7 +88,6 @@ origin: (f64, f64),
|
|||||||
|
|
||||||
<edit>
|
<edit>
|
||||||
<path>src/shapes/rectangle.rs</path>
|
<path>src/shapes/rectangle.rs</path>
|
||||||
<description>Update the Rectangle's new function to take an origin parameter</description>
|
|
||||||
<operation>update</operation>
|
<operation>update</operation>
|
||||||
<old_text>
|
<old_text>
|
||||||
fn new(width: f64, height: f64) -> Self {
|
fn new(width: f64, height: f64) -> Self {
|
||||||
@@ -117,7 +116,6 @@ pub struct Circle {
|
|||||||
|
|
||||||
<edit>
|
<edit>
|
||||||
<path>src/shapes/circle.rs</path>
|
<path>src/shapes/circle.rs</path>
|
||||||
<description>Update the Circle's new function to take an origin parameter</description>
|
|
||||||
<operation>update</operation>
|
<operation>update</operation>
|
||||||
<old_text>
|
<old_text>
|
||||||
fn new(radius: f64) -> Self {
|
fn new(radius: f64) -> Self {
|
||||||
@@ -134,7 +132,6 @@ fn new(origin: (f64, f64), radius: f64) -> Self {
|
|||||||
|
|
||||||
<edit>
|
<edit>
|
||||||
<path>src/shapes/rectangle.rs</path>
|
<path>src/shapes/rectangle.rs</path>
|
||||||
<description>Add an import for the std::fmt module</description>
|
|
||||||
<operation>insert_before</operation>
|
<operation>insert_before</operation>
|
||||||
<old_text>
|
<old_text>
|
||||||
struct Rectangle {
|
struct Rectangle {
|
||||||
@@ -147,7 +144,10 @@ use std::fmt;
|
|||||||
|
|
||||||
<edit>
|
<edit>
|
||||||
<path>src/shapes/rectangle.rs</path>
|
<path>src/shapes/rectangle.rs</path>
|
||||||
<description>Add a Display implementation for Rectangle</description>
|
<description>
|
||||||
|
Add a manual Display implementation for Rectangle.
|
||||||
|
Currently, this is the same as a derived Display implementation.
|
||||||
|
</description>
|
||||||
<operation>insert_after</operation>
|
<operation>insert_after</operation>
|
||||||
<old_text>
|
<old_text>
|
||||||
Rectangle { width, height }
|
Rectangle { width, height }
|
||||||
@@ -169,7 +169,6 @@ impl fmt::Display for Rectangle {
|
|||||||
|
|
||||||
<edit>
|
<edit>
|
||||||
<path>src/shapes/circle.rs</path>
|
<path>src/shapes/circle.rs</path>
|
||||||
<description>Add an import for the `std::fmt` module</description>
|
|
||||||
<operation>insert_before</operation>
|
<operation>insert_before</operation>
|
||||||
<old_text>
|
<old_text>
|
||||||
struct Circle {
|
struct Circle {
|
||||||
@@ -181,7 +180,6 @@ use std::fmt;
|
|||||||
|
|
||||||
<edit>
|
<edit>
|
||||||
<path>src/shapes/circle.rs</path>
|
<path>src/shapes/circle.rs</path>
|
||||||
<description>Add a Display implementation for Circle</description>
|
|
||||||
<operation>insert_after</operation>
|
<operation>insert_after</operation>
|
||||||
<old_text>
|
<old_text>
|
||||||
Circle { radius }
|
Circle { radius }
|
||||||
@@ -369,6 +369,17 @@
|
|||||||
/// 5. Never show the scrollbar:
|
/// 5. Never show the scrollbar:
|
||||||
/// "never"
|
/// "never"
|
||||||
"show": null
|
"show": null
|
||||||
|
},
|
||||||
|
// Settings related to indent guides in the project panel.
|
||||||
|
"indent_guides": {
|
||||||
|
// When to show indent guides in the project panel.
|
||||||
|
// This setting can take two values:
|
||||||
|
//
|
||||||
|
// 1. Always show indent guides:
|
||||||
|
// "always"
|
||||||
|
// 2. Never show indent guides:
|
||||||
|
// "never"
|
||||||
|
"show": "always"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"outline_panel": {
|
"outline_panel": {
|
||||||
@@ -392,7 +403,35 @@
|
|||||||
"auto_reveal_entries": true,
|
"auto_reveal_entries": true,
|
||||||
/// Whether to fold directories automatically
|
/// Whether to fold directories automatically
|
||||||
/// when a directory has only one directory inside.
|
/// when a directory has only one directory inside.
|
||||||
"auto_fold_dirs": true
|
"auto_fold_dirs": true,
|
||||||
|
// Settings related to indent guides in the outline panel.
|
||||||
|
"indent_guides": {
|
||||||
|
// When to show indent guides in the outline panel.
|
||||||
|
// This setting can take two values:
|
||||||
|
//
|
||||||
|
// 1. Always show indent guides:
|
||||||
|
// "always"
|
||||||
|
// 2. Never show indent guides:
|
||||||
|
// "never"
|
||||||
|
"show": "always"
|
||||||
|
},
|
||||||
|
/// Scrollbar-related settings
|
||||||
|
"scrollbar": {
|
||||||
|
/// When to show the scrollbar in the project panel.
|
||||||
|
/// This setting can take four values:
|
||||||
|
///
|
||||||
|
/// 1. null (default): Inherit editor settings
|
||||||
|
/// 2. Show the scrollbar if there's important information or
|
||||||
|
/// follow the system's configured behavior (default):
|
||||||
|
/// "auto"
|
||||||
|
/// 3. Match the system's configured behavior:
|
||||||
|
/// "system"
|
||||||
|
/// 4. Always show the scrollbar:
|
||||||
|
/// "always"
|
||||||
|
/// 5. Never show the scrollbar:
|
||||||
|
/// "never"
|
||||||
|
"show": null
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"collaboration_panel": {
|
"collaboration_panel": {
|
||||||
// Whether to show the collaboration panel button in the status bar.
|
// Whether to show the collaboration panel button in the status bar.
|
||||||
@@ -613,6 +652,12 @@
|
|||||||
// Sets a delay after which the inline blame information is shown.
|
// Sets a delay after which the inline blame information is shown.
|
||||||
// Delay is restarted with every cursor movement.
|
// Delay is restarted with every cursor movement.
|
||||||
// "delay_ms": 600
|
// "delay_ms": 600
|
||||||
|
//
|
||||||
|
// Whether or not do display the git commit summary on the same line.
|
||||||
|
// "show_commit_summary": false
|
||||||
|
//
|
||||||
|
// The minimum column number to show the inline blame information at
|
||||||
|
// "min_column": 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Configuration for how direnv configuration should be loaded. May take 2 values:
|
// Configuration for how direnv configuration should be loaded. May take 2 values:
|
||||||
@@ -775,6 +820,7 @@
|
|||||||
"tasks": {
|
"tasks": {
|
||||||
"variables": {}
|
"variables": {}
|
||||||
},
|
},
|
||||||
|
"toolchain": { "name": "default", "path": "default" },
|
||||||
// An object whose keys are language names, and whose values
|
// An object whose keys are language names, and whose values
|
||||||
// are arrays of filenames or extensions of files that should
|
// are arrays of filenames or extensions of files that should
|
||||||
// use those languages.
|
// use those languages.
|
||||||
@@ -803,7 +849,7 @@
|
|||||||
/// You can override this to use a version of node that is not in $PATH with:
|
/// You can override this to use a version of node that is not in $PATH with:
|
||||||
/// {
|
/// {
|
||||||
/// "node": {
|
/// "node": {
|
||||||
/// "node_path": "/path/to/node"
|
/// "path": "/path/to/node"
|
||||||
/// "npm_path": "/path/to/npm" (defaults to node_path/../npm)
|
/// "npm_path": "/path/to/npm" (defaults to node_path/../npm)
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
@@ -1099,13 +1145,13 @@
|
|||||||
// }
|
// }
|
||||||
"command_aliases": {},
|
"command_aliases": {},
|
||||||
// ssh_connections is an array of ssh connections.
|
// ssh_connections is an array of ssh connections.
|
||||||
// By default this setting is null, which disables the direct ssh connection support.
|
|
||||||
// You can configure these from `project: Open Remote` in the command palette.
|
// You can configure these from `project: Open Remote` in the command palette.
|
||||||
// Zed's ssh support will pull configuration from your ~/.ssh too.
|
// Zed's ssh support will pull configuration from your ~/.ssh too.
|
||||||
// Examples:
|
// Examples:
|
||||||
// [
|
// [
|
||||||
// {
|
// {
|
||||||
// "host": "example-box",
|
// "host": "example-box",
|
||||||
|
// // "port": 22, "username": "test", "args": ["-i", "/home/user/.ssh/id_rsa"]
|
||||||
// "projects": [
|
// "projects": [
|
||||||
// {
|
// {
|
||||||
// "paths": ["/home/user/code/zed"]
|
// "paths": ["/home/user/code/zed"]
|
||||||
@@ -1113,7 +1159,7 @@
|
|||||||
// ]
|
// ]
|
||||||
// }
|
// }
|
||||||
// ]
|
// ]
|
||||||
"ssh_connections": null,
|
"ssh_connections": [],
|
||||||
// Configures the Context Server Protocol binaries
|
// Configures the Context Server Protocol binaries
|
||||||
//
|
//
|
||||||
// Examples:
|
// Examples:
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"allow_concurrent_runs": false,
|
"allow_concurrent_runs": false,
|
||||||
// What to do with the terminal pane and tab, after the command was started:
|
// What to do with the terminal pane and tab, after the command was started:
|
||||||
// * `always` — always show the terminal pane, add and focus the corresponding task's tab in it (default)
|
// * `always` — always show the terminal pane, add and focus the corresponding task's tab in it (default)
|
||||||
|
// * `no_focus` — always show the terminal pane, add/reuse the task's tab there, but don't focus it
|
||||||
// * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there
|
// * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there
|
||||||
"reveal": "always",
|
"reveal": "always",
|
||||||
// What to do with the terminal pane and tab, after the command had finished:
|
// What to do with the terminal pane and tab, after the command had finished:
|
||||||
|
|||||||
@@ -16,13 +16,14 @@ doctest = false
|
|||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
auto_update.workspace = true
|
auto_update.workspace = true
|
||||||
editor.workspace = true
|
editor.workspace = true
|
||||||
extension.workspace = true
|
extension_host.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
language.workspace = true
|
language.workspace = true
|
||||||
project.workspace = true
|
project.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
|
util.workspace = true
|
||||||
workspace.workspace = true
|
workspace.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage};
|
use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage};
|
||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
use extension::ExtensionStore;
|
use extension_host::ExtensionStore;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, percentage, Animation, AnimationExt as _, AppContext, CursorStyle, EventEmitter,
|
actions, percentage, Animation, AnimationExt as _, AppContext, CursorStyle, EventEmitter,
|
||||||
@@ -13,7 +13,8 @@ use language::{
|
|||||||
use project::{EnvironmentErrorMessage, LanguageServerProgress, Project, WorktreeId};
|
use project::{EnvironmentErrorMessage, LanguageServerProgress, Project, WorktreeId};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::{cmp::Reverse, fmt::Write, sync::Arc, time::Duration};
|
use std::{cmp::Reverse, fmt::Write, sync::Arc, time::Duration};
|
||||||
use ui::{prelude::*, ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle};
|
use ui::{prelude::*, ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip};
|
||||||
|
use util::truncate_and_trailoff;
|
||||||
use workspace::{item::ItemHandle, StatusItemView, Workspace};
|
use workspace::{item::ItemHandle, StatusItemView, Workspace};
|
||||||
|
|
||||||
actions!(activity_indicator, [ShowErrorMessage]);
|
actions!(activity_indicator, [ShowErrorMessage]);
|
||||||
@@ -351,7 +352,10 @@ impl ActivityIndicator {
|
|||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
),
|
),
|
||||||
message: format!("Formatting failed: {}. Click to see logs.", failure),
|
message: format!("Formatting failed: {}. Click to see logs.", failure),
|
||||||
on_click: Some(Arc::new(|_, cx| {
|
on_click: Some(Arc::new(|indicator, cx| {
|
||||||
|
indicator.project.update(cx, |project, cx| {
|
||||||
|
project.reset_last_formatting_failure(cx);
|
||||||
|
});
|
||||||
cx.dispatch_action(Box::new(workspace::OpenLog));
|
cx.dispatch_action(Box::new(workspace::OpenLog));
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
@@ -446,6 +450,8 @@ impl ActivityIndicator {
|
|||||||
|
|
||||||
impl EventEmitter<Event> for ActivityIndicator {}
|
impl EventEmitter<Event> for ActivityIndicator {}
|
||||||
|
|
||||||
|
const MAX_MESSAGE_LEN: usize = 50;
|
||||||
|
|
||||||
impl Render for ActivityIndicator {
|
impl Render for ActivityIndicator {
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
let result = h_flex()
|
let result = h_flex()
|
||||||
@@ -456,6 +462,7 @@ impl Render for ActivityIndicator {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
let this = cx.view().downgrade();
|
let this = cx.view().downgrade();
|
||||||
|
let truncate_content = content.message.len() > MAX_MESSAGE_LEN;
|
||||||
result.gap_2().child(
|
result.gap_2().child(
|
||||||
PopoverMenu::new("activity-indicator-popover")
|
PopoverMenu::new("activity-indicator-popover")
|
||||||
.trigger(
|
.trigger(
|
||||||
@@ -464,7 +471,21 @@ impl Render for ActivityIndicator {
|
|||||||
.id("activity-indicator-status")
|
.id("activity-indicator-status")
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.children(content.icon)
|
.children(content.icon)
|
||||||
.child(Label::new(content.message).size(LabelSize::Small))
|
.map(|button| {
|
||||||
|
if truncate_content {
|
||||||
|
button
|
||||||
|
.child(
|
||||||
|
Label::new(truncate_and_trailoff(
|
||||||
|
&content.message,
|
||||||
|
MAX_MESSAGE_LEN,
|
||||||
|
))
|
||||||
|
.size(LabelSize::Small),
|
||||||
|
)
|
||||||
|
.tooltip(move |cx| Tooltip::text(&content.message, cx))
|
||||||
|
} else {
|
||||||
|
button.child(Label::new(content.message).size(LabelSize::Small))
|
||||||
|
}
|
||||||
|
})
|
||||||
.when_some(content.on_click, |this, handler| {
|
.when_some(content.on_click, |this, handler| {
|
||||||
this.on_click(cx.listener(move |this, _, cx| {
|
this.on_click(cx.listener(move |this, _, cx| {
|
||||||
handler(this, cx);
|
handler(this, cx);
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ pub struct AnthropicModelCacheConfiguration {
|
|||||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
|
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
|
||||||
pub enum Model {
|
pub enum Model {
|
||||||
#[default]
|
#[default]
|
||||||
#[serde(rename = "claude-3-5-sonnet", alias = "claude-3-5-sonnet-20240620")]
|
#[serde(rename = "claude-3-5-sonnet", alias = "claude-3-5-sonnet-latest")]
|
||||||
Claude3_5Sonnet,
|
Claude3_5Sonnet,
|
||||||
#[serde(rename = "claude-3-opus", alias = "claude-3-opus-20240229")]
|
#[serde(rename = "claude-3-opus", alias = "claude-3-opus-latest")]
|
||||||
Claude3Opus,
|
Claude3Opus,
|
||||||
#[serde(rename = "claude-3-sonnet", alias = "claude-3-sonnet-20240229")]
|
#[serde(rename = "claude-3-sonnet", alias = "claude-3-sonnet-latest")]
|
||||||
Claude3Sonnet,
|
Claude3Sonnet,
|
||||||
#[serde(rename = "claude-3-haiku", alias = "claude-3-haiku-20240307")]
|
#[serde(rename = "claude-3-haiku", alias = "claude-3-haiku-latest")]
|
||||||
Claude3Haiku,
|
Claude3Haiku,
|
||||||
#[serde(rename = "custom")]
|
#[serde(rename = "custom")]
|
||||||
Custom {
|
Custom {
|
||||||
@@ -69,10 +69,10 @@ impl Model {
|
|||||||
|
|
||||||
pub fn id(&self) -> &str {
|
pub fn id(&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
Model::Claude3_5Sonnet => "claude-3-5-sonnet-20240620",
|
Model::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
|
||||||
Model::Claude3Opus => "claude-3-opus-20240229",
|
Model::Claude3Opus => "claude-3-opus-latest",
|
||||||
Model::Claude3Sonnet => "claude-3-sonnet-20240229",
|
Model::Claude3Sonnet => "claude-3-sonnet-latest",
|
||||||
Model::Claude3Haiku => "claude-3-haiku-20240307",
|
Model::Claude3Haiku => "claude-3-haiku-latest",
|
||||||
Self::Custom { name, .. } => name,
|
Self::Custom { name, .. } => name,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,24 +41,24 @@ use prompts::PromptLoadingParams;
|
|||||||
use semantic_index::{CloudEmbeddingProvider, SemanticDb};
|
use semantic_index::{CloudEmbeddingProvider, SemanticDb};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::{update_settings_file, Settings, SettingsStore};
|
use settings::{update_settings_file, Settings, SettingsStore};
|
||||||
use slash_command::workflow_command::WorkflowSlashCommand;
|
|
||||||
use slash_command::{
|
use slash_command::{
|
||||||
auto_command, cargo_workspace_command, context_server_command, default_command, delta_command,
|
auto_command, cargo_workspace_command, context_server_command, default_command, delta_command,
|
||||||
diagnostics_command, docs_command, fetch_command, file_command, now_command, project_command,
|
diagnostics_command, docs_command, fetch_command, file_command, now_command, project_command,
|
||||||
prompt_command, search_command, symbols_command, tab_command, terminal_command,
|
prompt_command, search_command, symbols_command, tab_command, terminal_command,
|
||||||
workflow_command,
|
|
||||||
};
|
};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
pub(crate) use streaming_diff::*;
|
pub(crate) use streaming_diff::*;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
|
||||||
|
use crate::slash_command::streaming_example_command;
|
||||||
use crate::slash_command_settings::SlashCommandSettings;
|
use crate::slash_command_settings::SlashCommandSettings;
|
||||||
|
|
||||||
actions!(
|
actions!(
|
||||||
assistant,
|
assistant,
|
||||||
[
|
[
|
||||||
Assist,
|
Assist,
|
||||||
|
Edit,
|
||||||
Split,
|
Split,
|
||||||
CopyCode,
|
CopyCode,
|
||||||
CycleMessageRole,
|
CycleMessageRole,
|
||||||
@@ -298,25 +298,64 @@ fn register_context_server_handlers(cx: &mut AppContext) {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(prompts) = protocol.list_prompts().await.log_err() {
|
if protocol.capable(context_servers::protocol::ServerCapability::Prompts) {
|
||||||
for prompt in prompts
|
if let Some(prompts) = protocol.list_prompts().await.log_err() {
|
||||||
.into_iter()
|
for prompt in prompts
|
||||||
.filter(context_server_command::acceptable_prompt)
|
.into_iter()
|
||||||
{
|
.filter(context_server_command::acceptable_prompt)
|
||||||
log::info!(
|
{
|
||||||
"registering context server command: {:?}",
|
log::info!(
|
||||||
prompt.name
|
"registering context server command: {:?}",
|
||||||
);
|
prompt.name
|
||||||
context_server_registry.register_command(
|
);
|
||||||
server.id.clone(),
|
context_server_registry.register_command(
|
||||||
prompt.name.as_str(),
|
server.id.clone(),
|
||||||
);
|
prompt.name.as_str(),
|
||||||
slash_command_registry.register_command(
|
);
|
||||||
context_server_command::ContextServerSlashCommand::new(
|
slash_command_registry.register_command(
|
||||||
&server, prompt,
|
context_server_command::ContextServerSlashCommand::new(
|
||||||
),
|
&server, prompt,
|
||||||
true,
|
),
|
||||||
);
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
cx.update_model(
|
||||||
|
&manager,
|
||||||
|
|manager: &mut context_servers::manager::ContextServerManager, cx| {
|
||||||
|
let tool_registry = ToolRegistry::global(cx);
|
||||||
|
let context_server_registry = ContextServerRegistry::global(cx);
|
||||||
|
if let Some(server) = manager.get_server(server_id) {
|
||||||
|
cx.spawn(|_, _| async move {
|
||||||
|
let Some(protocol) = server.client.read().clone() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if protocol.capable(context_servers::protocol::ServerCapability::Tools) {
|
||||||
|
if let Some(tools) = protocol.list_tools().await.log_err() {
|
||||||
|
for tool in tools.tools {
|
||||||
|
log::info!(
|
||||||
|
"registering context server tool: {:?}",
|
||||||
|
tool.name
|
||||||
|
);
|
||||||
|
context_server_registry.register_tool(
|
||||||
|
server.id.clone(),
|
||||||
|
tool.name.as_str(),
|
||||||
|
);
|
||||||
|
tool_registry.register_tool(
|
||||||
|
tools::context_server_tool::ContextServerTool::new(
|
||||||
|
server.id.clone(),
|
||||||
|
tool
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -334,6 +373,14 @@ fn register_context_server_handlers(cx: &mut AppContext) {
|
|||||||
context_server_registry.unregister_command(&server_id, &command_name);
|
context_server_registry.unregister_command(&server_id, &command_name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(tools) = context_server_registry.get_tools(server_id) {
|
||||||
|
let tool_registry = ToolRegistry::global(cx);
|
||||||
|
for tool_name in tools {
|
||||||
|
tool_registry.unregister_tool_by_name(&tool_name);
|
||||||
|
context_server_registry.unregister_tool(&server_id, &tool_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -397,22 +444,6 @@ fn register_slash_commands(prompt_builder: Option<Arc<PromptBuilder>>, cx: &mut
|
|||||||
slash_command_registry.register_command(fetch_command::FetchSlashCommand, false);
|
slash_command_registry.register_command(fetch_command::FetchSlashCommand, false);
|
||||||
|
|
||||||
if let Some(prompt_builder) = prompt_builder {
|
if let Some(prompt_builder) = prompt_builder {
|
||||||
cx.observe_global::<SettingsStore>({
|
|
||||||
let slash_command_registry = slash_command_registry.clone();
|
|
||||||
let prompt_builder = prompt_builder.clone();
|
|
||||||
move |cx| {
|
|
||||||
if AssistantSettings::get_global(cx).are_live_diffs_enabled(cx) {
|
|
||||||
slash_command_registry.register_command(
|
|
||||||
workflow_command::WorkflowSlashCommand::new(prompt_builder.clone()),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
slash_command_registry.unregister_command_by_name(WorkflowSlashCommand::NAME);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
cx.observe_flag::<project_command::ProjectSlashCommandFeatureFlag, _>({
|
cx.observe_flag::<project_command::ProjectSlashCommandFeatureFlag, _>({
|
||||||
let slash_command_registry = slash_command_registry.clone();
|
let slash_command_registry = slash_command_registry.clone();
|
||||||
move |is_enabled, _cx| {
|
move |is_enabled, _cx| {
|
||||||
@@ -438,6 +469,19 @@ fn register_slash_commands(prompt_builder: Option<Arc<PromptBuilder>>, cx: &mut
|
|||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
|
cx.observe_flag::<streaming_example_command::StreamingExampleSlashCommandFeatureFlag, _>({
|
||||||
|
let slash_command_registry = slash_command_registry.clone();
|
||||||
|
move |is_enabled, _cx| {
|
||||||
|
if is_enabled {
|
||||||
|
slash_command_registry.register_command(
|
||||||
|
streaming_example_command::StreamingExampleSlashCommand,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
update_slash_commands_from_settings(cx);
|
update_slash_commands_from_settings(cx);
|
||||||
cx.observe_global::<SettingsStore>(update_slash_commands_from_settings)
|
cx.observe_global::<SettingsStore>(update_slash_commands_from_settings)
|
||||||
.detach();
|
.detach();
|
||||||
|
|||||||
@@ -13,10 +13,11 @@ use crate::{
|
|||||||
terminal_inline_assistant::TerminalInlineAssistant,
|
terminal_inline_assistant::TerminalInlineAssistant,
|
||||||
Assist, AssistantPatch, AssistantPatchStatus, CacheStatus, ConfirmCommand, Content, Context,
|
Assist, AssistantPatch, AssistantPatchStatus, CacheStatus, ConfirmCommand, Content, Context,
|
||||||
ContextEvent, ContextId, ContextStore, ContextStoreEvent, CopyCode, CycleMessageRole,
|
ContextEvent, ContextId, ContextStore, ContextStoreEvent, CopyCode, CycleMessageRole,
|
||||||
DeployHistory, DeployPromptLibrary, InlineAssistant, InsertDraggedFiles, InsertIntoEditor,
|
DeployHistory, DeployPromptLibrary, Edit, InlineAssistant, InsertDraggedFiles,
|
||||||
Message, MessageId, MessageMetadata, MessageStatus, ModelPickerDelegate, ModelSelector,
|
InsertIntoEditor, Message, MessageId, MessageMetadata, MessageStatus, ModelPickerDelegate,
|
||||||
NewContext, PendingSlashCommand, PendingSlashCommandStatus, QuoteSelection,
|
ModelSelector, NewContext, PendingSlashCommand, PendingSlashCommandStatus, QuoteSelection,
|
||||||
RemoteContextMetadata, SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector,
|
RemoteContextMetadata, RequestType, SavedContextMetadata, Split, ToggleFocus,
|
||||||
|
ToggleModelSelector,
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection};
|
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection};
|
||||||
@@ -26,8 +27,8 @@ use collections::{BTreeSet, HashMap, HashSet};
|
|||||||
use editor::{
|
use editor::{
|
||||||
actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt},
|
actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt},
|
||||||
display_map::{
|
display_map::{
|
||||||
BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, Crease,
|
BlockContext, BlockId, BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata,
|
||||||
CreaseMetadata, CustomBlockId, FoldId, RenderBlock, ToDisplayPoint,
|
CustomBlockId, FoldId, RenderBlock, ToDisplayPoint,
|
||||||
},
|
},
|
||||||
scroll::{Autoscroll, AutoscrollStrategy},
|
scroll::{Autoscroll, AutoscrollStrategy},
|
||||||
Anchor, Editor, EditorEvent, ProposedChangeLocation, ProposedChangesEditor, RowExt,
|
Anchor, Editor, EditorEvent, ProposedChangeLocation, ProposedChangesEditor, RowExt,
|
||||||
@@ -72,12 +73,11 @@ use std::{
|
|||||||
};
|
};
|
||||||
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
|
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
|
||||||
use text::SelectionGoal;
|
use text::SelectionGoal;
|
||||||
use ui::TintColor;
|
|
||||||
use ui::{
|
use ui::{
|
||||||
prelude::*,
|
prelude::*,
|
||||||
utils::{format_distance_from_now, DateTimeType},
|
utils::{format_distance_from_now, DateTimeType},
|
||||||
Avatar, ButtonLike, ContextMenu, Disclosure, ElevationIndex, KeyBinding, ListItem,
|
Avatar, ButtonLike, ContextMenu, Disclosure, ElevationIndex, KeyBinding, ListItem,
|
||||||
ListItemSpacing, PopoverMenu, PopoverMenuHandle, Tooltip,
|
ListItemSpacing, PopoverMenu, PopoverMenuHandle, TintColor, Tooltip,
|
||||||
};
|
};
|
||||||
use util::{maybe, ResultExt};
|
use util::{maybe, ResultExt};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
@@ -356,8 +356,10 @@ impl AssistantPanel {
|
|||||||
let project = workspace.project().clone();
|
let project = workspace.project().clone();
|
||||||
pane.set_custom_drop_handle(cx, move |_, dropped_item, cx| {
|
pane.set_custom_drop_handle(cx, move |_, dropped_item, cx| {
|
||||||
let action = maybe!({
|
let action = maybe!({
|
||||||
if let Some(paths) = dropped_item.downcast_ref::<ExternalPaths>() {
|
if project.read(cx).is_local() {
|
||||||
return Some(InsertDraggedFiles::ExternalFiles(paths.paths().to_vec()));
|
if let Some(paths) = dropped_item.downcast_ref::<ExternalPaths>() {
|
||||||
|
return Some(InsertDraggedFiles::ExternalFiles(paths.paths().to_vec()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let project_paths = if let Some(tab) = dropped_item.downcast_ref::<DraggedTab>()
|
let project_paths = if let Some(tab) = dropped_item.downcast_ref::<DraggedTab>()
|
||||||
@@ -961,7 +963,7 @@ impl AssistantPanel {
|
|||||||
|
|
||||||
fn new_context(&mut self, cx: &mut ViewContext<Self>) -> Option<View<ContextEditor>> {
|
fn new_context(&mut self, cx: &mut ViewContext<Self>) -> Option<View<ContextEditor>> {
|
||||||
let project = self.project.read(cx);
|
let project = self.project.read(cx);
|
||||||
if project.is_via_collab() && project.dev_server_project_id().is_none() {
|
if project.is_via_collab() {
|
||||||
let task = self
|
let task = self
|
||||||
.context_store
|
.context_store
|
||||||
.update(cx, |store, cx| store.create_remote_context(cx));
|
.update(cx, |store, cx| store.create_remote_context(cx));
|
||||||
@@ -1459,6 +1461,7 @@ type MessageHeader = MessageMetadata;
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
enum AssistError {
|
enum AssistError {
|
||||||
|
FileRequired,
|
||||||
PaymentRequired,
|
PaymentRequired,
|
||||||
MaxMonthlySpendReached,
|
MaxMonthlySpendReached,
|
||||||
Message(SharedString),
|
Message(SharedString),
|
||||||
@@ -1586,23 +1589,11 @@ impl ContextEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
|
fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
|
||||||
let provider = LanguageModelRegistry::read_global(cx).active_provider();
|
self.send_to_model(RequestType::Chat, cx);
|
||||||
if provider
|
}
|
||||||
.as_ref()
|
|
||||||
.map_or(false, |provider| provider.must_accept_terms(cx))
|
|
||||||
{
|
|
||||||
self.show_accept_terms = true;
|
|
||||||
cx.notify();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.focus_active_patch(cx) {
|
fn edit(&mut self, _: &Edit, cx: &mut ViewContext<Self>) {
|
||||||
return;
|
self.send_to_model(RequestType::SuggestEdits, cx);
|
||||||
}
|
|
||||||
|
|
||||||
self.last_error = None;
|
|
||||||
self.send_to_model(cx);
|
|
||||||
cx.notify();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn focus_active_patch(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
fn focus_active_patch(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||||
@@ -1620,8 +1611,30 @@ impl ContextEditor {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_to_model(&mut self, cx: &mut ViewContext<Self>) {
|
fn send_to_model(&mut self, request_type: RequestType, cx: &mut ViewContext<Self>) {
|
||||||
if let Some(user_message) = self.context.update(cx, |context, cx| context.assist(cx)) {
|
let provider = LanguageModelRegistry::read_global(cx).active_provider();
|
||||||
|
if provider
|
||||||
|
.as_ref()
|
||||||
|
.map_or(false, |provider| provider.must_accept_terms(cx))
|
||||||
|
{
|
||||||
|
self.show_accept_terms = true;
|
||||||
|
cx.notify();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.focus_active_patch(cx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.last_error = None;
|
||||||
|
|
||||||
|
if request_type == RequestType::SuggestEdits && !self.context.read(cx).contains_files(cx) {
|
||||||
|
self.last_error = Some(AssistError::FileRequired);
|
||||||
|
cx.notify();
|
||||||
|
} else if let Some(user_message) = self
|
||||||
|
.context
|
||||||
|
.update(cx, |context, cx| context.assist(request_type, cx))
|
||||||
|
{
|
||||||
let new_selection = {
|
let new_selection = {
|
||||||
let cursor = user_message
|
let cursor = user_message
|
||||||
.start
|
.start
|
||||||
@@ -1638,6 +1651,8 @@ impl ContextEditor {
|
|||||||
// Avoid scrolling to the new cursor position so the assistant's output is stable.
|
// Avoid scrolling to the new cursor position so the assistant's output is stable.
|
||||||
cx.defer(|this, _| this.scroll_position = None);
|
cx.defer(|this, _| this.scroll_position = None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
|
fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
|
||||||
@@ -1665,8 +1680,10 @@ impl ContextEditor {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cursors(&self, cx: &AppContext) -> Vec<usize> {
|
fn cursors(&self, cx: &mut WindowContext) -> Vec<usize> {
|
||||||
let selections = self.editor.read(cx).selections.all::<usize>(cx);
|
let selections = self
|
||||||
|
.editor
|
||||||
|
.update(cx, |editor, cx| editor.selections.all::<usize>(cx));
|
||||||
selections
|
selections
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|selection| selection.head())
|
.map(|selection| selection.head())
|
||||||
@@ -2007,13 +2024,12 @@ impl ContextEditor {
|
|||||||
})
|
})
|
||||||
.map(|(command, error_message)| BlockProperties {
|
.map(|(command, error_message)| BlockProperties {
|
||||||
style: BlockStyle::Fixed,
|
style: BlockStyle::Fixed,
|
||||||
position: Anchor {
|
height: 1,
|
||||||
|
placement: BlockPlacement::Below(Anchor {
|
||||||
buffer_id: Some(buffer_id),
|
buffer_id: Some(buffer_id),
|
||||||
excerpt_id,
|
excerpt_id,
|
||||||
text_anchor: command.source_range.start,
|
text_anchor: command.source_range.start,
|
||||||
},
|
}),
|
||||||
height: 1,
|
|
||||||
disposition: BlockDisposition::Below,
|
|
||||||
render: slash_command_error_block_renderer(error_message),
|
render: slash_command_error_block_renderer(error_message),
|
||||||
priority: 0,
|
priority: 0,
|
||||||
}),
|
}),
|
||||||
@@ -2240,11 +2256,10 @@ impl ContextEditor {
|
|||||||
} else {
|
} else {
|
||||||
let block_ids = editor.insert_blocks(
|
let block_ids = editor.insert_blocks(
|
||||||
[BlockProperties {
|
[BlockProperties {
|
||||||
position: patch_start,
|
|
||||||
height: path_count as u32 + 1,
|
height: path_count as u32 + 1,
|
||||||
style: BlockStyle::Flex,
|
style: BlockStyle::Flex,
|
||||||
render: render_block,
|
render: render_block,
|
||||||
disposition: BlockDisposition::Below,
|
placement: BlockPlacement::Below(patch_start),
|
||||||
priority: 0,
|
priority: 0,
|
||||||
}],
|
}],
|
||||||
None,
|
None,
|
||||||
@@ -2375,7 +2390,9 @@ impl ContextEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn update_active_patch(&mut self, cx: &mut ViewContext<Self>) {
|
fn update_active_patch(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
let newest_cursor = self.editor.read(cx).selections.newest::<Point>(cx).head();
|
let newest_cursor = self.editor.update(cx, |editor, cx| {
|
||||||
|
editor.selections.newest::<Point>(cx).head()
|
||||||
|
});
|
||||||
let context = self.context.read(cx);
|
let context = self.context.read(cx);
|
||||||
|
|
||||||
let new_patch = context.patch_containing(newest_cursor, cx).cloned();
|
let new_patch = context.patch_containing(newest_cursor, cx).cloned();
|
||||||
@@ -2729,12 +2746,13 @@ impl ContextEditor {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
let create_block_properties = |message: &Message| BlockProperties {
|
let create_block_properties = |message: &Message| BlockProperties {
|
||||||
position: buffer
|
|
||||||
.anchor_in_excerpt(excerpt_id, message.anchor_range.start)
|
|
||||||
.unwrap(),
|
|
||||||
height: 2,
|
height: 2,
|
||||||
style: BlockStyle::Sticky,
|
style: BlockStyle::Sticky,
|
||||||
disposition: BlockDisposition::Above,
|
placement: BlockPlacement::Above(
|
||||||
|
buffer
|
||||||
|
.anchor_in_excerpt(excerpt_id, message.anchor_range.start)
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
priority: usize::MAX,
|
priority: usize::MAX,
|
||||||
render: render_block(MessageMetadata::from(message)),
|
render: render_block(MessageMetadata::from(message)),
|
||||||
};
|
};
|
||||||
@@ -2781,39 +2799,40 @@ impl ContextEditor {
|
|||||||
) -> Option<(String, bool)> {
|
) -> Option<(String, bool)> {
|
||||||
const CODE_FENCE_DELIMITER: &'static str = "```";
|
const CODE_FENCE_DELIMITER: &'static str = "```";
|
||||||
|
|
||||||
let context_editor = context_editor_view.read(cx).editor.read(cx);
|
let context_editor = context_editor_view.read(cx).editor.clone();
|
||||||
|
context_editor.update(cx, |context_editor, cx| {
|
||||||
|
if context_editor.selections.newest::<Point>(cx).is_empty() {
|
||||||
|
let snapshot = context_editor.buffer().read(cx).snapshot(cx);
|
||||||
|
let (_, _, snapshot) = snapshot.as_singleton()?;
|
||||||
|
|
||||||
if context_editor.selections.newest::<Point>(cx).is_empty() {
|
let head = context_editor.selections.newest::<Point>(cx).head();
|
||||||
let snapshot = context_editor.buffer().read(cx).snapshot(cx);
|
let offset = snapshot.point_to_offset(head);
|
||||||
let (_, _, snapshot) = snapshot.as_singleton()?;
|
|
||||||
|
|
||||||
let head = context_editor.selections.newest::<Point>(cx).head();
|
let surrounding_code_block_range = find_surrounding_code_block(snapshot, offset)?;
|
||||||
let offset = snapshot.point_to_offset(head);
|
let mut text = snapshot
|
||||||
|
.text_for_range(surrounding_code_block_range)
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
let surrounding_code_block_range = find_surrounding_code_block(snapshot, offset)?;
|
// If there is no newline trailing the closing three-backticks, then
|
||||||
let mut text = snapshot
|
// tree-sitter-md extends the range of the content node to include
|
||||||
.text_for_range(surrounding_code_block_range)
|
// the backticks.
|
||||||
.collect::<String>();
|
if text.ends_with(CODE_FENCE_DELIMITER) {
|
||||||
|
text.drain((text.len() - CODE_FENCE_DELIMITER.len())..);
|
||||||
|
}
|
||||||
|
|
||||||
// If there is no newline trailing the closing three-backticks, then
|
(!text.is_empty()).then_some((text, true))
|
||||||
// tree-sitter-md extends the range of the content node to include
|
} else {
|
||||||
// the backticks.
|
let anchor = context_editor.selections.newest_anchor();
|
||||||
if text.ends_with(CODE_FENCE_DELIMITER) {
|
let text = context_editor
|
||||||
text.drain((text.len() - CODE_FENCE_DELIMITER.len())..);
|
.buffer()
|
||||||
|
.read(cx)
|
||||||
|
.read(cx)
|
||||||
|
.text_for_range(anchor.range())
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
(!text.is_empty()).then_some((text, false))
|
||||||
}
|
}
|
||||||
|
})
|
||||||
(!text.is_empty()).then_some((text, true))
|
|
||||||
} else {
|
|
||||||
let anchor = context_editor.selections.newest_anchor();
|
|
||||||
let text = context_editor
|
|
||||||
.buffer()
|
|
||||||
.read(cx)
|
|
||||||
.read(cx)
|
|
||||||
.text_for_range(anchor.range())
|
|
||||||
.collect::<String>();
|
|
||||||
|
|
||||||
(!text.is_empty()).then_some((text, false))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert_selection(
|
fn insert_selection(
|
||||||
@@ -3370,7 +3389,7 @@ impl ContextEditor {
|
|||||||
let anchor = buffer.anchor_in_excerpt(excerpt_id, anchor).unwrap();
|
let anchor = buffer.anchor_in_excerpt(excerpt_id, anchor).unwrap();
|
||||||
let image = render_image.clone();
|
let image = render_image.clone();
|
||||||
anchor.is_valid(&buffer).then(|| BlockProperties {
|
anchor.is_valid(&buffer).then(|| BlockProperties {
|
||||||
position: anchor,
|
placement: BlockPlacement::Above(anchor),
|
||||||
height: MAX_HEIGHT_IN_LINES,
|
height: MAX_HEIGHT_IN_LINES,
|
||||||
style: BlockStyle::Sticky,
|
style: BlockStyle::Sticky,
|
||||||
render: Box::new(move |cx| {
|
render: Box::new(move |cx| {
|
||||||
@@ -3391,8 +3410,6 @@ impl ContextEditor {
|
|||||||
)
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}),
|
}),
|
||||||
|
|
||||||
disposition: BlockDisposition::Above,
|
|
||||||
priority: 0,
|
priority: 0,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -3645,7 +3662,13 @@ impl ContextEditor {
|
|||||||
button.tooltip(move |_| tooltip.clone())
|
button.tooltip(move |_| tooltip.clone())
|
||||||
})
|
})
|
||||||
.layer(ElevationIndex::ModalSurface)
|
.layer(ElevationIndex::ModalSurface)
|
||||||
.child(Label::new("Send"))
|
.child(Label::new(
|
||||||
|
if AssistantSettings::get_global(cx).are_live_diffs_enabled(cx) {
|
||||||
|
"Chat"
|
||||||
|
} else {
|
||||||
|
"Send"
|
||||||
|
},
|
||||||
|
))
|
||||||
.children(
|
.children(
|
||||||
KeyBinding::for_action_in(&Assist, &focus_handle, cx)
|
KeyBinding::for_action_in(&Assist, &focus_handle, cx)
|
||||||
.map(|binding| binding.into_any_element()),
|
.map(|binding| binding.into_any_element()),
|
||||||
@@ -3655,6 +3678,57 @@ impl ContextEditor {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_edit_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
let focus_handle = self.focus_handle(cx).clone();
|
||||||
|
|
||||||
|
let (style, tooltip) = match token_state(&self.context, cx) {
|
||||||
|
Some(TokenState::NoTokensLeft { .. }) => (
|
||||||
|
ButtonStyle::Tinted(TintColor::Negative),
|
||||||
|
Some(Tooltip::text("Token limit reached", cx)),
|
||||||
|
),
|
||||||
|
Some(TokenState::HasMoreTokens {
|
||||||
|
over_warn_threshold,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
let (style, tooltip) = if over_warn_threshold {
|
||||||
|
(
|
||||||
|
ButtonStyle::Tinted(TintColor::Warning),
|
||||||
|
Some(Tooltip::text("Token limit is close to exhaustion", cx)),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(ButtonStyle::Filled, None)
|
||||||
|
};
|
||||||
|
(style, tooltip)
|
||||||
|
}
|
||||||
|
None => (ButtonStyle::Filled, None),
|
||||||
|
};
|
||||||
|
|
||||||
|
let provider = LanguageModelRegistry::read_global(cx).active_provider();
|
||||||
|
|
||||||
|
let has_configuration_error = configuration_error(cx).is_some();
|
||||||
|
let needs_to_accept_terms = self.show_accept_terms
|
||||||
|
&& provider
|
||||||
|
.as_ref()
|
||||||
|
.map_or(false, |provider| provider.must_accept_terms(cx));
|
||||||
|
let disabled = has_configuration_error || needs_to_accept_terms;
|
||||||
|
|
||||||
|
ButtonLike::new("edit_button")
|
||||||
|
.disabled(disabled)
|
||||||
|
.style(style)
|
||||||
|
.when_some(tooltip, |button, tooltip| {
|
||||||
|
button.tooltip(move |_| tooltip.clone())
|
||||||
|
})
|
||||||
|
.layer(ElevationIndex::ModalSurface)
|
||||||
|
.child(Label::new("Suggest Edits"))
|
||||||
|
.children(
|
||||||
|
KeyBinding::for_action_in(&Edit, &focus_handle, cx)
|
||||||
|
.map(|binding| binding.into_any_element()),
|
||||||
|
)
|
||||||
|
.on_click(move |_event, cx| {
|
||||||
|
focus_handle.dispatch_action(&Edit, cx);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn render_last_error(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
|
fn render_last_error(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
|
||||||
let last_error = self.last_error.as_ref()?;
|
let last_error = self.last_error.as_ref()?;
|
||||||
|
|
||||||
@@ -3669,6 +3743,7 @@ impl ContextEditor {
|
|||||||
.elevation_2(cx)
|
.elevation_2(cx)
|
||||||
.occlude()
|
.occlude()
|
||||||
.child(match last_error {
|
.child(match last_error {
|
||||||
|
AssistError::FileRequired => self.render_file_required_error(cx),
|
||||||
AssistError::PaymentRequired => self.render_payment_required_error(cx),
|
AssistError::PaymentRequired => self.render_payment_required_error(cx),
|
||||||
AssistError::MaxMonthlySpendReached => {
|
AssistError::MaxMonthlySpendReached => {
|
||||||
self.render_max_monthly_spend_reached_error(cx)
|
self.render_max_monthly_spend_reached_error(cx)
|
||||||
@@ -3681,6 +3756,41 @@ impl ContextEditor {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_file_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
|
||||||
|
v_flex()
|
||||||
|
.gap_0p5()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1p5()
|
||||||
|
.items_center()
|
||||||
|
.child(Icon::new(IconName::Warning).color(Color::Warning))
|
||||||
|
.child(
|
||||||
|
Label::new("Suggest Edits needs a file to edit").weight(FontWeight::MEDIUM),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.id("error-message")
|
||||||
|
.max_h_24()
|
||||||
|
.overflow_y_scroll()
|
||||||
|
.child(Label::new(
|
||||||
|
"To include files, type /file or /tab in your prompt.",
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.justify_end()
|
||||||
|
.mt_1()
|
||||||
|
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|
||||||
|
|this, _, cx| {
|
||||||
|
this.last_error = None;
|
||||||
|
cx.notify();
|
||||||
|
},
|
||||||
|
))),
|
||||||
|
)
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
|
||||||
fn render_payment_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
|
fn render_payment_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
|
||||||
const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
|
const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
|
||||||
|
|
||||||
@@ -3895,13 +4005,7 @@ impl Render for ContextEditor {
|
|||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
let focus_handle = self
|
|
||||||
.workspace
|
|
||||||
.update(cx, |workspace, cx| {
|
|
||||||
Some(workspace.active_item_as::<Editor>(cx)?.focus_handle(cx))
|
|
||||||
})
|
|
||||||
.ok()
|
|
||||||
.flatten();
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.key_context("ContextEditor")
|
.key_context("ContextEditor")
|
||||||
.capture_action(cx.listener(ContextEditor::cancel))
|
.capture_action(cx.listener(ContextEditor::cancel))
|
||||||
@@ -3911,6 +4015,7 @@ impl Render for ContextEditor {
|
|||||||
.capture_action(cx.listener(ContextEditor::paste))
|
.capture_action(cx.listener(ContextEditor::paste))
|
||||||
.capture_action(cx.listener(ContextEditor::cycle_message_role))
|
.capture_action(cx.listener(ContextEditor::cycle_message_role))
|
||||||
.capture_action(cx.listener(ContextEditor::confirm_command))
|
.capture_action(cx.listener(ContextEditor::confirm_command))
|
||||||
|
.on_action(cx.listener(ContextEditor::edit))
|
||||||
.on_action(cx.listener(ContextEditor::assist))
|
.on_action(cx.listener(ContextEditor::assist))
|
||||||
.on_action(cx.listener(ContextEditor::split))
|
.on_action(cx.listener(ContextEditor::split))
|
||||||
.size_full()
|
.size_full()
|
||||||
@@ -3947,35 +4052,28 @@ impl Render for ContextEditor {
|
|||||||
.bg(cx.theme().colors().editor_background)
|
.bg(cx.theme().colors().editor_background)
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_2()
|
.gap_1()
|
||||||
.child(render_inject_context_menu(cx.view().downgrade(), cx))
|
.child(render_inject_context_menu(cx.view().downgrade(), cx)),
|
||||||
.child(
|
|
||||||
IconButton::new("quote-button", IconName::Quote)
|
|
||||||
.icon_size(IconSize::Small)
|
|
||||||
.on_click(|_, cx| {
|
|
||||||
cx.dispatch_action(QuoteSelection.boxed_clone());
|
|
||||||
})
|
|
||||||
.tooltip(move |cx| {
|
|
||||||
cx.new_view(|cx| {
|
|
||||||
Tooltip::new("Insert Selection").key_binding(
|
|
||||||
focus_handle.as_ref().and_then(|handle| {
|
|
||||||
KeyBinding::for_action_in(
|
|
||||||
&QuoteSelection,
|
|
||||||
&handle,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.into()
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.w_full()
|
.w_full()
|
||||||
.justify_end()
|
.justify_end()
|
||||||
.child(div().child(self.render_send_button(cx))),
|
.when(
|
||||||
|
AssistantSettings::get_global(cx).are_live_diffs_enabled(cx),
|
||||||
|
|buttons| {
|
||||||
|
buttons
|
||||||
|
.items_center()
|
||||||
|
.gap_1p5()
|
||||||
|
.child(self.render_edit_button(cx))
|
||||||
|
.child(
|
||||||
|
Label::new("or")
|
||||||
|
.size(LabelSize::Small)
|
||||||
|
.color(Color::Muted),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.child(self.render_send_button(cx)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -4247,11 +4345,12 @@ fn render_inject_context_menu(
|
|||||||
slash_command_picker::SlashCommandSelector::new(
|
slash_command_picker::SlashCommandSelector::new(
|
||||||
commands.clone(),
|
commands.clone(),
|
||||||
active_context_editor,
|
active_context_editor,
|
||||||
IconButton::new("trigger", IconName::SlashSquare)
|
Button::new("trigger", "Add Context")
|
||||||
|
.icon(IconName::Plus)
|
||||||
.icon_size(IconSize::Small)
|
.icon_size(IconSize::Small)
|
||||||
.tooltip(|cx| {
|
.icon_color(Color::Muted)
|
||||||
Tooltip::with_meta("Insert Context", None, "Type / to insert via keyboard", cx)
|
.icon_position(IconPosition::Start)
|
||||||
}),
|
.tooltip(|cx| Tooltip::text("Type / to insert via keyboard", cx)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4424,7 +4523,7 @@ impl Render for ContextEditorToolbarItem {
|
|||||||
.w_full()
|
.w_full()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(Label::new("Insert Context"))
|
.child(Label::new("Add Context"))
|
||||||
.child(Label::new("/ command").color(Color::Muted))
|
.child(Label::new("/ command").color(Color::Muted))
|
||||||
.into_any()
|
.into_any()
|
||||||
},
|
},
|
||||||
@@ -4448,7 +4547,7 @@ impl Render for ContextEditorToolbarItem {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.action("Insert Selection", QuoteSelection.boxed_clone())
|
.action("Add Selection", QuoteSelection.boxed_clone())
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -4708,7 +4807,7 @@ impl Render for ConfigurationView {
|
|||||||
|
|
||||||
let mut element = v_flex()
|
let mut element = v_flex()
|
||||||
.id("assistant-configuration-view")
|
.id("assistant-configuration-view")
|
||||||
.track_focus(&self.focus_handle)
|
.track_focus(&self.focus_handle(cx))
|
||||||
.bg(cx.theme().colors().editor_background)
|
.bg(cx.theme().colors().editor_background)
|
||||||
.size_full()
|
.size_full()
|
||||||
.overflow_y_scroll()
|
.overflow_y_scroll()
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
mod context_tests;
|
mod context_tests;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
prompts::PromptBuilder, slash_command::SlashCommandLine, AssistantEdit, AssistantPatch,
|
prompts::PromptBuilder,
|
||||||
AssistantPatchStatus, MessageId, MessageStatus,
|
slash_command::{file_command::FileCommandMetadata, SlashCommandLine},
|
||||||
|
AssistantEdit, AssistantPatch, AssistantPatchStatus, MessageId, MessageStatus,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Context as _, Result};
|
use anyhow::{anyhow, Context as _, Result};
|
||||||
use assistant_slash_command::{
|
use assistant_slash_command::{
|
||||||
SlashCommandOutput, SlashCommandOutputSection, SlashCommandRegistry,
|
SlashCommandOutput, SlashCommandOutputSection, SlashCommandRegistry, SlashCommandResult,
|
||||||
};
|
};
|
||||||
use assistant_tool::ToolRegistry;
|
use assistant_tool::ToolRegistry;
|
||||||
use client::{self, proto, telemetry::Telemetry};
|
use client::{self, proto, telemetry::Telemetry};
|
||||||
@@ -23,6 +24,7 @@ use gpui::{
|
|||||||
|
|
||||||
use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset};
|
use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset};
|
||||||
use language_model::{
|
use language_model::{
|
||||||
|
logging::report_assistant_event,
|
||||||
provider::cloud::{MaxMonthlySpendReachedError, PaymentRequiredError},
|
provider::cloud::{MaxMonthlySpendReachedError, PaymentRequiredError},
|
||||||
LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionEvent,
|
LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionEvent,
|
||||||
LanguageModelImage, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
LanguageModelImage, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||||
@@ -66,6 +68,14 @@ impl ContextId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum RequestType {
|
||||||
|
/// Request a normal chat response from the model.
|
||||||
|
Chat,
|
||||||
|
/// Add a preamble to the message, which tells the model to return a structured response that suggests edits.
|
||||||
|
SuggestEdits,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum ContextOperation {
|
pub enum ContextOperation {
|
||||||
InsertMessage {
|
InsertMessage {
|
||||||
@@ -981,6 +991,20 @@ impl Context {
|
|||||||
&self.slash_command_output_sections
|
&self.slash_command_output_sections
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn contains_files(&self, cx: &AppContext) -> bool {
|
||||||
|
let buffer = self.buffer.read(cx);
|
||||||
|
self.slash_command_output_sections.iter().any(|section| {
|
||||||
|
section.is_valid(buffer)
|
||||||
|
&& section
|
||||||
|
.metadata
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|metadata| {
|
||||||
|
serde_json::from_value::<FileCommandMetadata>(metadata.clone()).ok()
|
||||||
|
})
|
||||||
|
.is_some()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn pending_tool_uses(&self) -> Vec<&PendingToolUse> {
|
pub fn pending_tool_uses(&self) -> Vec<&PendingToolUse> {
|
||||||
self.pending_tool_uses_by_id.values().collect()
|
self.pending_tool_uses_by_id.values().collect()
|
||||||
}
|
}
|
||||||
@@ -1028,7 +1052,7 @@ impl Context {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn count_remaining_tokens(&mut self, cx: &mut ModelContext<Self>) {
|
pub(crate) fn count_remaining_tokens(&mut self, cx: &mut ModelContext<Self>) {
|
||||||
let request = self.to_completion_request(cx);
|
let request = self.to_completion_request(RequestType::SuggestEdits, cx); // Conservatively assume SuggestEdits, since it takes more tokens.
|
||||||
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
|
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -1171,7 +1195,7 @@ impl Context {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let request = {
|
let request = {
|
||||||
let mut req = self.to_completion_request(cx);
|
let mut req = self.to_completion_request(RequestType::Chat, cx);
|
||||||
// Skip the last message because it's likely to change and
|
// Skip the last message because it's likely to change and
|
||||||
// therefore would be a waste to cache.
|
// therefore would be a waste to cache.
|
||||||
req.messages.pop();
|
req.messages.pop();
|
||||||
@@ -1677,7 +1701,7 @@ impl Context {
|
|||||||
pub fn insert_command_output(
|
pub fn insert_command_output(
|
||||||
&mut self,
|
&mut self,
|
||||||
command_range: Range<language::Anchor>,
|
command_range: Range<language::Anchor>,
|
||||||
output: Task<Result<SlashCommandOutput>>,
|
output: Task<SlashCommandResult>,
|
||||||
ensure_trailing_newline: bool,
|
ensure_trailing_newline: bool,
|
||||||
expand_result: bool,
|
expand_result: bool,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
@@ -1688,19 +1712,13 @@ impl Context {
|
|||||||
let command_range = command_range.clone();
|
let command_range = command_range.clone();
|
||||||
async move {
|
async move {
|
||||||
let output = output.await;
|
let output = output.await;
|
||||||
|
let output = match output {
|
||||||
|
Ok(output) => SlashCommandOutput::from_event_stream(output).await,
|
||||||
|
Err(err) => Err(err),
|
||||||
|
};
|
||||||
this.update(&mut cx, |this, cx| match output {
|
this.update(&mut cx, |this, cx| match output {
|
||||||
Ok(mut output) => {
|
Ok(mut output) => {
|
||||||
// Ensure section ranges are valid.
|
output.ensure_valid_section_ranges();
|
||||||
for section in &mut output.sections {
|
|
||||||
section.range.start = section.range.start.min(output.text.len());
|
|
||||||
section.range.end = section.range.end.min(output.text.len());
|
|
||||||
while !output.text.is_char_boundary(section.range.start) {
|
|
||||||
section.range.start -= 1;
|
|
||||||
}
|
|
||||||
while !output.text.is_char_boundary(section.range.end) {
|
|
||||||
section.range.end += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure there is a newline after the last section.
|
// Ensure there is a newline after the last section.
|
||||||
if ensure_trailing_newline {
|
if ensure_trailing_newline {
|
||||||
@@ -1865,7 +1883,11 @@ impl Context {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn assist(&mut self, cx: &mut ModelContext<Self>) -> Option<MessageAnchor> {
|
pub fn assist(
|
||||||
|
&mut self,
|
||||||
|
request_type: RequestType,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Option<MessageAnchor> {
|
||||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||||
let provider = model_registry.active_provider()?;
|
let provider = model_registry.active_provider()?;
|
||||||
let model = model_registry.active_model()?;
|
let model = model_registry.active_model()?;
|
||||||
@@ -1878,7 +1900,7 @@ impl Context {
|
|||||||
// Compute which messages to cache, including the last one.
|
// Compute which messages to cache, including the last one.
|
||||||
self.mark_cache_anchors(&model.cache_configuration(), false, cx);
|
self.mark_cache_anchors(&model.cache_configuration(), false, cx);
|
||||||
|
|
||||||
let mut request = self.to_completion_request(cx);
|
let mut request = self.to_completion_request(request_type, cx);
|
||||||
|
|
||||||
if cx.has_flag::<ToolUseFeatureFlag>() {
|
if cx.has_flag::<ToolUseFeatureFlag>() {
|
||||||
let tool_registry = ToolRegistry::global(cx);
|
let tool_registry = ToolRegistry::global(cx);
|
||||||
@@ -1934,6 +1956,7 @@ impl Context {
|
|||||||
});
|
});
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
|
LanguageModelCompletionEvent::StartMessage { .. } => {}
|
||||||
LanguageModelCompletionEvent::Stop(reason) => {
|
LanguageModelCompletionEvent::Stop(reason) => {
|
||||||
stop_reason = reason;
|
stop_reason = reason;
|
||||||
}
|
}
|
||||||
@@ -2039,23 +2062,28 @@ impl Context {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(telemetry) = this.telemetry.as_ref() {
|
let language_name = this
|
||||||
let language_name = this
|
.buffer
|
||||||
.buffer
|
.read(cx)
|
||||||
.read(cx)
|
.language()
|
||||||
.language()
|
.map(|language| language.name());
|
||||||
.map(|language| language.name());
|
report_assistant_event(
|
||||||
telemetry.report_assistant_event(AssistantEvent {
|
AssistantEvent {
|
||||||
conversation_id: Some(this.id.0.clone()),
|
conversation_id: Some(this.id.0.clone()),
|
||||||
kind: AssistantKind::Panel,
|
kind: AssistantKind::Panel,
|
||||||
phase: AssistantPhase::Response,
|
phase: AssistantPhase::Response,
|
||||||
|
message_id: None,
|
||||||
model: model.telemetry_id(),
|
model: model.telemetry_id(),
|
||||||
model_provider: model.provider_id().to_string(),
|
model_provider: model.provider_id().to_string(),
|
||||||
response_latency,
|
response_latency,
|
||||||
error_message,
|
error_message,
|
||||||
language_name: language_name.map(|name| name.to_proto()),
|
language_name: language_name.map(|name| name.to_proto()),
|
||||||
});
|
},
|
||||||
}
|
this.telemetry.clone(),
|
||||||
|
cx.http_client(),
|
||||||
|
model.api_key(cx),
|
||||||
|
cx.background_executor(),
|
||||||
|
);
|
||||||
|
|
||||||
if let Ok(stop_reason) = result {
|
if let Ok(stop_reason) = result {
|
||||||
match stop_reason {
|
match stop_reason {
|
||||||
@@ -2080,7 +2108,11 @@ impl Context {
|
|||||||
Some(user_message)
|
Some(user_message)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_completion_request(&self, cx: &AppContext) -> LanguageModelRequest {
|
pub fn to_completion_request(
|
||||||
|
&self,
|
||||||
|
request_type: RequestType,
|
||||||
|
cx: &AppContext,
|
||||||
|
) -> LanguageModelRequest {
|
||||||
let buffer = self.buffer.read(cx);
|
let buffer = self.buffer.read(cx);
|
||||||
|
|
||||||
let mut contents = self.contents(cx).peekable();
|
let mut contents = self.contents(cx).peekable();
|
||||||
@@ -2169,6 +2201,25 @@ impl Context {
|
|||||||
completion_request.messages.push(request_message);
|
completion_request.messages.push(request_message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let RequestType::SuggestEdits = request_type {
|
||||||
|
if let Ok(preamble) = self.prompt_builder.generate_workflow_prompt() {
|
||||||
|
let last_elem_index = completion_request.messages.len();
|
||||||
|
|
||||||
|
completion_request
|
||||||
|
.messages
|
||||||
|
.push(LanguageModelRequestMessage {
|
||||||
|
role: Role::User,
|
||||||
|
content: vec![MessageContent::Text(preamble)],
|
||||||
|
cache: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// The preamble message should be sent right before the last actual user message.
|
||||||
|
completion_request
|
||||||
|
.messages
|
||||||
|
.swap(last_elem_index, last_elem_index.saturating_sub(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
completion_request
|
completion_request
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2483,11 +2534,12 @@ impl Context {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut request = self.to_completion_request(cx);
|
let mut request = self.to_completion_request(RequestType::Chat, cx);
|
||||||
request.messages.push(LanguageModelRequestMessage {
|
request.messages.push(LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec![
|
content: vec![
|
||||||
"Summarize the context into a short title without punctuation.".into(),
|
"Generate a concise 3-7 word title for this conversation, omitting punctuation"
|
||||||
|
.into(),
|
||||||
],
|
],
|
||||||
cache: false,
|
cache: false,
|
||||||
});
|
});
|
||||||
@@ -2498,7 +2550,7 @@ impl Context {
|
|||||||
let mut messages = stream.await?;
|
let mut messages = stream.await?;
|
||||||
|
|
||||||
let mut replaced = !replace_old;
|
let mut replaced = !replace_old;
|
||||||
while let Some(message) = messages.next().await {
|
while let Some(message) = messages.stream.next().await {
|
||||||
let text = message?;
|
let text = message?;
|
||||||
let mut lines = text.lines();
|
let mut lines = text.lines();
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use crate::{
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use assistant_slash_command::{
|
use assistant_slash_command::{
|
||||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||||
SlashCommandRegistry,
|
SlashCommandRegistry, SlashCommandResult,
|
||||||
};
|
};
|
||||||
use collections::HashSet;
|
use collections::HashSet;
|
||||||
use fs::FakeFs;
|
use fs::FakeFs;
|
||||||
@@ -636,7 +636,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
|||||||
kind: AssistantEditKind::InsertAfter {
|
kind: AssistantEditKind::InsertAfter {
|
||||||
old_text: "fn one".into(),
|
old_text: "fn one".into(),
|
||||||
new_text: "fn two() {}".into(),
|
new_text: "fn two() {}".into(),
|
||||||
description: "add a `two` function".into(),
|
description: Some("add a `two` function".into()),
|
||||||
},
|
},
|
||||||
}]],
|
}]],
|
||||||
cx,
|
cx,
|
||||||
@@ -690,7 +690,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
|||||||
kind: AssistantEditKind::InsertAfter {
|
kind: AssistantEditKind::InsertAfter {
|
||||||
old_text: "fn zero".into(),
|
old_text: "fn zero".into(),
|
||||||
new_text: "fn two() {}".into(),
|
new_text: "fn two() {}".into(),
|
||||||
description: "add a `two` function".into(),
|
description: Some("add a `two` function".into()),
|
||||||
},
|
},
|
||||||
}]],
|
}]],
|
||||||
cx,
|
cx,
|
||||||
@@ -754,7 +754,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
|||||||
kind: AssistantEditKind::InsertAfter {
|
kind: AssistantEditKind::InsertAfter {
|
||||||
old_text: "fn zero".into(),
|
old_text: "fn zero".into(),
|
||||||
new_text: "fn two() {}".into(),
|
new_text: "fn two() {}".into(),
|
||||||
description: "add a `two` function".into(),
|
description: Some("add a `two` function".into()),
|
||||||
},
|
},
|
||||||
}]],
|
}]],
|
||||||
cx,
|
cx,
|
||||||
@@ -798,7 +798,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
|||||||
kind: AssistantEditKind::InsertAfter {
|
kind: AssistantEditKind::InsertAfter {
|
||||||
old_text: "fn zero".into(),
|
old_text: "fn zero".into(),
|
||||||
new_text: "fn two() {}".into(),
|
new_text: "fn two() {}".into(),
|
||||||
description: "add a `two` function".into(),
|
description: Some("add a `two` function".into()),
|
||||||
},
|
},
|
||||||
}]],
|
}]],
|
||||||
cx,
|
cx,
|
||||||
@@ -1097,7 +1097,8 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
|||||||
text: output_text,
|
text: output_text,
|
||||||
sections,
|
sections,
|
||||||
run_commands_in_text: false,
|
run_commands_in_text: false,
|
||||||
})),
|
}
|
||||||
|
.to_event_stream())),
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
cx,
|
cx,
|
||||||
@@ -1416,11 +1417,12 @@ impl SlashCommand for FakeSlashCommand {
|
|||||||
_workspace: WeakView<Workspace>,
|
_workspace: WeakView<Workspace>,
|
||||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||||
_cx: &mut WindowContext,
|
_cx: &mut WindowContext,
|
||||||
) -> Task<Result<SlashCommandOutput>> {
|
) -> Task<SlashCommandResult> {
|
||||||
Task::ready(Ok(SlashCommandOutput {
|
Task::ready(Ok(SlashCommandOutput {
|
||||||
text: format!("Executed fake command: {}", self.0),
|
text: format!("Executed fake command: {}", self.0),
|
||||||
sections: vec![],
|
sections: vec![],
|
||||||
run_commands_in_text: false,
|
run_commands_in_text: false,
|
||||||
}))
|
}
|
||||||
|
.to_event_stream()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
assistant_settings::AssistantSettings, humanize_token_count, prompts::PromptBuilder,
|
assistant_settings::AssistantSettings, humanize_token_count, prompts::PromptBuilder,
|
||||||
AssistantPanel, AssistantPanelEvent, CharOperation, CycleNextInlineAssist,
|
AssistantPanel, AssistantPanelEvent, CharOperation, CycleNextInlineAssist,
|
||||||
CyclePreviousInlineAssist, LineDiff, LineOperation, ModelSelector, StreamingDiff,
|
CyclePreviousInlineAssist, LineDiff, LineOperation, ModelSelector, RequestType, StreamingDiff,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Context as _, Result};
|
use anyhow::{anyhow, Context as _, Result};
|
||||||
use client::{telemetry::Telemetry, ErrorExt};
|
use client::{telemetry::Telemetry, ErrorExt};
|
||||||
@@ -9,7 +9,7 @@ use collections::{hash_map, HashMap, HashSet, VecDeque};
|
|||||||
use editor::{
|
use editor::{
|
||||||
actions::{MoveDown, MoveUp, SelectAll},
|
actions::{MoveDown, MoveUp, SelectAll},
|
||||||
display_map::{
|
display_map::{
|
||||||
BlockContext, BlockDisposition, BlockProperties, BlockStyle, CustomBlockId, RenderBlock,
|
BlockContext, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, RenderBlock,
|
||||||
ToDisplayPoint,
|
ToDisplayPoint,
|
||||||
},
|
},
|
||||||
Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorElement, EditorEvent, EditorMode,
|
Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorElement, EditorEvent, EditorMode,
|
||||||
@@ -21,9 +21,7 @@ use fs::Fs;
|
|||||||
use futures::{
|
use futures::{
|
||||||
channel::mpsc,
|
channel::mpsc,
|
||||||
future::{BoxFuture, LocalBoxFuture},
|
future::{BoxFuture, LocalBoxFuture},
|
||||||
join,
|
join, SinkExt, Stream, StreamExt,
|
||||||
stream::{self, BoxStream},
|
|
||||||
SinkExt, Stream, StreamExt,
|
|
||||||
};
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
anchored, deferred, point, AnyElement, AppContext, ClickEvent, EventEmitter, FocusHandle,
|
anchored, deferred, point, AnyElement, AppContext, ClickEvent, EventEmitter, FocusHandle,
|
||||||
@@ -32,7 +30,8 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
use language::{Buffer, IndentKind, Point, Selection, TransactionId};
|
use language::{Buffer, IndentKind, Point, Selection, TransactionId};
|
||||||
use language_model::{
|
use language_model::{
|
||||||
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
|
logging::report_assistant_event, LanguageModel, LanguageModelRegistry, LanguageModelRequest,
|
||||||
|
LanguageModelRequestMessage, LanguageModelTextStream, Role,
|
||||||
};
|
};
|
||||||
use multi_buffer::MultiBufferRow;
|
use multi_buffer::MultiBufferRow;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
@@ -54,7 +53,7 @@ use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
|
|||||||
use terminal_view::terminal_panel::TerminalPanel;
|
use terminal_view::terminal_panel::TerminalPanel;
|
||||||
use text::{OffsetRangeExt, ToPoint as _};
|
use text::{OffsetRangeExt, ToPoint as _};
|
||||||
use theme::ThemeSettings;
|
use theme::ThemeSettings;
|
||||||
use ui::{prelude::*, CheckboxWithLabel, IconButtonShape, Popover, Tooltip};
|
use ui::{prelude::*, text_for_action, CheckboxWithLabel, IconButtonShape, Popover, Tooltip};
|
||||||
use util::{RangeExt, ResultExt};
|
use util::{RangeExt, ResultExt};
|
||||||
use workspace::{notifications::NotificationId, ItemHandle, Toast, Workspace};
|
use workspace::{notifications::NotificationId, ItemHandle, Toast, Workspace};
|
||||||
|
|
||||||
@@ -189,11 +188,16 @@ impl InlineAssistant {
|
|||||||
initial_prompt: Option<String>,
|
initial_prompt: Option<String>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) {
|
) {
|
||||||
let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
|
let (snapshot, initial_selections) = editor.update(cx, |editor, cx| {
|
||||||
|
(
|
||||||
|
editor.buffer().read(cx).snapshot(cx),
|
||||||
|
editor.selections.all::<Point>(cx),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
let mut selections = Vec::<Selection<Point>>::new();
|
let mut selections = Vec::<Selection<Point>>::new();
|
||||||
let mut newest_selection = None;
|
let mut newest_selection = None;
|
||||||
for mut selection in editor.read(cx).selections.all::<Point>(cx) {
|
for mut selection in initial_selections {
|
||||||
if selection.end > selection.start {
|
if selection.end > selection.start {
|
||||||
selection.start.column = 0;
|
selection.start.column = 0;
|
||||||
// If the selection ends at the start of the line, we don't want to include it.
|
// If the selection ends at the start of the line, we don't want to include it.
|
||||||
@@ -236,12 +240,13 @@ impl InlineAssistant {
|
|||||||
};
|
};
|
||||||
codegen_ranges.push(start..end);
|
codegen_ranges.push(start..end);
|
||||||
|
|
||||||
if let Some(telemetry) = self.telemetry.as_ref() {
|
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
|
||||||
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
|
if let Some(telemetry) = self.telemetry.as_ref() {
|
||||||
telemetry.report_assistant_event(AssistantEvent {
|
telemetry.report_assistant_event(AssistantEvent {
|
||||||
conversation_id: None,
|
conversation_id: None,
|
||||||
kind: AssistantKind::Inline,
|
kind: AssistantKind::Inline,
|
||||||
phase: AssistantPhase::Invoked,
|
phase: AssistantPhase::Invoked,
|
||||||
|
message_id: None,
|
||||||
model: model.telemetry_id(),
|
model: model.telemetry_id(),
|
||||||
model_provider: model.provider_id().to_string(),
|
model_provider: model.provider_id().to_string(),
|
||||||
response_latency: None,
|
response_latency: None,
|
||||||
@@ -446,15 +451,14 @@ impl InlineAssistant {
|
|||||||
let assist_blocks = vec![
|
let assist_blocks = vec![
|
||||||
BlockProperties {
|
BlockProperties {
|
||||||
style: BlockStyle::Sticky,
|
style: BlockStyle::Sticky,
|
||||||
position: range.start,
|
placement: BlockPlacement::Above(range.start),
|
||||||
height: prompt_editor_height,
|
height: prompt_editor_height,
|
||||||
render: build_assist_editor_renderer(prompt_editor),
|
render: build_assist_editor_renderer(prompt_editor),
|
||||||
disposition: BlockDisposition::Above,
|
|
||||||
priority: 0,
|
priority: 0,
|
||||||
},
|
},
|
||||||
BlockProperties {
|
BlockProperties {
|
||||||
style: BlockStyle::Sticky,
|
style: BlockStyle::Sticky,
|
||||||
position: range.end,
|
placement: BlockPlacement::Below(range.end),
|
||||||
height: 0,
|
height: 0,
|
||||||
render: Box::new(|cx| {
|
render: Box::new(|cx| {
|
||||||
v_flex()
|
v_flex()
|
||||||
@@ -464,7 +468,6 @@ impl InlineAssistant {
|
|||||||
.border_color(cx.theme().status().info_border)
|
.border_color(cx.theme().status().info_border)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}),
|
}),
|
||||||
disposition: BlockDisposition::Below,
|
|
||||||
priority: 0,
|
priority: 0,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -568,10 +571,13 @@ impl InlineAssistant {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let editor = editor.read(cx);
|
if editor.read(cx).selections.count() == 1 {
|
||||||
if editor.selections.count() == 1 {
|
let (selection, buffer) = editor.update(cx, |editor, cx| {
|
||||||
let selection = editor.selections.newest::<usize>(cx);
|
(
|
||||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
editor.selections.newest::<usize>(cx),
|
||||||
|
editor.buffer().read(cx).snapshot(cx),
|
||||||
|
)
|
||||||
|
});
|
||||||
for assist_id in &editor_assists.assist_ids {
|
for assist_id in &editor_assists.assist_ids {
|
||||||
let assist = &self.assists[assist_id];
|
let assist = &self.assists[assist_id];
|
||||||
let assist_range = assist.range.to_offset(&buffer);
|
let assist_range = assist.range.to_offset(&buffer);
|
||||||
@@ -596,10 +602,13 @@ impl InlineAssistant {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let editor = editor.read(cx);
|
if editor.read(cx).selections.count() == 1 {
|
||||||
if editor.selections.count() == 1 {
|
let (selection, buffer) = editor.update(cx, |editor, cx| {
|
||||||
let selection = editor.selections.newest::<usize>(cx);
|
(
|
||||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
editor.selections.newest::<usize>(cx),
|
||||||
|
editor.buffer().read(cx).snapshot(cx),
|
||||||
|
)
|
||||||
|
});
|
||||||
let mut closest_assist_fallback = None;
|
let mut closest_assist_fallback = None;
|
||||||
for assist_id in &editor_assists.assist_ids {
|
for assist_id in &editor_assists.assist_ids {
|
||||||
let assist = &self.assists[assist_id];
|
let assist = &self.assists[assist_id];
|
||||||
@@ -745,33 +754,6 @@ impl InlineAssistant {
|
|||||||
|
|
||||||
pub fn finish_assist(&mut self, assist_id: InlineAssistId, undo: bool, cx: &mut WindowContext) {
|
pub fn finish_assist(&mut self, assist_id: InlineAssistId, undo: bool, cx: &mut WindowContext) {
|
||||||
if let Some(assist) = self.assists.get(&assist_id) {
|
if let Some(assist) = self.assists.get(&assist_id) {
|
||||||
if let Some(telemetry) = self.telemetry.as_ref() {
|
|
||||||
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
|
|
||||||
let language_name = assist.editor.upgrade().and_then(|editor| {
|
|
||||||
let multibuffer = editor.read(cx).buffer().read(cx);
|
|
||||||
let ranges = multibuffer.range_to_buffer_ranges(assist.range.clone(), cx);
|
|
||||||
ranges
|
|
||||||
.first()
|
|
||||||
.and_then(|(buffer, _, _)| buffer.read(cx).language())
|
|
||||||
.map(|language| language.name())
|
|
||||||
});
|
|
||||||
telemetry.report_assistant_event(AssistantEvent {
|
|
||||||
conversation_id: None,
|
|
||||||
kind: AssistantKind::Inline,
|
|
||||||
phase: if undo {
|
|
||||||
AssistantPhase::Rejected
|
|
||||||
} else {
|
|
||||||
AssistantPhase::Accepted
|
|
||||||
},
|
|
||||||
model: model.telemetry_id(),
|
|
||||||
model_provider: model.provider_id().to_string(),
|
|
||||||
response_latency: None,
|
|
||||||
error_message: None,
|
|
||||||
language_name: language_name.map(|name| name.to_proto()),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let assist_group_id = assist.group_id;
|
let assist_group_id = assist.group_id;
|
||||||
if self.assist_groups[&assist_group_id].linked {
|
if self.assist_groups[&assist_group_id].linked {
|
||||||
for assist_id in self.unlink_assist_group(assist_group_id, cx) {
|
for assist_id in self.unlink_assist_group(assist_group_id, cx) {
|
||||||
@@ -806,12 +788,45 @@ impl InlineAssistant {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let active_alternative = assist.codegen.read(cx).active_alternative().clone();
|
||||||
|
let message_id = active_alternative.read(cx).message_id.clone();
|
||||||
|
|
||||||
|
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
|
||||||
|
let language_name = assist.editor.upgrade().and_then(|editor| {
|
||||||
|
let multibuffer = editor.read(cx).buffer().read(cx);
|
||||||
|
let ranges = multibuffer.range_to_buffer_ranges(assist.range.clone(), cx);
|
||||||
|
ranges
|
||||||
|
.first()
|
||||||
|
.and_then(|(buffer, _, _)| buffer.read(cx).language())
|
||||||
|
.map(|language| language.name())
|
||||||
|
});
|
||||||
|
report_assistant_event(
|
||||||
|
AssistantEvent {
|
||||||
|
conversation_id: None,
|
||||||
|
kind: AssistantKind::Inline,
|
||||||
|
message_id,
|
||||||
|
phase: if undo {
|
||||||
|
AssistantPhase::Rejected
|
||||||
|
} else {
|
||||||
|
AssistantPhase::Accepted
|
||||||
|
},
|
||||||
|
model: model.telemetry_id(),
|
||||||
|
model_provider: model.provider_id().to_string(),
|
||||||
|
response_latency: None,
|
||||||
|
error_message: None,
|
||||||
|
language_name: language_name.map(|name| name.to_proto()),
|
||||||
|
},
|
||||||
|
self.telemetry.clone(),
|
||||||
|
cx.http_client(),
|
||||||
|
model.api_key(cx),
|
||||||
|
cx.background_executor(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if undo {
|
if undo {
|
||||||
assist.codegen.update(cx, |codegen, cx| codegen.undo(cx));
|
assist.codegen.update(cx, |codegen, cx| codegen.undo(cx));
|
||||||
} else {
|
} else {
|
||||||
let confirmed_alternative = assist.codegen.read(cx).active_alternative().clone();
|
self.confirmed_assists.insert(assist_id, active_alternative);
|
||||||
self.confirmed_assists
|
|
||||||
.insert(assist_id, confirmed_alternative);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1179,7 +1194,7 @@ impl InlineAssistant {
|
|||||||
let height =
|
let height =
|
||||||
deleted_lines_editor.update(cx, |editor, cx| editor.max_point(cx).row().0 + 1);
|
deleted_lines_editor.update(cx, |editor, cx| editor.max_point(cx).row().0 + 1);
|
||||||
new_blocks.push(BlockProperties {
|
new_blocks.push(BlockProperties {
|
||||||
position: new_row,
|
placement: BlockPlacement::Above(new_row),
|
||||||
height,
|
height,
|
||||||
style: BlockStyle::Flex,
|
style: BlockStyle::Flex,
|
||||||
render: Box::new(move |cx| {
|
render: Box::new(move |cx| {
|
||||||
@@ -1191,7 +1206,6 @@ impl InlineAssistant {
|
|||||||
.child(deleted_lines_editor.clone())
|
.child(deleted_lines_editor.clone())
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}),
|
}),
|
||||||
disposition: BlockDisposition::Above,
|
|
||||||
priority: 0,
|
priority: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1599,7 +1613,7 @@ impl PromptEditor {
|
|||||||
// always show the cursor (even when it isn't focused) because
|
// always show the cursor (even when it isn't focused) because
|
||||||
// typing in one will make what you typed appear in all of them.
|
// typing in one will make what you typed appear in all of them.
|
||||||
editor.set_show_cursor_when_unfocused(true, cx);
|
editor.set_show_cursor_when_unfocused(true, cx);
|
||||||
editor.set_placeholder_text("Add a prompt…", cx);
|
editor.set_placeholder_text(Self::placeholder_text(codegen.read(cx), cx), cx);
|
||||||
editor
|
editor
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1656,6 +1670,7 @@ impl PromptEditor {
|
|||||||
self.editor = cx.new_view(|cx| {
|
self.editor = cx.new_view(|cx| {
|
||||||
let mut editor = Editor::auto_height(Self::MAX_LINES as usize, cx);
|
let mut editor = Editor::auto_height(Self::MAX_LINES as usize, cx);
|
||||||
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
|
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
|
||||||
|
editor.set_placeholder_text(Self::placeholder_text(self.codegen.read(cx), cx), cx);
|
||||||
editor.set_placeholder_text("Add a prompt…", cx);
|
editor.set_placeholder_text("Add a prompt…", cx);
|
||||||
editor.set_text(prompt, cx);
|
editor.set_text(prompt, cx);
|
||||||
if focus {
|
if focus {
|
||||||
@@ -1666,6 +1681,20 @@ impl PromptEditor {
|
|||||||
self.subscribe_to_editor(cx);
|
self.subscribe_to_editor(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn placeholder_text(codegen: &Codegen, cx: &WindowContext) -> String {
|
||||||
|
let context_keybinding = text_for_action(&crate::ToggleFocus, cx)
|
||||||
|
.map(|keybinding| format!(" • {keybinding} for context"))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let action = if codegen.is_insertion {
|
||||||
|
"Generate"
|
||||||
|
} else {
|
||||||
|
"Transform"
|
||||||
|
};
|
||||||
|
|
||||||
|
format!("{action}…{context_keybinding} • ↓↑ for history")
|
||||||
|
}
|
||||||
|
|
||||||
fn prompt(&self, cx: &AppContext) -> String {
|
fn prompt(&self, cx: &AppContext) -> String {
|
||||||
self.editor.read(cx).text(cx)
|
self.editor.read(cx).text(cx)
|
||||||
}
|
}
|
||||||
@@ -2222,7 +2251,7 @@ impl InlineAssist {
|
|||||||
.read(cx)
|
.read(cx)
|
||||||
.active_context(cx)?
|
.active_context(cx)?
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.to_completion_request(cx),
|
.to_completion_request(RequestType::Chat, cx),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -2256,12 +2285,14 @@ pub enum CodegenEvent {
|
|||||||
pub struct Codegen {
|
pub struct Codegen {
|
||||||
alternatives: Vec<Model<CodegenAlternative>>,
|
alternatives: Vec<Model<CodegenAlternative>>,
|
||||||
active_alternative: usize,
|
active_alternative: usize,
|
||||||
|
seen_alternatives: HashSet<usize>,
|
||||||
subscriptions: Vec<Subscription>,
|
subscriptions: Vec<Subscription>,
|
||||||
buffer: Model<MultiBuffer>,
|
buffer: Model<MultiBuffer>,
|
||||||
range: Range<Anchor>,
|
range: Range<Anchor>,
|
||||||
initial_transaction_id: Option<TransactionId>,
|
initial_transaction_id: Option<TransactionId>,
|
||||||
telemetry: Option<Arc<Telemetry>>,
|
telemetry: Option<Arc<Telemetry>>,
|
||||||
builder: Arc<PromptBuilder>,
|
builder: Arc<PromptBuilder>,
|
||||||
|
is_insertion: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Codegen {
|
impl Codegen {
|
||||||
@@ -2284,8 +2315,10 @@ impl Codegen {
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
let mut this = Self {
|
let mut this = Self {
|
||||||
|
is_insertion: range.to_offset(&buffer.read(cx).snapshot(cx)).is_empty(),
|
||||||
alternatives: vec![codegen],
|
alternatives: vec![codegen],
|
||||||
active_alternative: 0,
|
active_alternative: 0,
|
||||||
|
seen_alternatives: HashSet::default(),
|
||||||
subscriptions: Vec::new(),
|
subscriptions: Vec::new(),
|
||||||
buffer,
|
buffer,
|
||||||
range,
|
range,
|
||||||
@@ -2338,6 +2371,7 @@ impl Codegen {
|
|||||||
fn activate(&mut self, index: usize, cx: &mut ModelContext<Self>) {
|
fn activate(&mut self, index: usize, cx: &mut ModelContext<Self>) {
|
||||||
self.active_alternative()
|
self.active_alternative()
|
||||||
.update(cx, |codegen, cx| codegen.set_active(false, cx));
|
.update(cx, |codegen, cx| codegen.set_active(false, cx));
|
||||||
|
self.seen_alternatives.insert(index);
|
||||||
self.active_alternative = index;
|
self.active_alternative = index;
|
||||||
self.active_alternative()
|
self.active_alternative()
|
||||||
.update(cx, |codegen, cx| codegen.set_active(true, cx));
|
.update(cx, |codegen, cx| codegen.set_active(true, cx));
|
||||||
@@ -2467,6 +2501,9 @@ pub struct CodegenAlternative {
|
|||||||
active: bool,
|
active: bool,
|
||||||
edits: Vec<(Range<Anchor>, String)>,
|
edits: Vec<(Range<Anchor>, String)>,
|
||||||
line_operations: Vec<LineOperation>,
|
line_operations: Vec<LineOperation>,
|
||||||
|
request: Option<LanguageModelRequest>,
|
||||||
|
elapsed_time: Option<f64>,
|
||||||
|
message_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum CodegenStatus {
|
enum CodegenStatus {
|
||||||
@@ -2525,6 +2562,7 @@ impl CodegenAlternative {
|
|||||||
buffer: buffer.clone(),
|
buffer: buffer.clone(),
|
||||||
old_buffer,
|
old_buffer,
|
||||||
edit_position: None,
|
edit_position: None,
|
||||||
|
message_id: None,
|
||||||
snapshot,
|
snapshot,
|
||||||
last_equal_ranges: Default::default(),
|
last_equal_ranges: Default::default(),
|
||||||
transformation_transaction_id: None,
|
transformation_transaction_id: None,
|
||||||
@@ -2538,6 +2576,8 @@ impl CodegenAlternative {
|
|||||||
edits: Vec::new(),
|
edits: Vec::new(),
|
||||||
line_operations: Vec::new(),
|
line_operations: Vec::new(),
|
||||||
range,
|
range,
|
||||||
|
request: None,
|
||||||
|
elapsed_time: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2627,19 +2667,20 @@ impl CodegenAlternative {
|
|||||||
|
|
||||||
self.edit_position = Some(self.range.start.bias_right(&self.snapshot));
|
self.edit_position = Some(self.range.start.bias_right(&self.snapshot));
|
||||||
|
|
||||||
|
let api_key = model.api_key(cx);
|
||||||
let telemetry_id = model.telemetry_id();
|
let telemetry_id = model.telemetry_id();
|
||||||
let provider_id = model.provider_id();
|
let provider_id = model.provider_id();
|
||||||
let chunks: LocalBoxFuture<Result<BoxStream<Result<String>>>> =
|
let stream: LocalBoxFuture<Result<LanguageModelTextStream>> =
|
||||||
if user_prompt.trim().to_lowercase() == "delete" {
|
if user_prompt.trim().to_lowercase() == "delete" {
|
||||||
async { Ok(stream::empty().boxed()) }.boxed_local()
|
async { Ok(LanguageModelTextStream::default()) }.boxed_local()
|
||||||
} else {
|
} else {
|
||||||
let request = self.build_request(user_prompt, assistant_panel_context, cx)?;
|
let request = self.build_request(user_prompt, assistant_panel_context, cx)?;
|
||||||
|
self.request = Some(request.clone());
|
||||||
|
|
||||||
let chunks = cx
|
cx.spawn(|_, cx| async move { model.stream_completion_text(request, &cx).await })
|
||||||
.spawn(|_, cx| async move { model.stream_completion_text(request, &cx).await });
|
.boxed_local()
|
||||||
async move { Ok(chunks.await?.boxed()) }.boxed_local()
|
|
||||||
};
|
};
|
||||||
self.handle_stream(telemetry_id, provider_id.to_string(), chunks, cx);
|
self.handle_stream(telemetry_id, provider_id.to_string(), api_key, stream, cx);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2678,7 +2719,7 @@ impl CodegenAlternative {
|
|||||||
|
|
||||||
let prompt = self
|
let prompt = self
|
||||||
.builder
|
.builder
|
||||||
.generate_content_prompt(user_prompt, language_name, buffer, range)
|
.generate_inline_transformation_prompt(user_prompt, language_name, buffer, range)
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to generate content prompt: {}", e))?;
|
.map_err(|e| anyhow::anyhow!("Failed to generate content prompt: {}", e))?;
|
||||||
|
|
||||||
let mut messages = Vec::new();
|
let mut messages = Vec::new();
|
||||||
@@ -2704,9 +2745,11 @@ impl CodegenAlternative {
|
|||||||
&mut self,
|
&mut self,
|
||||||
model_telemetry_id: String,
|
model_telemetry_id: String,
|
||||||
model_provider_id: String,
|
model_provider_id: String,
|
||||||
stream: impl 'static + Future<Output = Result<BoxStream<'static, Result<String>>>>,
|
model_api_key: Option<String>,
|
||||||
|
stream: impl 'static + Future<Output = Result<LanguageModelTextStream>>,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) {
|
) {
|
||||||
|
let start_time = Instant::now();
|
||||||
let snapshot = self.snapshot.clone();
|
let snapshot = self.snapshot.clone();
|
||||||
let selected_text = snapshot
|
let selected_text = snapshot
|
||||||
.text_for_range(self.range.start..self.range.end)
|
.text_for_range(self.range.start..self.range.end)
|
||||||
@@ -2733,6 +2776,7 @@ impl CodegenAlternative {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let http_client = cx.http_client().clone();
|
||||||
let telemetry = self.telemetry.clone();
|
let telemetry = self.telemetry.clone();
|
||||||
let language_name = {
|
let language_name = {
|
||||||
let multibuffer = self.buffer.read(cx);
|
let multibuffer = self.buffer.read(cx);
|
||||||
@@ -2748,15 +2792,21 @@ impl CodegenAlternative {
|
|||||||
let mut edit_start = self.range.start.to_offset(&snapshot);
|
let mut edit_start = self.range.start.to_offset(&snapshot);
|
||||||
self.generation = cx.spawn(|codegen, mut cx| {
|
self.generation = cx.spawn(|codegen, mut cx| {
|
||||||
async move {
|
async move {
|
||||||
let chunks = stream.await;
|
let stream = stream.await;
|
||||||
|
let message_id = stream
|
||||||
|
.as_ref()
|
||||||
|
.ok()
|
||||||
|
.and_then(|stream| stream.message_id.clone());
|
||||||
let generate = async {
|
let generate = async {
|
||||||
let (mut diff_tx, mut diff_rx) = mpsc::channel(1);
|
let (mut diff_tx, mut diff_rx) = mpsc::channel(1);
|
||||||
|
let executor = cx.background_executor().clone();
|
||||||
|
let message_id = message_id.clone();
|
||||||
let line_based_stream_diff: Task<anyhow::Result<()>> =
|
let line_based_stream_diff: Task<anyhow::Result<()>> =
|
||||||
cx.background_executor().spawn(async move {
|
cx.background_executor().spawn(async move {
|
||||||
let mut response_latency = None;
|
let mut response_latency = None;
|
||||||
let request_start = Instant::now();
|
let request_start = Instant::now();
|
||||||
let diff = async {
|
let diff = async {
|
||||||
let chunks = StripInvalidSpans::new(chunks?);
|
let chunks = StripInvalidSpans::new(stream?.stream);
|
||||||
futures::pin_mut!(chunks);
|
futures::pin_mut!(chunks);
|
||||||
let mut diff = StreamingDiff::new(selected_text.to_string());
|
let mut diff = StreamingDiff::new(selected_text.to_string());
|
||||||
let mut line_diff = LineDiff::default();
|
let mut line_diff = LineDiff::default();
|
||||||
@@ -2852,9 +2902,10 @@ impl CodegenAlternative {
|
|||||||
|
|
||||||
let error_message =
|
let error_message =
|
||||||
result.as_ref().err().map(|error| error.to_string());
|
result.as_ref().err().map(|error| error.to_string());
|
||||||
if let Some(telemetry) = telemetry {
|
report_assistant_event(
|
||||||
telemetry.report_assistant_event(AssistantEvent {
|
AssistantEvent {
|
||||||
conversation_id: None,
|
conversation_id: None,
|
||||||
|
message_id,
|
||||||
kind: AssistantKind::Inline,
|
kind: AssistantKind::Inline,
|
||||||
phase: AssistantPhase::Response,
|
phase: AssistantPhase::Response,
|
||||||
model: model_telemetry_id,
|
model: model_telemetry_id,
|
||||||
@@ -2862,8 +2913,12 @@ impl CodegenAlternative {
|
|||||||
response_latency,
|
response_latency,
|
||||||
error_message,
|
error_message,
|
||||||
language_name: language_name.map(|name| name.to_proto()),
|
language_name: language_name.map(|name| name.to_proto()),
|
||||||
});
|
},
|
||||||
}
|
telemetry,
|
||||||
|
http_client,
|
||||||
|
model_api_key,
|
||||||
|
&executor,
|
||||||
|
);
|
||||||
|
|
||||||
result?;
|
result?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -2923,14 +2978,18 @@ impl CodegenAlternative {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let result = generate.await;
|
let result = generate.await;
|
||||||
|
let elapsed_time = start_time.elapsed().as_secs_f64();
|
||||||
|
|
||||||
codegen
|
codegen
|
||||||
.update(&mut cx, |this, cx| {
|
.update(&mut cx, |this, cx| {
|
||||||
|
this.message_id = message_id;
|
||||||
this.last_equal_ranges.clear();
|
this.last_equal_ranges.clear();
|
||||||
if let Err(error) = result {
|
if let Err(error) = result {
|
||||||
this.status = CodegenStatus::Error(error);
|
this.status = CodegenStatus::Error(error);
|
||||||
} else {
|
} else {
|
||||||
this.status = CodegenStatus::Done;
|
this.status = CodegenStatus::Done;
|
||||||
}
|
}
|
||||||
|
this.elapsed_time = Some(elapsed_time);
|
||||||
cx.emit(CodegenEvent::Finished);
|
cx.emit(CodegenEvent::Finished);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})
|
})
|
||||||
@@ -3277,6 +3336,10 @@ impl CodeActionProvider for AssistantCodeActionProvider {
|
|||||||
range: Range<text::Anchor>,
|
range: Range<text::Anchor>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> Task<Result<Vec<CodeAction>>> {
|
) -> Task<Result<Vec<CodeAction>>> {
|
||||||
|
if !AssistantSettings::get_global(cx).enabled {
|
||||||
|
return Task::ready(Ok(Vec::new()));
|
||||||
|
}
|
||||||
|
|
||||||
let snapshot = buffer.read(cx).snapshot();
|
let snapshot = buffer.read(cx).snapshot();
|
||||||
let mut range = range.to_point(&snapshot);
|
let mut range = range.to_point(&snapshot);
|
||||||
|
|
||||||
@@ -3471,15 +3534,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
let (chunks_tx, chunks_rx) = mpsc::unbounded();
|
let chunks_tx = simulate_response_stream(codegen.clone(), cx);
|
||||||
codegen.update(cx, |codegen, cx| {
|
|
||||||
codegen.handle_stream(
|
|
||||||
String::new(),
|
|
||||||
String::new(),
|
|
||||||
future::ready(Ok(chunks_rx.map(Ok).boxed())),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut new_text = concat!(
|
let mut new_text = concat!(
|
||||||
" let mut x = 0;\n",
|
" let mut x = 0;\n",
|
||||||
@@ -3543,15 +3598,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
let (chunks_tx, chunks_rx) = mpsc::unbounded();
|
let chunks_tx = simulate_response_stream(codegen.clone(), cx);
|
||||||
codegen.update(cx, |codegen, cx| {
|
|
||||||
codegen.handle_stream(
|
|
||||||
String::new(),
|
|
||||||
String::new(),
|
|
||||||
future::ready(Ok(chunks_rx.map(Ok).boxed())),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.background_executor.run_until_parked();
|
cx.background_executor.run_until_parked();
|
||||||
|
|
||||||
@@ -3618,15 +3665,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
let (chunks_tx, chunks_rx) = mpsc::unbounded();
|
let chunks_tx = simulate_response_stream(codegen.clone(), cx);
|
||||||
codegen.update(cx, |codegen, cx| {
|
|
||||||
codegen.handle_stream(
|
|
||||||
String::new(),
|
|
||||||
String::new(),
|
|
||||||
future::ready(Ok(chunks_rx.map(Ok).boxed())),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.background_executor.run_until_parked();
|
cx.background_executor.run_until_parked();
|
||||||
|
|
||||||
@@ -3692,16 +3731,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
let (chunks_tx, chunks_rx) = mpsc::unbounded();
|
let chunks_tx = simulate_response_stream(codegen.clone(), cx);
|
||||||
codegen.update(cx, |codegen, cx| {
|
|
||||||
codegen.handle_stream(
|
|
||||||
String::new(),
|
|
||||||
String::new(),
|
|
||||||
future::ready(Ok(chunks_rx.map(Ok).boxed())),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
let new_text = concat!(
|
let new_text = concat!(
|
||||||
"func main() {\n",
|
"func main() {\n",
|
||||||
"\tx := 0\n",
|
"\tx := 0\n",
|
||||||
@@ -3756,16 +3786,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
let (chunks_tx, chunks_rx) = mpsc::unbounded();
|
let chunks_tx = simulate_response_stream(codegen.clone(), cx);
|
||||||
codegen.update(cx, |codegen, cx| {
|
|
||||||
codegen.handle_stream(
|
|
||||||
String::new(),
|
|
||||||
String::new(),
|
|
||||||
future::ready(Ok(chunks_rx.map(Ok).boxed())),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
chunks_tx
|
chunks_tx
|
||||||
.unbounded_send("let mut x = 0;\nx += 1;".to_string())
|
.unbounded_send("let mut x = 0;\nx += 1;".to_string())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -3839,6 +3860,26 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn simulate_response_stream(
|
||||||
|
codegen: Model<CodegenAlternative>,
|
||||||
|
cx: &mut TestAppContext,
|
||||||
|
) -> mpsc::UnboundedSender<String> {
|
||||||
|
let (chunks_tx, chunks_rx) = mpsc::unbounded();
|
||||||
|
codegen.update(cx, |codegen, cx| {
|
||||||
|
codegen.handle_stream(
|
||||||
|
String::new(),
|
||||||
|
String::new(),
|
||||||
|
None,
|
||||||
|
future::ready(Ok(LanguageModelTextStream {
|
||||||
|
message_id: None,
|
||||||
|
stream: chunks_rx.map(Ok).boxed(),
|
||||||
|
})),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
chunks_tx
|
||||||
|
}
|
||||||
|
|
||||||
fn rust_lang() -> Language {
|
fn rust_lang() -> Language {
|
||||||
Language::new(
|
Language::new(
|
||||||
LanguageConfig {
|
LanguageConfig {
|
||||||
|
|||||||
@@ -158,39 +158,34 @@ impl PickerDelegate for ModelPickerDelegate {
|
|||||||
.spacing(ListItemSpacing::Sparse)
|
.spacing(ListItemSpacing::Sparse)
|
||||||
.selected(selected)
|
.selected(selected)
|
||||||
.start_slot(
|
.start_slot(
|
||||||
div().pr_1().child(
|
div().pr_0p5().child(
|
||||||
Icon::new(model_info.icon)
|
Icon::new(model_info.icon)
|
||||||
.color(Color::Muted)
|
.color(Color::Muted)
|
||||||
.size(IconSize::Medium),
|
.size(IconSize::Medium),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex().w_full().justify_between().min_w(px(200.)).child(
|
||||||
.w_full()
|
h_flex()
|
||||||
.justify_between()
|
.gap_1p5()
|
||||||
.font_buffer(cx)
|
.child(Label::new(model_info.model.name().0.clone()))
|
||||||
.min_w(px(240.))
|
.child(
|
||||||
.child(
|
Label::new(provider_name)
|
||||||
h_flex()
|
.size(LabelSize::XSmall)
|
||||||
.gap_2()
|
.color(Color::Muted),
|
||||||
.child(Label::new(model_info.model.name().0.clone()))
|
)
|
||||||
.child(
|
.children(match model_info.availability {
|
||||||
Label::new(provider_name)
|
LanguageModelAvailability::Public => None,
|
||||||
.size(LabelSize::XSmall)
|
LanguageModelAvailability::RequiresPlan(Plan::Free) => None,
|
||||||
.color(Color::Muted),
|
LanguageModelAvailability::RequiresPlan(Plan::ZedPro) => {
|
||||||
)
|
show_badges.then(|| {
|
||||||
.children(match model_info.availability {
|
Label::new("Pro")
|
||||||
LanguageModelAvailability::Public => None,
|
.size(LabelSize::XSmall)
|
||||||
LanguageModelAvailability::RequiresPlan(Plan::Free) => None,
|
.color(Color::Muted)
|
||||||
LanguageModelAvailability::RequiresPlan(Plan::ZedPro) => {
|
})
|
||||||
show_badges.then(|| {
|
}
|
||||||
Label::new("Pro")
|
}),
|
||||||
.size(LabelSize::XSmall)
|
),
|
||||||
.color(Color::Muted)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.end_slot(div().when(model_info.is_selected, |this| {
|
.end_slot(div().when(model_info.is_selected, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
@@ -212,7 +207,7 @@ impl PickerDelegate for ModelPickerDelegate {
|
|||||||
h_flex()
|
h_flex()
|
||||||
.w_full()
|
.w_full()
|
||||||
.border_t_1()
|
.border_t_1()
|
||||||
.border_color(cx.theme().colors().border)
|
.border_color(cx.theme().colors().border_variant)
|
||||||
.p_1()
|
.p_1()
|
||||||
.gap_4()
|
.gap_4()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
|
|||||||
@@ -33,21 +33,21 @@ pub enum AssistantEditKind {
|
|||||||
Update {
|
Update {
|
||||||
old_text: String,
|
old_text: String,
|
||||||
new_text: String,
|
new_text: String,
|
||||||
description: String,
|
description: Option<String>,
|
||||||
},
|
},
|
||||||
Create {
|
Create {
|
||||||
new_text: String,
|
new_text: String,
|
||||||
description: String,
|
description: Option<String>,
|
||||||
},
|
},
|
||||||
InsertBefore {
|
InsertBefore {
|
||||||
old_text: String,
|
old_text: String,
|
||||||
new_text: String,
|
new_text: String,
|
||||||
description: String,
|
description: Option<String>,
|
||||||
},
|
},
|
||||||
InsertAfter {
|
InsertAfter {
|
||||||
old_text: String,
|
old_text: String,
|
||||||
new_text: String,
|
new_text: String,
|
||||||
description: String,
|
description: Option<String>,
|
||||||
},
|
},
|
||||||
Delete {
|
Delete {
|
||||||
old_text: String,
|
old_text: String,
|
||||||
@@ -86,19 +86,37 @@ enum SearchDirection {
|
|||||||
Diagonal,
|
Diagonal,
|
||||||
}
|
}
|
||||||
|
|
||||||
// A measure of the currently quality of an in-progress fuzzy search.
|
|
||||||
//
|
|
||||||
// Uses 60 bits to store a numeric cost, and 4 bits to store the preceding
|
|
||||||
// operation in the search.
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
struct SearchState {
|
struct SearchState {
|
||||||
score: u32,
|
cost: u32,
|
||||||
direction: SearchDirection,
|
direction: SearchDirection,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SearchState {
|
impl SearchState {
|
||||||
fn new(score: u32, direction: SearchDirection) -> Self {
|
fn new(cost: u32, direction: SearchDirection) -> Self {
|
||||||
Self { score, direction }
|
Self { cost, direction }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SearchMatrix {
|
||||||
|
cols: usize,
|
||||||
|
data: Vec<SearchState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SearchMatrix {
|
||||||
|
fn new(rows: usize, cols: usize) -> Self {
|
||||||
|
SearchMatrix {
|
||||||
|
cols,
|
||||||
|
data: vec![SearchState::new(0, SearchDirection::Diagonal); rows * cols],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get(&self, row: usize, col: usize) -> SearchState {
|
||||||
|
self.data[row * self.cols + col]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set(&mut self, row: usize, col: usize, cost: SearchState) {
|
||||||
|
self.data[row * self.cols + col] = cost;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,23 +205,23 @@ impl AssistantEdit {
|
|||||||
"update" => AssistantEditKind::Update {
|
"update" => AssistantEditKind::Update {
|
||||||
old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
|
old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
|
||||||
new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
|
new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
|
||||||
description: description.ok_or_else(|| anyhow!("missing description"))?,
|
description,
|
||||||
},
|
},
|
||||||
"insert_before" => AssistantEditKind::InsertBefore {
|
"insert_before" => AssistantEditKind::InsertBefore {
|
||||||
old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
|
old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
|
||||||
new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
|
new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
|
||||||
description: description.ok_or_else(|| anyhow!("missing description"))?,
|
description,
|
||||||
},
|
},
|
||||||
"insert_after" => AssistantEditKind::InsertAfter {
|
"insert_after" => AssistantEditKind::InsertAfter {
|
||||||
old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
|
old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
|
||||||
new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
|
new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
|
||||||
description: description.ok_or_else(|| anyhow!("missing description"))?,
|
description,
|
||||||
},
|
},
|
||||||
"delete" => AssistantEditKind::Delete {
|
"delete" => AssistantEditKind::Delete {
|
||||||
old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
|
old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
|
||||||
},
|
},
|
||||||
"create" => AssistantEditKind::Create {
|
"create" => AssistantEditKind::Create {
|
||||||
description: description.ok_or_else(|| anyhow!("missing description"))?,
|
description,
|
||||||
new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
|
new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
|
||||||
},
|
},
|
||||||
_ => Err(anyhow!("unknown operation {operation:?}"))?,
|
_ => Err(anyhow!("unknown operation {operation:?}"))?,
|
||||||
@@ -264,7 +282,7 @@ impl AssistantEditKind {
|
|||||||
ResolvedEdit {
|
ResolvedEdit {
|
||||||
range,
|
range,
|
||||||
new_text,
|
new_text,
|
||||||
description: Some(description),
|
description,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::Create {
|
Self::Create {
|
||||||
@@ -272,7 +290,7 @@ impl AssistantEditKind {
|
|||||||
description,
|
description,
|
||||||
} => ResolvedEdit {
|
} => ResolvedEdit {
|
||||||
range: text::Anchor::MIN..text::Anchor::MAX,
|
range: text::Anchor::MIN..text::Anchor::MAX,
|
||||||
description: Some(description),
|
description,
|
||||||
new_text,
|
new_text,
|
||||||
},
|
},
|
||||||
Self::InsertBefore {
|
Self::InsertBefore {
|
||||||
@@ -285,7 +303,7 @@ impl AssistantEditKind {
|
|||||||
ResolvedEdit {
|
ResolvedEdit {
|
||||||
range: range.start..range.start,
|
range: range.start..range.start,
|
||||||
new_text,
|
new_text,
|
||||||
description: Some(description),
|
description,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::InsertAfter {
|
Self::InsertAfter {
|
||||||
@@ -298,7 +316,7 @@ impl AssistantEditKind {
|
|||||||
ResolvedEdit {
|
ResolvedEdit {
|
||||||
range: range.end..range.end,
|
range: range.end..range.end,
|
||||||
new_text,
|
new_text,
|
||||||
description: Some(description),
|
description,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::Delete { old_text } => {
|
Self::Delete { old_text } => {
|
||||||
@@ -314,44 +332,29 @@ impl AssistantEditKind {
|
|||||||
|
|
||||||
fn resolve_location(buffer: &text::BufferSnapshot, search_query: &str) -> Range<text::Anchor> {
|
fn resolve_location(buffer: &text::BufferSnapshot, search_query: &str) -> Range<text::Anchor> {
|
||||||
const INSERTION_COST: u32 = 3;
|
const INSERTION_COST: u32 = 3;
|
||||||
|
const DELETION_COST: u32 = 10;
|
||||||
const WHITESPACE_INSERTION_COST: u32 = 1;
|
const WHITESPACE_INSERTION_COST: u32 = 1;
|
||||||
const DELETION_COST: u32 = 3;
|
|
||||||
const WHITESPACE_DELETION_COST: u32 = 1;
|
const WHITESPACE_DELETION_COST: u32 = 1;
|
||||||
const EQUALITY_BONUS: u32 = 5;
|
|
||||||
|
|
||||||
struct Matrix {
|
|
||||||
cols: usize,
|
|
||||||
data: Vec<SearchState>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Matrix {
|
|
||||||
fn new(rows: usize, cols: usize) -> Self {
|
|
||||||
Matrix {
|
|
||||||
cols,
|
|
||||||
data: vec![SearchState::new(0, SearchDirection::Diagonal); rows * cols],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get(&self, row: usize, col: usize) -> SearchState {
|
|
||||||
self.data[row * self.cols + col]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set(&mut self, row: usize, col: usize, cost: SearchState) {
|
|
||||||
self.data[row * self.cols + col] = cost;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let buffer_len = buffer.len();
|
let buffer_len = buffer.len();
|
||||||
let query_len = search_query.len();
|
let query_len = search_query.len();
|
||||||
let mut matrix = Matrix::new(query_len + 1, buffer_len + 1);
|
let mut matrix = SearchMatrix::new(query_len + 1, buffer_len + 1);
|
||||||
|
let mut leading_deletion_cost = 0_u32;
|
||||||
for (row, query_byte) in search_query.bytes().enumerate() {
|
for (row, query_byte) in search_query.bytes().enumerate() {
|
||||||
|
let deletion_cost = if query_byte.is_ascii_whitespace() {
|
||||||
|
WHITESPACE_DELETION_COST
|
||||||
|
} else {
|
||||||
|
DELETION_COST
|
||||||
|
};
|
||||||
|
|
||||||
|
leading_deletion_cost = leading_deletion_cost.saturating_add(deletion_cost);
|
||||||
|
matrix.set(
|
||||||
|
row + 1,
|
||||||
|
0,
|
||||||
|
SearchState::new(leading_deletion_cost, SearchDirection::Diagonal),
|
||||||
|
);
|
||||||
|
|
||||||
for (col, buffer_byte) in buffer.bytes_in_range(0..buffer.len()).flatten().enumerate() {
|
for (col, buffer_byte) in buffer.bytes_in_range(0..buffer.len()).flatten().enumerate() {
|
||||||
let deletion_cost = if query_byte.is_ascii_whitespace() {
|
|
||||||
WHITESPACE_DELETION_COST
|
|
||||||
} else {
|
|
||||||
DELETION_COST
|
|
||||||
};
|
|
||||||
let insertion_cost = if buffer_byte.is_ascii_whitespace() {
|
let insertion_cost = if buffer_byte.is_ascii_whitespace() {
|
||||||
WHITESPACE_INSERTION_COST
|
WHITESPACE_INSERTION_COST
|
||||||
} else {
|
} else {
|
||||||
@@ -359,38 +362,35 @@ impl AssistantEditKind {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let up = SearchState::new(
|
let up = SearchState::new(
|
||||||
matrix.get(row, col + 1).score.saturating_sub(deletion_cost),
|
matrix.get(row, col + 1).cost.saturating_add(deletion_cost),
|
||||||
SearchDirection::Up,
|
SearchDirection::Up,
|
||||||
);
|
);
|
||||||
let left = SearchState::new(
|
let left = SearchState::new(
|
||||||
matrix
|
matrix.get(row + 1, col).cost.saturating_add(insertion_cost),
|
||||||
.get(row + 1, col)
|
|
||||||
.score
|
|
||||||
.saturating_sub(insertion_cost),
|
|
||||||
SearchDirection::Left,
|
SearchDirection::Left,
|
||||||
);
|
);
|
||||||
let diagonal = SearchState::new(
|
let diagonal = SearchState::new(
|
||||||
if query_byte == *buffer_byte {
|
if query_byte == *buffer_byte {
|
||||||
matrix.get(row, col).score.saturating_add(EQUALITY_BONUS)
|
matrix.get(row, col).cost
|
||||||
} else {
|
} else {
|
||||||
matrix
|
matrix
|
||||||
.get(row, col)
|
.get(row, col)
|
||||||
.score
|
.cost
|
||||||
.saturating_sub(deletion_cost + insertion_cost)
|
.saturating_add(deletion_cost + insertion_cost)
|
||||||
},
|
},
|
||||||
SearchDirection::Diagonal,
|
SearchDirection::Diagonal,
|
||||||
);
|
);
|
||||||
matrix.set(row + 1, col + 1, up.max(left).max(diagonal));
|
matrix.set(row + 1, col + 1, up.min(left).min(diagonal));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Traceback to find the best match
|
// Traceback to find the best match
|
||||||
let mut best_buffer_end = buffer_len;
|
let mut best_buffer_end = buffer_len;
|
||||||
let mut best_score = 0;
|
let mut best_cost = u32::MAX;
|
||||||
for col in 1..=buffer_len {
|
for col in 1..=buffer_len {
|
||||||
let score = matrix.get(query_len, col).score;
|
let cost = matrix.get(query_len, col).cost;
|
||||||
if score > best_score {
|
if cost < best_cost {
|
||||||
best_score = score;
|
best_cost = cost;
|
||||||
best_buffer_end = col;
|
best_buffer_end = col;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -560,89 +560,84 @@ mod tests {
|
|||||||
language_settings::AllLanguageSettings, Language, LanguageConfig, LanguageMatcher,
|
language_settings::AllLanguageSettings, Language, LanguageConfig, LanguageMatcher,
|
||||||
};
|
};
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
use text::{OffsetRangeExt, Point};
|
|
||||||
use ui::BorrowAppContext;
|
use ui::BorrowAppContext;
|
||||||
use unindent::Unindent as _;
|
use unindent::Unindent as _;
|
||||||
|
use util::test::{generate_marked_text, marked_text_ranges};
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_resolve_location(cx: &mut AppContext) {
|
fn test_resolve_location(cx: &mut AppContext) {
|
||||||
{
|
assert_location_resolution(
|
||||||
let buffer = cx.new_model(|cx| {
|
concat!(
|
||||||
Buffer::local(
|
" Lorem\n",
|
||||||
concat!(
|
"« ipsum\n",
|
||||||
" Lorem\n",
|
" dolor sit amet»\n",
|
||||||
" ipsum\n",
|
" consecteur",
|
||||||
" dolor sit amet\n",
|
),
|
||||||
" consecteur",
|
"ipsum\ndolor",
|
||||||
),
|
cx,
|
||||||
cx,
|
);
|
||||||
)
|
|
||||||
});
|
|
||||||
let snapshot = buffer.read(cx).snapshot();
|
|
||||||
assert_eq!(
|
|
||||||
AssistantEditKind::resolve_location(&snapshot, "ipsum\ndolor").to_point(&snapshot),
|
|
||||||
Point::new(1, 0)..Point::new(2, 18)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
assert_location_resolution(
|
||||||
let buffer = cx.new_model(|cx| {
|
&"
|
||||||
Buffer::local(
|
«fn foo1(a: usize) -> usize {
|
||||||
concat!(
|
40
|
||||||
"fn foo1(a: usize) -> usize {\n",
|
}»
|
||||||
" 40\n",
|
|
||||||
"}\n",
|
|
||||||
"\n",
|
|
||||||
"fn foo2(b: usize) -> usize {\n",
|
|
||||||
" 42\n",
|
|
||||||
"}\n",
|
|
||||||
),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
let snapshot = buffer.read(cx).snapshot();
|
|
||||||
assert_eq!(
|
|
||||||
AssistantEditKind::resolve_location(&snapshot, "fn foo1(b: usize) {\n40\n}")
|
|
||||||
.to_point(&snapshot),
|
|
||||||
Point::new(0, 0)..Point::new(2, 1)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
fn foo2(b: usize) -> usize {
|
||||||
let buffer = cx.new_model(|cx| {
|
42
|
||||||
Buffer::local(
|
}
|
||||||
concat!(
|
"
|
||||||
"fn main() {\n",
|
.unindent(),
|
||||||
" Foo\n",
|
"fn foo1(b: usize) {\n40\n}",
|
||||||
" .bar()\n",
|
cx,
|
||||||
" .baz()\n",
|
);
|
||||||
" .qux()\n",
|
|
||||||
"}\n",
|
assert_location_resolution(
|
||||||
"\n",
|
&"
|
||||||
"fn foo2(b: usize) -> usize {\n",
|
fn main() {
|
||||||
" 42\n",
|
« Foo
|
||||||
"}\n",
|
.bar()
|
||||||
),
|
.baz()
|
||||||
cx,
|
.qux()»
|
||||||
)
|
}
|
||||||
});
|
|
||||||
let snapshot = buffer.read(cx).snapshot();
|
fn foo2(b: usize) -> usize {
|
||||||
assert_eq!(
|
42
|
||||||
AssistantEditKind::resolve_location(&snapshot, "Foo.bar.baz.qux()")
|
}
|
||||||
.to_point(&snapshot),
|
"
|
||||||
Point::new(1, 0)..Point::new(4, 14)
|
.unindent(),
|
||||||
);
|
"Foo.bar.baz.qux()",
|
||||||
}
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_location_resolution(
|
||||||
|
&"
|
||||||
|
class Something {
|
||||||
|
one() { return 1; }
|
||||||
|
« two() { return 2222; }
|
||||||
|
three() { return 333; }
|
||||||
|
four() { return 4444; }
|
||||||
|
five() { return 5555; }
|
||||||
|
six() { return 6666; }
|
||||||
|
» seven() { return 7; }
|
||||||
|
eight() { return 8; }
|
||||||
|
}
|
||||||
|
"
|
||||||
|
.unindent(),
|
||||||
|
&"
|
||||||
|
two() { return 2222; }
|
||||||
|
four() { return 4444; }
|
||||||
|
five() { return 5555; }
|
||||||
|
six() { return 6666; }
|
||||||
|
"
|
||||||
|
.unindent(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_resolve_edits(cx: &mut AppContext) {
|
fn test_resolve_edits(cx: &mut AppContext) {
|
||||||
let settings_store = SettingsStore::test(cx);
|
init_test(cx);
|
||||||
cx.set_global(settings_store);
|
|
||||||
language::init(cx);
|
|
||||||
cx.update_global::<SettingsStore, _>(|settings, cx| {
|
|
||||||
settings.update_user_settings::<AllLanguageSettings>(cx, |_| {});
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_edits(
|
assert_edits(
|
||||||
"
|
"
|
||||||
@@ -675,7 +670,7 @@ mod tests {
|
|||||||
last_name: String,
|
last_name: String,
|
||||||
"
|
"
|
||||||
.unindent(),
|
.unindent(),
|
||||||
description: "".into(),
|
description: None,
|
||||||
},
|
},
|
||||||
AssistantEditKind::Update {
|
AssistantEditKind::Update {
|
||||||
old_text: "
|
old_text: "
|
||||||
@@ -690,7 +685,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
.unindent(),
|
.unindent(),
|
||||||
description: "".into(),
|
description: None,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"
|
"
|
||||||
@@ -717,7 +712,6 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Ensure InsertBefore merges correctly with Update of the same text
|
// Ensure InsertBefore merges correctly with Update of the same text
|
||||||
|
|
||||||
assert_edits(
|
assert_edits(
|
||||||
"
|
"
|
||||||
fn foo() {
|
fn foo() {
|
||||||
@@ -735,7 +729,7 @@ mod tests {
|
|||||||
qux();
|
qux();
|
||||||
}"
|
}"
|
||||||
.unindent(),
|
.unindent(),
|
||||||
description: "implement bar".into(),
|
description: Some("implement bar".into()),
|
||||||
},
|
},
|
||||||
AssistantEditKind::Update {
|
AssistantEditKind::Update {
|
||||||
old_text: "
|
old_text: "
|
||||||
@@ -748,7 +742,7 @@ mod tests {
|
|||||||
bar();
|
bar();
|
||||||
}"
|
}"
|
||||||
.unindent(),
|
.unindent(),
|
||||||
description: "call bar in foo".into(),
|
description: Some("call bar in foo".into()),
|
||||||
},
|
},
|
||||||
AssistantEditKind::InsertAfter {
|
AssistantEditKind::InsertAfter {
|
||||||
old_text: "
|
old_text: "
|
||||||
@@ -763,7 +757,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
.unindent(),
|
.unindent(),
|
||||||
description: "implement qux".into(),
|
description: Some("implement qux".into()),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"
|
"
|
||||||
@@ -782,6 +776,153 @@ mod tests {
|
|||||||
.unindent(),
|
.unindent(),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Correctly indent new text when replacing multiple adjacent indented blocks.
|
||||||
|
assert_edits(
|
||||||
|
"
|
||||||
|
impl Numbers {
|
||||||
|
fn one() {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn two() {
|
||||||
|
2
|
||||||
|
}
|
||||||
|
|
||||||
|
fn three() {
|
||||||
|
3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"
|
||||||
|
.unindent(),
|
||||||
|
vec![
|
||||||
|
AssistantEditKind::Update {
|
||||||
|
old_text: "
|
||||||
|
fn one() {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
"
|
||||||
|
.unindent(),
|
||||||
|
new_text: "
|
||||||
|
fn one() {
|
||||||
|
101
|
||||||
|
}
|
||||||
|
"
|
||||||
|
.unindent(),
|
||||||
|
description: None,
|
||||||
|
},
|
||||||
|
AssistantEditKind::Update {
|
||||||
|
old_text: "
|
||||||
|
fn two() {
|
||||||
|
2
|
||||||
|
}
|
||||||
|
"
|
||||||
|
.unindent(),
|
||||||
|
new_text: "
|
||||||
|
fn two() {
|
||||||
|
102
|
||||||
|
}
|
||||||
|
"
|
||||||
|
.unindent(),
|
||||||
|
description: None,
|
||||||
|
},
|
||||||
|
AssistantEditKind::Update {
|
||||||
|
old_text: "
|
||||||
|
fn three() {
|
||||||
|
3
|
||||||
|
}
|
||||||
|
"
|
||||||
|
.unindent(),
|
||||||
|
new_text: "
|
||||||
|
fn three() {
|
||||||
|
103
|
||||||
|
}
|
||||||
|
"
|
||||||
|
.unindent(),
|
||||||
|
description: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"
|
||||||
|
impl Numbers {
|
||||||
|
fn one() {
|
||||||
|
101
|
||||||
|
}
|
||||||
|
|
||||||
|
fn two() {
|
||||||
|
102
|
||||||
|
}
|
||||||
|
|
||||||
|
fn three() {
|
||||||
|
103
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"
|
||||||
|
.unindent(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_edits(
|
||||||
|
"
|
||||||
|
impl Person {
|
||||||
|
fn set_name(&mut self, name: String) {
|
||||||
|
self.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> String {
|
||||||
|
return self.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"
|
||||||
|
.unindent(),
|
||||||
|
vec![
|
||||||
|
AssistantEditKind::Update {
|
||||||
|
old_text: "self.name = name;".unindent(),
|
||||||
|
new_text: "self._name = name;".unindent(),
|
||||||
|
description: None,
|
||||||
|
},
|
||||||
|
AssistantEditKind::Update {
|
||||||
|
old_text: "return self.name;\n".unindent(),
|
||||||
|
new_text: "return self._name;\n".unindent(),
|
||||||
|
description: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"
|
||||||
|
impl Person {
|
||||||
|
fn set_name(&mut self, name: String) {
|
||||||
|
self._name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> String {
|
||||||
|
return self._name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"
|
||||||
|
.unindent(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_test(cx: &mut AppContext) {
|
||||||
|
let settings_store = SettingsStore::test(cx);
|
||||||
|
cx.set_global(settings_store);
|
||||||
|
language::init(cx);
|
||||||
|
cx.update_global::<SettingsStore, _>(|settings, cx| {
|
||||||
|
settings.update_user_settings::<AllLanguageSettings>(cx, |_| {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
fn assert_location_resolution(
|
||||||
|
text_with_expected_range: &str,
|
||||||
|
query: &str,
|
||||||
|
cx: &mut AppContext,
|
||||||
|
) {
|
||||||
|
let (text, _) = marked_text_ranges(text_with_expected_range, false);
|
||||||
|
let buffer = cx.new_model(|cx| Buffer::local(text.clone(), cx));
|
||||||
|
let snapshot = buffer.read(cx).snapshot();
|
||||||
|
let range = AssistantEditKind::resolve_location(&snapshot, query).to_offset(&snapshot);
|
||||||
|
let text_with_actual_range = generate_marked_text(&text, &[range], false);
|
||||||
|
pretty_assertions::assert_eq!(text_with_actual_range, text_with_expected_range);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ impl PromptBuilder {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_content_prompt(
|
pub fn generate_inline_transformation_prompt(
|
||||||
&self,
|
&self,
|
||||||
user_prompt: String,
|
user_prompt: String,
|
||||||
language_name: Option<&LanguageName>,
|
language_name: Option<&LanguageName>,
|
||||||
@@ -311,7 +311,7 @@ impl PromptBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_workflow_prompt(&self) -> Result<String, RenderError> {
|
pub fn generate_workflow_prompt(&self) -> Result<String, RenderError> {
|
||||||
self.handlebars.lock().render("edit_workflow", &())
|
self.handlebars.lock().render("suggest_edits", &())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_project_slash_command_prompt(
|
pub fn generate_project_slash_command_prompt(
|
||||||
|
|||||||
@@ -31,10 +31,10 @@ pub mod now_command;
|
|||||||
pub mod project_command;
|
pub mod project_command;
|
||||||
pub mod prompt_command;
|
pub mod prompt_command;
|
||||||
pub mod search_command;
|
pub mod search_command;
|
||||||
|
pub mod streaming_example_command;
|
||||||
pub mod symbols_command;
|
pub mod symbols_command;
|
||||||
pub mod tab_command;
|
pub mod tab_command;
|
||||||
pub mod terminal_command;
|
pub mod terminal_command;
|
||||||
pub mod workflow_command;
|
|
||||||
|
|
||||||
pub(crate) struct SlashCommandCompletionProvider {
|
pub(crate) struct SlashCommandCompletionProvider {
|
||||||
cancel_flag: Mutex<Arc<AtomicBool>>,
|
cancel_flag: Mutex<Arc<AtomicBool>>,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use super::create_label_for_command;
|
|
||||||
use super::{SlashCommand, SlashCommandOutput};
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
|
use assistant_slash_command::{
|
||||||
|
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||||
|
SlashCommandResult,
|
||||||
|
};
|
||||||
use feature_flags::FeatureFlag;
|
use feature_flags::FeatureFlag;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use gpui::{AppContext, AsyncAppContext, Task, WeakView};
|
use gpui::{AppContext, AsyncAppContext, Task, WeakView};
|
||||||
@@ -13,10 +14,12 @@ use language_model::{
|
|||||||
use semantic_index::{FileSummary, SemanticDb};
|
use semantic_index::{FileSummary, SemanticDb};
|
||||||
use smol::channel;
|
use smol::channel;
|
||||||
use std::sync::{atomic::AtomicBool, Arc};
|
use std::sync::{atomic::AtomicBool, Arc};
|
||||||
use ui::{BorrowAppContext, WindowContext};
|
use ui::{prelude::*, BorrowAppContext, WindowContext};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
use crate::slash_command::create_label_for_command;
|
||||||
|
|
||||||
pub struct AutoSlashCommandFeatureFlag;
|
pub struct AutoSlashCommandFeatureFlag;
|
||||||
|
|
||||||
impl FeatureFlag for AutoSlashCommandFeatureFlag {
|
impl FeatureFlag for AutoSlashCommandFeatureFlag {
|
||||||
@@ -34,6 +37,10 @@ impl SlashCommand for AutoCommand {
|
|||||||
"Automatically infer what context to add".into()
|
"Automatically infer what context to add".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn icon(&self) -> IconName {
|
||||||
|
IconName::Wand
|
||||||
|
}
|
||||||
|
|
||||||
fn menu_text(&self) -> String {
|
fn menu_text(&self) -> String {
|
||||||
self.description()
|
self.description()
|
||||||
}
|
}
|
||||||
@@ -92,7 +99,7 @@ impl SlashCommand for AutoCommand {
|
|||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> Task<Result<SlashCommandOutput>> {
|
) -> Task<SlashCommandResult> {
|
||||||
let Some(workspace) = workspace.upgrade() else {
|
let Some(workspace) = workspace.upgrade() else {
|
||||||
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
|
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
|
||||||
};
|
};
|
||||||
@@ -144,7 +151,8 @@ impl SlashCommand for AutoCommand {
|
|||||||
text: prompt,
|
text: prompt,
|
||||||
sections: Vec::new(),
|
sections: Vec::new(),
|
||||||
run_commands_in_text: true,
|
run_commands_in_text: true,
|
||||||
})
|
}
|
||||||
|
.to_event_stream())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use super::{SlashCommand, SlashCommandOutput};
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
|
use assistant_slash_command::{
|
||||||
|
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||||
|
SlashCommandResult,
|
||||||
|
};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::{AppContext, Model, Task, WeakView};
|
use gpui::{AppContext, Model, Task, WeakView};
|
||||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||||
@@ -123,7 +125,7 @@ impl SlashCommand for CargoWorkspaceSlashCommand {
|
|||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> Task<Result<SlashCommandOutput>> {
|
) -> Task<SlashCommandResult> {
|
||||||
let output = workspace.update(cx, |workspace, cx| {
|
let output = workspace.update(cx, |workspace, cx| {
|
||||||
let project = workspace.project().clone();
|
let project = workspace.project().clone();
|
||||||
let fs = workspace.project().read(cx).fs().clone();
|
let fs = workspace.project().read(cx).fs().clone();
|
||||||
@@ -145,7 +147,8 @@ impl SlashCommand for CargoWorkspaceSlashCommand {
|
|||||||
metadata: None,
|
metadata: None,
|
||||||
}],
|
}],
|
||||||
run_commands_in_text: false,
|
run_commands_in_text: false,
|
||||||
})
|
}
|
||||||
|
.to_event_stream())
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
output.unwrap_or_else(|error| Task::ready(Err(error)))
|
output.unwrap_or_else(|error| Task::ready(Err(error)))
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
use super::create_label_for_command;
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use assistant_slash_command::{
|
use assistant_slash_command::{
|
||||||
AfterCompletion, ArgumentCompletion, SlashCommand, SlashCommandOutput,
|
AfterCompletion, ArgumentCompletion, SlashCommand, SlashCommandOutput,
|
||||||
SlashCommandOutputSection,
|
SlashCommandOutputSection, SlashCommandResult,
|
||||||
};
|
};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use context_servers::{
|
use context_servers::{
|
||||||
@@ -17,6 +16,8 @@ use text::LineEnding;
|
|||||||
use ui::{IconName, SharedString};
|
use ui::{IconName, SharedString};
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
use crate::slash_command::create_label_for_command;
|
||||||
|
|
||||||
pub struct ContextServerSlashCommand {
|
pub struct ContextServerSlashCommand {
|
||||||
server_id: String,
|
server_id: String,
|
||||||
prompt: Prompt,
|
prompt: Prompt,
|
||||||
@@ -128,7 +129,7 @@ impl SlashCommand for ContextServerSlashCommand {
|
|||||||
_workspace: WeakView<Workspace>,
|
_workspace: WeakView<Workspace>,
|
||||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> Task<Result<SlashCommandOutput>> {
|
) -> Task<SlashCommandResult> {
|
||||||
let server_id = self.server_id.clone();
|
let server_id = self.server_id.clone();
|
||||||
let prompt_name = self.prompt.name.clone();
|
let prompt_name = self.prompt.name.clone();
|
||||||
|
|
||||||
@@ -145,7 +146,28 @@ impl SlashCommand for ContextServerSlashCommand {
|
|||||||
return Err(anyhow!("Context server not initialized"));
|
return Err(anyhow!("Context server not initialized"));
|
||||||
};
|
};
|
||||||
let result = protocol.run_prompt(&prompt_name, prompt_args).await?;
|
let result = protocol.run_prompt(&prompt_name, prompt_args).await?;
|
||||||
let mut prompt = result.prompt;
|
|
||||||
|
// Check that there are only user roles
|
||||||
|
if result
|
||||||
|
.messages
|
||||||
|
.iter()
|
||||||
|
.any(|msg| !matches!(msg.role, context_servers::types::SamplingRole::User))
|
||||||
|
{
|
||||||
|
return Err(anyhow!(
|
||||||
|
"Prompt contains non-user roles, which is not supported"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract text from user messages into a single prompt string
|
||||||
|
let mut prompt = result
|
||||||
|
.messages
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|msg| match msg.content {
|
||||||
|
context_servers::types::SamplingContent::Text { text } => Some(text),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join("\n\n");
|
||||||
|
|
||||||
// We must normalize the line endings here, since servers might return CR characters.
|
// We must normalize the line endings here, since servers might return CR characters.
|
||||||
LineEnding::normalize(&mut prompt);
|
LineEnding::normalize(&mut prompt);
|
||||||
@@ -163,7 +185,8 @@ impl SlashCommand for ContextServerSlashCommand {
|
|||||||
}],
|
}],
|
||||||
text: prompt,
|
text: prompt,
|
||||||
run_commands_in_text: false,
|
run_commands_in_text: false,
|
||||||
})
|
}
|
||||||
|
.to_event_stream())
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Task::ready(Err(anyhow!("Context server not found")))
|
Task::ready(Err(anyhow!("Context server not found")))
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
use super::{SlashCommand, SlashCommandOutput};
|
|
||||||
use crate::prompt_library::PromptStore;
|
use crate::prompt_library::PromptStore;
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
|
use assistant_slash_command::{
|
||||||
|
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||||
|
SlashCommandResult,
|
||||||
|
};
|
||||||
use gpui::{Task, WeakView};
|
use gpui::{Task, WeakView};
|
||||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||||
use std::{
|
use std::{
|
||||||
@@ -48,7 +50,7 @@ impl SlashCommand for DefaultSlashCommand {
|
|||||||
_workspace: WeakView<Workspace>,
|
_workspace: WeakView<Workspace>,
|
||||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> Task<Result<SlashCommandOutput>> {
|
) -> Task<SlashCommandResult> {
|
||||||
let store = PromptStore::global(cx);
|
let store = PromptStore::global(cx);
|
||||||
cx.background_executor().spawn(async move {
|
cx.background_executor().spawn(async move {
|
||||||
let store = store.await?;
|
let store = store.await?;
|
||||||
@@ -76,7 +78,8 @@ impl SlashCommand for DefaultSlashCommand {
|
|||||||
}],
|
}],
|
||||||
text,
|
text,
|
||||||
run_commands_in_text: true,
|
run_commands_in_text: true,
|
||||||
})
|
}
|
||||||
|
.to_event_stream())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use crate::slash_command::file_command::{FileCommandMetadata, FileSlashCommand};
|
use crate::slash_command::file_command::{FileCommandMetadata, FileSlashCommand};
|
||||||
use anyhow::Result;
|
use anyhow::{anyhow, Result};
|
||||||
use assistant_slash_command::{
|
use assistant_slash_command::{
|
||||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||||
|
SlashCommandResult,
|
||||||
};
|
};
|
||||||
use collections::HashSet;
|
use collections::HashSet;
|
||||||
use futures::future;
|
use futures::future;
|
||||||
@@ -9,6 +10,7 @@ use gpui::{Task, WeakView, WindowContext};
|
|||||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||||
use std::sync::{atomic::AtomicBool, Arc};
|
use std::sync::{atomic::AtomicBool, Arc};
|
||||||
use text::OffsetRangeExt;
|
use text::OffsetRangeExt;
|
||||||
|
use ui::prelude::*;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
pub(crate) struct DeltaSlashCommand;
|
pub(crate) struct DeltaSlashCommand;
|
||||||
@@ -26,6 +28,10 @@ impl SlashCommand for DeltaSlashCommand {
|
|||||||
self.description()
|
self.description()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn icon(&self) -> IconName {
|
||||||
|
IconName::Diff
|
||||||
|
}
|
||||||
|
|
||||||
fn requires_argument(&self) -> bool {
|
fn requires_argument(&self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@@ -37,7 +43,7 @@ impl SlashCommand for DeltaSlashCommand {
|
|||||||
_workspace: Option<WeakView<Workspace>>,
|
_workspace: Option<WeakView<Workspace>>,
|
||||||
_cx: &mut WindowContext,
|
_cx: &mut WindowContext,
|
||||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||||
unimplemented!()
|
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(
|
fn run(
|
||||||
@@ -48,7 +54,7 @@ impl SlashCommand for DeltaSlashCommand {
|
|||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> Task<Result<SlashCommandOutput>> {
|
) -> Task<SlashCommandResult> {
|
||||||
let mut paths = HashSet::default();
|
let mut paths = HashSet::default();
|
||||||
let mut file_command_old_outputs = Vec::new();
|
let mut file_command_old_outputs = Vec::new();
|
||||||
let mut file_command_new_outputs = Vec::new();
|
let mut file_command_new_outputs = Vec::new();
|
||||||
@@ -85,25 +91,28 @@ impl SlashCommand for DeltaSlashCommand {
|
|||||||
.zip(file_command_new_outputs)
|
.zip(file_command_new_outputs)
|
||||||
{
|
{
|
||||||
if let Ok(new_output) = new_output {
|
if let Ok(new_output) = new_output {
|
||||||
if let Some(file_command_range) = new_output.sections.first() {
|
if let Ok(new_output) = SlashCommandOutput::from_event_stream(new_output).await
|
||||||
let new_text = &new_output.text[file_command_range.range.clone()];
|
{
|
||||||
if old_text.chars().ne(new_text.chars()) {
|
if let Some(file_command_range) = new_output.sections.first() {
|
||||||
output.sections.extend(new_output.sections.into_iter().map(
|
let new_text = &new_output.text[file_command_range.range.clone()];
|
||||||
|section| SlashCommandOutputSection {
|
if old_text.chars().ne(new_text.chars()) {
|
||||||
range: output.text.len() + section.range.start
|
output.sections.extend(new_output.sections.into_iter().map(
|
||||||
..output.text.len() + section.range.end,
|
|section| SlashCommandOutputSection {
|
||||||
icon: section.icon,
|
range: output.text.len() + section.range.start
|
||||||
label: section.label,
|
..output.text.len() + section.range.end,
|
||||||
metadata: section.metadata,
|
icon: section.icon,
|
||||||
},
|
label: section.label,
|
||||||
));
|
metadata: section.metadata,
|
||||||
output.text.push_str(&new_output.text);
|
},
|
||||||
|
));
|
||||||
|
output.text.push_str(&new_output.text);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(output)
|
Ok(output.to_event_stream())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use super::{create_label_for_command, SlashCommand, SlashCommandOutput};
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
|
use assistant_slash_command::{
|
||||||
|
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||||
|
SlashCommandResult,
|
||||||
|
};
|
||||||
use fuzzy::{PathMatch, StringMatchCandidate};
|
use fuzzy::{PathMatch, StringMatchCandidate};
|
||||||
use gpui::{AppContext, Model, Task, View, WeakView};
|
use gpui::{AppContext, Model, Task, View, WeakView};
|
||||||
use language::{
|
use language::{
|
||||||
@@ -19,6 +21,8 @@ use util::paths::PathMatcher;
|
|||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
use crate::slash_command::create_label_for_command;
|
||||||
|
|
||||||
pub(crate) struct DiagnosticsSlashCommand;
|
pub(crate) struct DiagnosticsSlashCommand;
|
||||||
|
|
||||||
impl DiagnosticsSlashCommand {
|
impl DiagnosticsSlashCommand {
|
||||||
@@ -94,6 +98,10 @@ impl SlashCommand for DiagnosticsSlashCommand {
|
|||||||
"Insert diagnostics".into()
|
"Insert diagnostics".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn icon(&self) -> IconName {
|
||||||
|
IconName::XCircle
|
||||||
|
}
|
||||||
|
|
||||||
fn menu_text(&self) -> String {
|
fn menu_text(&self) -> String {
|
||||||
self.description()
|
self.description()
|
||||||
}
|
}
|
||||||
@@ -167,7 +175,7 @@ impl SlashCommand for DiagnosticsSlashCommand {
|
|||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> Task<Result<SlashCommandOutput>> {
|
) -> Task<SlashCommandResult> {
|
||||||
let Some(workspace) = workspace.upgrade() else {
|
let Some(workspace) = workspace.upgrade() else {
|
||||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||||
};
|
};
|
||||||
@@ -176,7 +184,11 @@ impl SlashCommand for DiagnosticsSlashCommand {
|
|||||||
|
|
||||||
let task = collect_diagnostics(workspace.read(cx).project().clone(), options, cx);
|
let task = collect_diagnostics(workspace.read(cx).project().clone(), options, cx);
|
||||||
|
|
||||||
cx.spawn(move |_| async move { task.await?.ok_or_else(|| anyhow!("No diagnostics found")) })
|
cx.spawn(move |_| async move {
|
||||||
|
task.await?
|
||||||
|
.map(|output| output.to_event_stream())
|
||||||
|
.ok_or_else(|| anyhow!("No diagnostics found"))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use std::time::Duration;
|
|||||||
use anyhow::{anyhow, bail, Result};
|
use anyhow::{anyhow, bail, Result};
|
||||||
use assistant_slash_command::{
|
use assistant_slash_command::{
|
||||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||||
|
SlashCommandResult,
|
||||||
};
|
};
|
||||||
use gpui::{AppContext, BackgroundExecutor, Model, Task, WeakView};
|
use gpui::{AppContext, BackgroundExecutor, Model, Task, WeakView};
|
||||||
use indexed_docs::{
|
use indexed_docs::{
|
||||||
@@ -274,7 +275,7 @@ impl SlashCommand for DocsSlashCommand {
|
|||||||
_workspace: WeakView<Workspace>,
|
_workspace: WeakView<Workspace>,
|
||||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> Task<Result<SlashCommandOutput>> {
|
) -> Task<SlashCommandResult> {
|
||||||
if arguments.is_empty() {
|
if arguments.is_empty() {
|
||||||
return Task::ready(Err(anyhow!("missing an argument")));
|
return Task::ready(Err(anyhow!("missing an argument")));
|
||||||
};
|
};
|
||||||
@@ -355,7 +356,8 @@ impl SlashCommand for DocsSlashCommand {
|
|||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
run_commands_in_text: false,
|
run_commands_in_text: false,
|
||||||
})
|
}
|
||||||
|
.to_event_stream())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use std::sync::Arc;
|
|||||||
use anyhow::{anyhow, bail, Context, Result};
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
use assistant_slash_command::{
|
use assistant_slash_command::{
|
||||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||||
|
SlashCommandResult,
|
||||||
};
|
};
|
||||||
use futures::AsyncReadExt;
|
use futures::AsyncReadExt;
|
||||||
use gpui::{Task, WeakView};
|
use gpui::{Task, WeakView};
|
||||||
@@ -133,7 +134,7 @@ impl SlashCommand for FetchSlashCommand {
|
|||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> Task<Result<SlashCommandOutput>> {
|
) -> Task<SlashCommandResult> {
|
||||||
let Some(argument) = arguments.first() else {
|
let Some(argument) = arguments.first() else {
|
||||||
return Task::ready(Err(anyhow!("missing URL")));
|
return Task::ready(Err(anyhow!("missing URL")));
|
||||||
};
|
};
|
||||||
@@ -166,7 +167,8 @@ impl SlashCommand for FetchSlashCommand {
|
|||||||
metadata: None,
|
metadata: None,
|
||||||
}],
|
}],
|
||||||
run_commands_in_text: false,
|
run_commands_in_text: false,
|
||||||
})
|
}
|
||||||
|
.to_event_stream())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
use super::{diagnostics_command::collect_buffer_diagnostics, SlashCommand, SlashCommandOutput};
|
|
||||||
use anyhow::{anyhow, Context as _, Result};
|
use anyhow::{anyhow, Context as _, Result};
|
||||||
use assistant_slash_command::{AfterCompletion, ArgumentCompletion, SlashCommandOutputSection};
|
use assistant_slash_command::{
|
||||||
|
AfterCompletion, ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent,
|
||||||
|
SlashCommandOutput, SlashCommandOutputSection, SlashCommandResult,
|
||||||
|
};
|
||||||
|
use futures::channel::mpsc;
|
||||||
|
use futures::Stream;
|
||||||
use fuzzy::PathMatch;
|
use fuzzy::PathMatch;
|
||||||
use gpui::{AppContext, Model, Task, View, WeakView};
|
use gpui::{AppContext, Model, Task, View, WeakView};
|
||||||
use language::{BufferSnapshot, CodeLabel, HighlightId, LineEnding, LspAdapterDelegate};
|
use language::{BufferSnapshot, CodeLabel, HighlightId, LineEnding, LspAdapterDelegate};
|
||||||
use project::{PathMatchCandidateSet, Project};
|
use project::{PathMatchCandidateSet, Project};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use smol::stream::StreamExt;
|
||||||
use std::{
|
use std::{
|
||||||
fmt::Write,
|
fmt::Write,
|
||||||
ops::{Range, RangeInclusive},
|
ops::{Range, RangeInclusive},
|
||||||
@@ -16,6 +21,8 @@ use ui::prelude::*;
|
|||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
use crate::slash_command::diagnostics_command::collect_buffer_diagnostics;
|
||||||
|
|
||||||
pub(crate) struct FileSlashCommand;
|
pub(crate) struct FileSlashCommand;
|
||||||
|
|
||||||
impl FileSlashCommand {
|
impl FileSlashCommand {
|
||||||
@@ -110,7 +117,7 @@ impl SlashCommand for FileSlashCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn description(&self) -> String {
|
fn description(&self) -> String {
|
||||||
"Insert file".into()
|
"Insert file and/or directory".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn menu_text(&self) -> String {
|
fn menu_text(&self) -> String {
|
||||||
@@ -121,6 +128,10 @@ impl SlashCommand for FileSlashCommand {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn icon(&self) -> IconName {
|
||||||
|
IconName::File
|
||||||
|
}
|
||||||
|
|
||||||
fn complete_argument(
|
fn complete_argument(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
arguments: &[String],
|
arguments: &[String],
|
||||||
@@ -181,7 +192,7 @@ impl SlashCommand for FileSlashCommand {
|
|||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> Task<Result<SlashCommandOutput>> {
|
) -> Task<SlashCommandResult> {
|
||||||
let Some(workspace) = workspace.upgrade() else {
|
let Some(workspace) = workspace.upgrade() else {
|
||||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||||
};
|
};
|
||||||
@@ -190,7 +201,12 @@ impl SlashCommand for FileSlashCommand {
|
|||||||
return Task::ready(Err(anyhow!("missing path")));
|
return Task::ready(Err(anyhow!("missing path")));
|
||||||
};
|
};
|
||||||
|
|
||||||
collect_files(workspace.read(cx).project().clone(), arguments, cx)
|
Task::ready(Ok(collect_files(
|
||||||
|
workspace.read(cx).project().clone(),
|
||||||
|
arguments,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.boxed()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,7 +214,7 @@ fn collect_files(
|
|||||||
project: Model<Project>,
|
project: Model<Project>,
|
||||||
glob_inputs: &[String],
|
glob_inputs: &[String],
|
||||||
cx: &mut AppContext,
|
cx: &mut AppContext,
|
||||||
) -> Task<Result<SlashCommandOutput>> {
|
) -> impl Stream<Item = Result<SlashCommandEvent>> {
|
||||||
let Ok(matchers) = glob_inputs
|
let Ok(matchers) = glob_inputs
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|glob_input| {
|
.map(|glob_input| {
|
||||||
@@ -207,7 +223,7 @@ fn collect_files(
|
|||||||
})
|
})
|
||||||
.collect::<anyhow::Result<Vec<custom_path_matcher::PathMatcher>>>()
|
.collect::<anyhow::Result<Vec<custom_path_matcher::PathMatcher>>>()
|
||||||
else {
|
else {
|
||||||
return Task::ready(Err(anyhow!("invalid path")));
|
return futures::stream::once(async { Err(anyhow!("invalid path")) }).boxed();
|
||||||
};
|
};
|
||||||
|
|
||||||
let project_handle = project.downgrade();
|
let project_handle = project.downgrade();
|
||||||
@@ -217,11 +233,11 @@ fn collect_files(
|
|||||||
.map(|worktree| worktree.read(cx).snapshot())
|
.map(|worktree| worktree.read(cx).snapshot())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let (events_tx, events_rx) = mpsc::unbounded();
|
||||||
cx.spawn(|mut cx| async move {
|
cx.spawn(|mut cx| async move {
|
||||||
let mut output = SlashCommandOutput::default();
|
|
||||||
for snapshot in snapshots {
|
for snapshot in snapshots {
|
||||||
let worktree_id = snapshot.id();
|
let worktree_id = snapshot.id();
|
||||||
let mut directory_stack: Vec<(Arc<Path>, String, usize)> = Vec::new();
|
let mut directory_stack: Vec<Arc<Path>> = Vec::new();
|
||||||
let mut folded_directory_names_stack = Vec::new();
|
let mut folded_directory_names_stack = Vec::new();
|
||||||
let mut is_top_level_directory = true;
|
let mut is_top_level_directory = true;
|
||||||
|
|
||||||
@@ -237,17 +253,19 @@ fn collect_files(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
while let Some((dir, _, _)) = directory_stack.last() {
|
while let Some(dir) = directory_stack.last() {
|
||||||
if entry.path.starts_with(dir) {
|
if entry.path.starts_with(dir) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let (_, entry_name, start) = directory_stack.pop().unwrap();
|
directory_stack.pop().unwrap();
|
||||||
output.sections.push(build_entry_output_section(
|
events_tx
|
||||||
start..output.text.len().saturating_sub(1),
|
.unbounded_send(Ok(SlashCommandEvent::EndSection { metadata: None }))?;
|
||||||
Some(&PathBuf::from(entry_name)),
|
events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
|
||||||
true,
|
SlashCommandContent::Text {
|
||||||
None,
|
text: "\n".into(),
|
||||||
));
|
run_commands_in_text: false,
|
||||||
|
},
|
||||||
|
)))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let filename = entry
|
let filename = entry
|
||||||
@@ -279,23 +297,46 @@ fn collect_files(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let prefix_paths = folded_directory_names_stack.drain(..).as_slice().join("/");
|
let prefix_paths = folded_directory_names_stack.drain(..).as_slice().join("/");
|
||||||
let entry_start = output.text.len();
|
|
||||||
if prefix_paths.is_empty() {
|
if prefix_paths.is_empty() {
|
||||||
if is_top_level_directory {
|
let label = if is_top_level_directory {
|
||||||
output
|
|
||||||
.text
|
|
||||||
.push_str(&path_including_worktree_name.to_string_lossy());
|
|
||||||
is_top_level_directory = false;
|
is_top_level_directory = false;
|
||||||
|
path_including_worktree_name.to_string_lossy().to_string()
|
||||||
} else {
|
} else {
|
||||||
output.text.push_str(&filename);
|
filename
|
||||||
}
|
};
|
||||||
directory_stack.push((entry.path.clone(), filename, entry_start));
|
events_tx.unbounded_send(Ok(SlashCommandEvent::StartSection {
|
||||||
|
icon: IconName::Folder,
|
||||||
|
label: label.clone().into(),
|
||||||
|
metadata: None,
|
||||||
|
}))?;
|
||||||
|
events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
|
||||||
|
SlashCommandContent::Text {
|
||||||
|
text: label,
|
||||||
|
run_commands_in_text: false,
|
||||||
|
},
|
||||||
|
)))?;
|
||||||
|
directory_stack.push(entry.path.clone());
|
||||||
} else {
|
} else {
|
||||||
let entry_name = format!("{}/{}", prefix_paths, &filename);
|
let entry_name = format!("{}/{}", prefix_paths, &filename);
|
||||||
output.text.push_str(&entry_name);
|
events_tx.unbounded_send(Ok(SlashCommandEvent::StartSection {
|
||||||
directory_stack.push((entry.path.clone(), entry_name, entry_start));
|
icon: IconName::Folder,
|
||||||
|
label: entry_name.clone().into(),
|
||||||
|
metadata: None,
|
||||||
|
}))?;
|
||||||
|
events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
|
||||||
|
SlashCommandContent::Text {
|
||||||
|
text: entry_name,
|
||||||
|
run_commands_in_text: false,
|
||||||
|
},
|
||||||
|
)))?;
|
||||||
|
directory_stack.push(entry.path.clone());
|
||||||
}
|
}
|
||||||
output.text.push('\n');
|
events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
|
||||||
|
SlashCommandContent::Text {
|
||||||
|
text: "\n".into(),
|
||||||
|
run_commands_in_text: false,
|
||||||
|
},
|
||||||
|
)))?;
|
||||||
} else if entry.is_file() {
|
} else if entry.is_file() {
|
||||||
let Some(open_buffer_task) = project_handle
|
let Some(open_buffer_task) = project_handle
|
||||||
.update(&mut cx, |project, cx| {
|
.update(&mut cx, |project, cx| {
|
||||||
@@ -306,6 +347,7 @@ fn collect_files(
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if let Some(buffer) = open_buffer_task.await.log_err() {
|
if let Some(buffer) = open_buffer_task.await.log_err() {
|
||||||
|
let mut output = SlashCommandOutput::default();
|
||||||
let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot())?;
|
let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot())?;
|
||||||
append_buffer_to_output(
|
append_buffer_to_output(
|
||||||
&snapshot,
|
&snapshot,
|
||||||
@@ -313,33 +355,24 @@ fn collect_files(
|
|||||||
&mut output,
|
&mut output,
|
||||||
)
|
)
|
||||||
.log_err();
|
.log_err();
|
||||||
|
let mut buffer_events = output.to_event_stream();
|
||||||
|
while let Some(event) = buffer_events.next().await {
|
||||||
|
events_tx.unbounded_send(event)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
while let Some((dir, entry, start)) = directory_stack.pop() {
|
while let Some(_) = directory_stack.pop() {
|
||||||
if directory_stack.is_empty() {
|
events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection { metadata: None }))?;
|
||||||
let mut root_path = PathBuf::new();
|
|
||||||
root_path.push(snapshot.root_name());
|
|
||||||
root_path.push(&dir);
|
|
||||||
output.sections.push(build_entry_output_section(
|
|
||||||
start..output.text.len(),
|
|
||||||
Some(&root_path),
|
|
||||||
true,
|
|
||||||
None,
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
output.sections.push(build_entry_output_section(
|
|
||||||
start..output.text.len(),
|
|
||||||
Some(&PathBuf::from(entry.as_str())),
|
|
||||||
true,
|
|
||||||
None,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(output)
|
|
||||||
|
anyhow::Ok(())
|
||||||
})
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
|
||||||
|
events_rx.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn codeblock_fence_for_path(
|
pub fn codeblock_fence_for_path(
|
||||||
@@ -524,11 +557,14 @@ pub fn append_buffer_to_output(
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
use assistant_slash_command::SlashCommandOutput;
|
||||||
use fs::FakeFs;
|
use fs::FakeFs;
|
||||||
use gpui::TestAppContext;
|
use gpui::TestAppContext;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
|
use smol::stream::StreamExt;
|
||||||
|
|
||||||
use crate::slash_command::file_command::collect_files;
|
use crate::slash_command::file_command::collect_files;
|
||||||
|
|
||||||
@@ -569,8 +605,9 @@ mod test {
|
|||||||
|
|
||||||
let project = Project::test(fs, ["/root".as_ref()], cx).await;
|
let project = Project::test(fs, ["/root".as_ref()], cx).await;
|
||||||
|
|
||||||
let result_1 = cx
|
let result_1 =
|
||||||
.update(|cx| collect_files(project.clone(), &["root/dir".to_string()], cx))
|
cx.update(|cx| collect_files(project.clone(), &["root/dir".to_string()], cx));
|
||||||
|
let result_1 = SlashCommandOutput::from_event_stream(result_1.boxed())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -578,17 +615,17 @@ mod test {
|
|||||||
// 4 files + 2 directories
|
// 4 files + 2 directories
|
||||||
assert_eq!(result_1.sections.len(), 6);
|
assert_eq!(result_1.sections.len(), 6);
|
||||||
|
|
||||||
let result_2 = cx
|
let result_2 =
|
||||||
.update(|cx| collect_files(project.clone(), &["root/dir/".to_string()], cx))
|
cx.update(|cx| collect_files(project.clone(), &["root/dir/".to_string()], cx));
|
||||||
|
let result_2 = SlashCommandOutput::from_event_stream(result_2.boxed())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(result_1, result_2);
|
assert_eq!(result_1, result_2);
|
||||||
|
|
||||||
let result = cx
|
let result =
|
||||||
.update(|cx| collect_files(project.clone(), &["root/dir*".to_string()], cx))
|
cx.update(|cx| collect_files(project.clone(), &["root/dir*".to_string()], cx).boxed());
|
||||||
.await
|
let result = SlashCommandOutput::from_event_stream(result).await.unwrap();
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(result.text.starts_with("root/dir"));
|
assert!(result.text.starts_with("root/dir"));
|
||||||
// 5 files + 2 directories
|
// 5 files + 2 directories
|
||||||
@@ -631,8 +668,9 @@ mod test {
|
|||||||
|
|
||||||
let project = Project::test(fs, ["/zed".as_ref()], cx).await;
|
let project = Project::test(fs, ["/zed".as_ref()], cx).await;
|
||||||
|
|
||||||
let result = cx
|
let result =
|
||||||
.update(|cx| collect_files(project.clone(), &["zed/assets/themes".to_string()], cx))
|
cx.update(|cx| collect_files(project.clone(), &["zed/assets/themes".to_string()], cx));
|
||||||
|
let result = SlashCommandOutput::from_event_stream(result.boxed())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -692,8 +730,9 @@ mod test {
|
|||||||
|
|
||||||
let project = Project::test(fs, ["/zed".as_ref()], cx).await;
|
let project = Project::test(fs, ["/zed".as_ref()], cx).await;
|
||||||
|
|
||||||
let result = cx
|
let result =
|
||||||
.update(|cx| collect_files(project.clone(), &["zed/assets/themes".to_string()], cx))
|
cx.update(|cx| collect_files(project.clone(), &["zed/assets/themes".to_string()], cx));
|
||||||
|
let result = SlashCommandOutput::from_event_stream(result.boxed())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -716,6 +755,8 @@ mod test {
|
|||||||
assert_eq!(result.sections[6].label, "summercamp");
|
assert_eq!(result.sections[6].label, "summercamp");
|
||||||
assert_eq!(result.sections[7].label, "zed/assets/themes");
|
assert_eq!(result.sections[7].label, "zed/assets/themes");
|
||||||
|
|
||||||
|
assert_eq!(result.text, "zed/assets/themes\n```zed/assets/themes/LICENSE\n1\n```\n\nsummercamp\n```zed/assets/themes/summercamp/LICENSE\n1\n```\n\nsubdir\n```zed/assets/themes/summercamp/subdir/LICENSE\n1\n```\n\nsubsubdir\n```zed/assets/themes/summercamp/subdir/subsubdir/LICENSE\n3\n```\n\n");
|
||||||
|
|
||||||
// Ensure that the project lasts until after the last await
|
// Ensure that the project lasts until after the last await
|
||||||
drop(project);
|
drop(project);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use std::sync::Arc;
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use assistant_slash_command::{
|
use assistant_slash_command::{
|
||||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||||
|
SlashCommandResult,
|
||||||
};
|
};
|
||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
use gpui::{Task, WeakView};
|
use gpui::{Task, WeakView};
|
||||||
@@ -48,7 +49,7 @@ impl SlashCommand for NowSlashCommand {
|
|||||||
_workspace: WeakView<Workspace>,
|
_workspace: WeakView<Workspace>,
|
||||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||||
_cx: &mut WindowContext,
|
_cx: &mut WindowContext,
|
||||||
) -> Task<Result<SlashCommandOutput>> {
|
) -> Task<SlashCommandResult> {
|
||||||
let now = Local::now();
|
let now = Local::now();
|
||||||
let text = format!("Today is {now}.", now = now.to_rfc2822());
|
let text = format!("Today is {now}.", now = now.to_rfc2822());
|
||||||
let range = 0..text.len();
|
let range = 0..text.len();
|
||||||
@@ -62,6 +63,7 @@ impl SlashCommand for NowSlashCommand {
|
|||||||
metadata: None,
|
metadata: None,
|
||||||
}],
|
}],
|
||||||
run_commands_in_text: false,
|
run_commands_in_text: false,
|
||||||
}))
|
}
|
||||||
|
.to_event_stream()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use super::{
|
|||||||
};
|
};
|
||||||
use crate::PromptBuilder;
|
use crate::PromptBuilder;
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
|
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection, SlashCommandResult};
|
||||||
use feature_flags::FeatureFlag;
|
use feature_flags::FeatureFlag;
|
||||||
use gpui::{AppContext, Task, WeakView, WindowContext};
|
use gpui::{AppContext, Task, WeakView, WindowContext};
|
||||||
use language::{Anchor, CodeLabel, LspAdapterDelegate};
|
use language::{Anchor, CodeLabel, LspAdapterDelegate};
|
||||||
@@ -24,7 +24,8 @@ use std::{
|
|||||||
ops::DerefMut,
|
ops::DerefMut,
|
||||||
sync::{atomic::AtomicBool, Arc},
|
sync::{atomic::AtomicBool, Arc},
|
||||||
};
|
};
|
||||||
use ui::{BorrowAppContext as _, IconName};
|
|
||||||
|
use ui::prelude::*;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
pub struct ProjectSlashCommand {
|
pub struct ProjectSlashCommand {
|
||||||
@@ -50,6 +51,10 @@ impl SlashCommand for ProjectSlashCommand {
|
|||||||
"Generate a semantic search based on context".into()
|
"Generate a semantic search based on context".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn icon(&self) -> IconName {
|
||||||
|
IconName::Folder
|
||||||
|
}
|
||||||
|
|
||||||
fn menu_text(&self) -> String {
|
fn menu_text(&self) -> String {
|
||||||
self.description()
|
self.description()
|
||||||
}
|
}
|
||||||
@@ -76,7 +81,7 @@ impl SlashCommand for ProjectSlashCommand {
|
|||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> Task<Result<SlashCommandOutput>> {
|
) -> Task<SlashCommandResult> {
|
||||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||||
let current_model = model_registry.active_model();
|
let current_model = model_registry.active_model();
|
||||||
let prompt_builder = self.prompt_builder.clone();
|
let prompt_builder = self.prompt_builder.clone();
|
||||||
@@ -162,7 +167,8 @@ impl SlashCommand for ProjectSlashCommand {
|
|||||||
text: output,
|
text: output,
|
||||||
sections,
|
sections,
|
||||||
run_commands_in_text: true,
|
run_commands_in_text: true,
|
||||||
})
|
}
|
||||||
|
.to_event_stream())
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
use super::{SlashCommand, SlashCommandOutput};
|
|
||||||
use crate::prompt_library::PromptStore;
|
use crate::prompt_library::PromptStore;
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
|
use assistant_slash_command::{
|
||||||
|
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||||
|
SlashCommandResult,
|
||||||
|
};
|
||||||
use gpui::{Task, WeakView};
|
use gpui::{Task, WeakView};
|
||||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||||
use std::sync::{atomic::AtomicBool, Arc};
|
use std::sync::{atomic::AtomicBool, Arc};
|
||||||
@@ -19,6 +21,10 @@ impl SlashCommand for PromptSlashCommand {
|
|||||||
"Insert prompt from library".into()
|
"Insert prompt from library".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn icon(&self) -> IconName {
|
||||||
|
IconName::Library
|
||||||
|
}
|
||||||
|
|
||||||
fn menu_text(&self) -> String {
|
fn menu_text(&self) -> String {
|
||||||
self.description()
|
self.description()
|
||||||
}
|
}
|
||||||
@@ -61,7 +67,7 @@ impl SlashCommand for PromptSlashCommand {
|
|||||||
_workspace: WeakView<Workspace>,
|
_workspace: WeakView<Workspace>,
|
||||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> Task<Result<SlashCommandOutput>> {
|
) -> Task<SlashCommandResult> {
|
||||||
let title = arguments.to_owned().join(" ");
|
let title = arguments.to_owned().join(" ");
|
||||||
if title.trim().is_empty() {
|
if title.trim().is_empty() {
|
||||||
return Task::ready(Err(anyhow!("missing prompt name")));
|
return Task::ready(Err(anyhow!("missing prompt name")));
|
||||||
@@ -100,7 +106,8 @@ impl SlashCommand for PromptSlashCommand {
|
|||||||
metadata: None,
|
metadata: None,
|
||||||
}],
|
}],
|
||||||
run_commands_in_text: true,
|
run_commands_in_text: true,
|
||||||
})
|
}
|
||||||
|
.to_event_stream())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
use super::{
|
|
||||||
create_label_for_command,
|
|
||||||
file_command::{build_entry_output_section, codeblock_fence_for_path},
|
|
||||||
SlashCommand, SlashCommandOutput,
|
|
||||||
};
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
|
use assistant_slash_command::{
|
||||||
|
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||||
|
SlashCommandResult,
|
||||||
|
};
|
||||||
use feature_flags::FeatureFlag;
|
use feature_flags::FeatureFlag;
|
||||||
use gpui::{AppContext, Task, WeakView};
|
use gpui::{AppContext, Task, WeakView};
|
||||||
use language::{CodeLabel, LspAdapterDelegate};
|
use language::{CodeLabel, LspAdapterDelegate};
|
||||||
@@ -16,6 +14,9 @@ use std::{
|
|||||||
use ui::{prelude::*, IconName};
|
use ui::{prelude::*, IconName};
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
use crate::slash_command::create_label_for_command;
|
||||||
|
use crate::slash_command::file_command::{build_entry_output_section, codeblock_fence_for_path};
|
||||||
|
|
||||||
pub(crate) struct SearchSlashCommandFeatureFlag;
|
pub(crate) struct SearchSlashCommandFeatureFlag;
|
||||||
|
|
||||||
impl FeatureFlag for SearchSlashCommandFeatureFlag {
|
impl FeatureFlag for SearchSlashCommandFeatureFlag {
|
||||||
@@ -37,6 +38,10 @@ impl SlashCommand for SearchSlashCommand {
|
|||||||
"Search your project semantically".into()
|
"Search your project semantically".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn icon(&self) -> IconName {
|
||||||
|
IconName::SearchCode
|
||||||
|
}
|
||||||
|
|
||||||
fn menu_text(&self) -> String {
|
fn menu_text(&self) -> String {
|
||||||
self.description()
|
self.description()
|
||||||
}
|
}
|
||||||
@@ -63,7 +68,7 @@ impl SlashCommand for SearchSlashCommand {
|
|||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> Task<Result<SlashCommandOutput>> {
|
) -> Task<SlashCommandResult> {
|
||||||
let Some(workspace) = workspace.upgrade() else {
|
let Some(workspace) = workspace.upgrade() else {
|
||||||
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
|
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
|
||||||
};
|
};
|
||||||
@@ -129,6 +134,7 @@ impl SlashCommand for SearchSlashCommand {
|
|||||||
sections,
|
sections,
|
||||||
run_commands_in_text: false,
|
run_commands_in_text: false,
|
||||||
}
|
}
|
||||||
|
.to_event_stream()
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
|||||||
136
crates/assistant/src/slash_command/streaming_example_command.rs
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
use std::sync::atomic::AtomicBool;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use assistant_slash_command::{
|
||||||
|
ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent,
|
||||||
|
SlashCommandOutputSection, SlashCommandResult,
|
||||||
|
};
|
||||||
|
use feature_flags::FeatureFlag;
|
||||||
|
use futures::channel::mpsc;
|
||||||
|
use gpui::{Task, WeakView};
|
||||||
|
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||||
|
use smol::stream::StreamExt;
|
||||||
|
use smol::Timer;
|
||||||
|
use ui::prelude::*;
|
||||||
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
pub struct StreamingExampleSlashCommandFeatureFlag;
|
||||||
|
|
||||||
|
impl FeatureFlag for StreamingExampleSlashCommandFeatureFlag {
|
||||||
|
const NAME: &'static str = "streaming-example-slash-command";
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct StreamingExampleSlashCommand;
|
||||||
|
|
||||||
|
impl SlashCommand for StreamingExampleSlashCommand {
|
||||||
|
fn name(&self) -> String {
|
||||||
|
"streaming-example".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> String {
|
||||||
|
"An example slash command that showcases streaming.".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn menu_text(&self) -> String {
|
||||||
|
self.description()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn requires_argument(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn complete_argument(
|
||||||
|
self: Arc<Self>,
|
||||||
|
_arguments: &[String],
|
||||||
|
_cancel: Arc<AtomicBool>,
|
||||||
|
_workspace: Option<WeakView<Workspace>>,
|
||||||
|
_cx: &mut WindowContext,
|
||||||
|
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||||
|
Task::ready(Ok(Vec::new()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
self: Arc<Self>,
|
||||||
|
_arguments: &[String],
|
||||||
|
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||||
|
_context_buffer: BufferSnapshot,
|
||||||
|
_workspace: WeakView<Workspace>,
|
||||||
|
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Task<SlashCommandResult> {
|
||||||
|
let (events_tx, events_rx) = mpsc::unbounded();
|
||||||
|
cx.background_executor()
|
||||||
|
.spawn(async move {
|
||||||
|
events_tx.unbounded_send(Ok(SlashCommandEvent::StartSection {
|
||||||
|
icon: IconName::FileRust,
|
||||||
|
label: "Section 1".into(),
|
||||||
|
metadata: None,
|
||||||
|
}))?;
|
||||||
|
events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
|
||||||
|
SlashCommandContent::Text {
|
||||||
|
text: "Hello".into(),
|
||||||
|
run_commands_in_text: false,
|
||||||
|
},
|
||||||
|
)))?;
|
||||||
|
events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection { metadata: None }))?;
|
||||||
|
events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
|
||||||
|
SlashCommandContent::Text {
|
||||||
|
text: "\n".into(),
|
||||||
|
run_commands_in_text: false,
|
||||||
|
},
|
||||||
|
)))?;
|
||||||
|
|
||||||
|
Timer::after(Duration::from_secs(1)).await;
|
||||||
|
|
||||||
|
events_tx.unbounded_send(Ok(SlashCommandEvent::StartSection {
|
||||||
|
icon: IconName::FileRust,
|
||||||
|
label: "Section 2".into(),
|
||||||
|
metadata: None,
|
||||||
|
}))?;
|
||||||
|
events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
|
||||||
|
SlashCommandContent::Text {
|
||||||
|
text: "World".into(),
|
||||||
|
run_commands_in_text: false,
|
||||||
|
},
|
||||||
|
)))?;
|
||||||
|
events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection { metadata: None }))?;
|
||||||
|
events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
|
||||||
|
SlashCommandContent::Text {
|
||||||
|
text: "\n".into(),
|
||||||
|
run_commands_in_text: false,
|
||||||
|
},
|
||||||
|
)))?;
|
||||||
|
|
||||||
|
for n in 1..=10 {
|
||||||
|
Timer::after(Duration::from_secs(1)).await;
|
||||||
|
|
||||||
|
events_tx.unbounded_send(Ok(SlashCommandEvent::StartSection {
|
||||||
|
icon: IconName::StarFilled,
|
||||||
|
label: format!("Section {n}").into(),
|
||||||
|
metadata: None,
|
||||||
|
}))?;
|
||||||
|
events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
|
||||||
|
SlashCommandContent::Text {
|
||||||
|
text: "lorem ipsum ".repeat(n).trim().into(),
|
||||||
|
run_commands_in_text: false,
|
||||||
|
},
|
||||||
|
)))?;
|
||||||
|
events_tx
|
||||||
|
.unbounded_send(Ok(SlashCommandEvent::EndSection { metadata: None }))?;
|
||||||
|
events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
|
||||||
|
SlashCommandContent::Text {
|
||||||
|
text: "\n".into(),
|
||||||
|
run_commands_in_text: false,
|
||||||
|
},
|
||||||
|
)))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
anyhow::Ok(())
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
|
||||||
|
Task::ready(Ok(events_rx.boxed()))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
use super::{SlashCommand, SlashCommandOutput};
|
|
||||||
use anyhow::{anyhow, Context as _, Result};
|
use anyhow::{anyhow, Context as _, Result};
|
||||||
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
|
use assistant_slash_command::{
|
||||||
|
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||||
|
SlashCommandResult,
|
||||||
|
};
|
||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
use gpui::{Task, WeakView};
|
use gpui::{Task, WeakView};
|
||||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||||
@@ -20,6 +22,10 @@ impl SlashCommand for OutlineSlashCommand {
|
|||||||
"Insert symbols for active tab".into()
|
"Insert symbols for active tab".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn icon(&self) -> IconName {
|
||||||
|
IconName::ListTree
|
||||||
|
}
|
||||||
|
|
||||||
fn menu_text(&self) -> String {
|
fn menu_text(&self) -> String {
|
||||||
self.description()
|
self.description()
|
||||||
}
|
}
|
||||||
@@ -46,7 +52,7 @@ impl SlashCommand for OutlineSlashCommand {
|
|||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> Task<Result<SlashCommandOutput>> {
|
) -> Task<SlashCommandResult> {
|
||||||
let output = workspace.update(cx, |workspace, cx| {
|
let output = workspace.update(cx, |workspace, cx| {
|
||||||
let Some(active_item) = workspace.active_item(cx) else {
|
let Some(active_item) = workspace.active_item(cx) else {
|
||||||
return Task::ready(Err(anyhow!("no active tab")));
|
return Task::ready(Err(anyhow!("no active tab")));
|
||||||
@@ -83,7 +89,8 @@ impl SlashCommand for OutlineSlashCommand {
|
|||||||
}],
|
}],
|
||||||
text: outline_text,
|
text: outline_text,
|
||||||
run_commands_in_text: false,
|
run_commands_in_text: false,
|
||||||
})
|
}
|
||||||
|
.to_event_stream())
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use super::{file_command::append_buffer_to_output, SlashCommand, SlashCommandOutput};
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
|
use assistant_slash_command::{
|
||||||
|
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||||
|
SlashCommandResult,
|
||||||
|
};
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
use futures::future::join_all;
|
use futures::future::join_all;
|
||||||
@@ -10,10 +12,12 @@ use std::{
|
|||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::{atomic::AtomicBool, Arc},
|
sync::{atomic::AtomicBool, Arc},
|
||||||
};
|
};
|
||||||
use ui::{ActiveTheme, WindowContext};
|
use ui::{prelude::*, ActiveTheme, WindowContext};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
use crate::slash_command::file_command::append_buffer_to_output;
|
||||||
|
|
||||||
pub(crate) struct TabSlashCommand;
|
pub(crate) struct TabSlashCommand;
|
||||||
|
|
||||||
const ALL_TABS_COMPLETION_ITEM: &str = "all";
|
const ALL_TABS_COMPLETION_ITEM: &str = "all";
|
||||||
@@ -27,6 +31,10 @@ impl SlashCommand for TabSlashCommand {
|
|||||||
"Insert open tabs (active tab by default)".to_owned()
|
"Insert open tabs (active tab by default)".to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn icon(&self) -> IconName {
|
||||||
|
IconName::FileTree
|
||||||
|
}
|
||||||
|
|
||||||
fn menu_text(&self) -> String {
|
fn menu_text(&self) -> String {
|
||||||
self.description()
|
self.description()
|
||||||
}
|
}
|
||||||
@@ -132,7 +140,7 @@ impl SlashCommand for TabSlashCommand {
|
|||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> Task<Result<SlashCommandOutput>> {
|
) -> Task<SlashCommandResult> {
|
||||||
let tab_items_search = tab_items_for_queries(
|
let tab_items_search = tab_items_for_queries(
|
||||||
Some(workspace),
|
Some(workspace),
|
||||||
arguments,
|
arguments,
|
||||||
@@ -146,7 +154,7 @@ impl SlashCommand for TabSlashCommand {
|
|||||||
for (full_path, buffer, _) in tab_items_search.await? {
|
for (full_path, buffer, _) in tab_items_search.await? {
|
||||||
append_buffer_to_output(&buffer, full_path.as_deref(), &mut output).log_err();
|
append_buffer_to_output(&buffer, full_path.as_deref(), &mut output).log_err();
|
||||||
}
|
}
|
||||||
Ok(output)
|
Ok(output.to_event_stream())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use std::sync::Arc;
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use assistant_slash_command::{
|
use assistant_slash_command::{
|
||||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||||
|
SlashCommandResult,
|
||||||
};
|
};
|
||||||
use gpui::{AppContext, Task, View, WeakView};
|
use gpui::{AppContext, Task, View, WeakView};
|
||||||
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
|
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
|
||||||
@@ -32,6 +33,10 @@ impl SlashCommand for TerminalSlashCommand {
|
|||||||
"Insert terminal output".into()
|
"Insert terminal output".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn icon(&self) -> IconName {
|
||||||
|
IconName::Terminal
|
||||||
|
}
|
||||||
|
|
||||||
fn menu_text(&self) -> String {
|
fn menu_text(&self) -> String {
|
||||||
self.description()
|
self.description()
|
||||||
}
|
}
|
||||||
@@ -62,7 +67,7 @@ impl SlashCommand for TerminalSlashCommand {
|
|||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> Task<Result<SlashCommandOutput>> {
|
) -> Task<SlashCommandResult> {
|
||||||
let Some(workspace) = workspace.upgrade() else {
|
let Some(workspace) = workspace.upgrade() else {
|
||||||
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
|
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
|
||||||
};
|
};
|
||||||
@@ -96,7 +101,8 @@ impl SlashCommand for TerminalSlashCommand {
|
|||||||
metadata: None,
|
metadata: None,
|
||||||
}],
|
}],
|
||||||
run_commands_in_text: false,
|
run_commands_in_text: false,
|
||||||
}))
|
}
|
||||||
|
.to_event_stream()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
use crate::prompts::PromptBuilder;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use std::sync::atomic::AtomicBool;
|
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
use assistant_slash_command::{
|
|
||||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
|
||||||
};
|
|
||||||
use gpui::{Task, WeakView};
|
|
||||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
|
||||||
use ui::prelude::*;
|
|
||||||
|
|
||||||
use workspace::Workspace;
|
|
||||||
|
|
||||||
pub(crate) struct WorkflowSlashCommand {
|
|
||||||
prompt_builder: Arc<PromptBuilder>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WorkflowSlashCommand {
|
|
||||||
pub const NAME: &'static str = "workflow";
|
|
||||||
|
|
||||||
pub fn new(prompt_builder: Arc<PromptBuilder>) -> Self {
|
|
||||||
Self { prompt_builder }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SlashCommand for WorkflowSlashCommand {
|
|
||||||
fn name(&self) -> String {
|
|
||||||
Self::NAME.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn description(&self) -> String {
|
|
||||||
"Insert prompt to opt into the edit workflow".into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn menu_text(&self) -> String {
|
|
||||||
self.description()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn requires_argument(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn complete_argument(
|
|
||||||
self: Arc<Self>,
|
|
||||||
_arguments: &[String],
|
|
||||||
_cancel: Arc<AtomicBool>,
|
|
||||||
_workspace: Option<WeakView<Workspace>>,
|
|
||||||
_cx: &mut WindowContext,
|
|
||||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
|
||||||
Task::ready(Ok(Vec::new()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run(
|
|
||||||
self: Arc<Self>,
|
|
||||||
_arguments: &[String],
|
|
||||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
|
||||||
_context_buffer: BufferSnapshot,
|
|
||||||
_workspace: WeakView<Workspace>,
|
|
||||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
|
||||||
cx: &mut WindowContext,
|
|
||||||
) -> Task<Result<SlashCommandOutput>> {
|
|
||||||
let prompt_builder = self.prompt_builder.clone();
|
|
||||||
cx.spawn(|_cx| async move {
|
|
||||||
let text = prompt_builder.generate_workflow_prompt()?;
|
|
||||||
let range = 0..text.len();
|
|
||||||
|
|
||||||
Ok(SlashCommandOutput {
|
|
||||||
text,
|
|
||||||
sections: vec![SlashCommandOutputSection {
|
|
||||||
range,
|
|
||||||
icon: IconName::Route,
|
|
||||||
label: "Workflow".into(),
|
|
||||||
metadata: None,
|
|
||||||
}],
|
|
||||||
run_commands_in_text: false,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +1,13 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use assistant_slash_command::SlashCommandRegistry;
|
use assistant_slash_command::SlashCommandRegistry;
|
||||||
use gpui::AnyElement;
|
|
||||||
use gpui::DismissEvent;
|
|
||||||
use gpui::WeakView;
|
|
||||||
use picker::PickerEditorPosition;
|
|
||||||
|
|
||||||
use ui::ListItemSpacing;
|
use gpui::{AnyElement, DismissEvent, SharedString, Task, WeakView};
|
||||||
|
use picker::{Picker, PickerDelegate, PickerEditorPosition};
|
||||||
use gpui::SharedString;
|
use ui::{prelude::*, KeyBinding, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger};
|
||||||
use gpui::Task;
|
|
||||||
use picker::{Picker, PickerDelegate};
|
|
||||||
use ui::{prelude::*, ListItem, PopoverMenu, PopoverTrigger};
|
|
||||||
|
|
||||||
use crate::assistant_panel::ContextEditor;
|
use crate::assistant_panel::ContextEditor;
|
||||||
|
use crate::QuoteSelection;
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub(super) struct SlashCommandSelector<T: PopoverTrigger> {
|
pub(super) struct SlashCommandSelector<T: PopoverTrigger> {
|
||||||
@@ -27,6 +21,7 @@ struct SlashCommandInfo {
|
|||||||
name: SharedString,
|
name: SharedString,
|
||||||
description: SharedString,
|
description: SharedString,
|
||||||
args: Option<SharedString>,
|
args: Option<SharedString>,
|
||||||
|
icon: IconName,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -37,6 +32,7 @@ enum SlashCommandEntry {
|
|||||||
renderer: fn(&mut WindowContext<'_>) -> AnyElement,
|
renderer: fn(&mut WindowContext<'_>) -> AnyElement,
|
||||||
on_confirm: fn(&mut WindowContext<'_>),
|
on_confirm: fn(&mut WindowContext<'_>),
|
||||||
},
|
},
|
||||||
|
QuoteButton,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsRef<str> for SlashCommandEntry {
|
impl AsRef<str> for SlashCommandEntry {
|
||||||
@@ -44,6 +40,7 @@ impl AsRef<str> for SlashCommandEntry {
|
|||||||
match self {
|
match self {
|
||||||
SlashCommandEntry::Info(SlashCommandInfo { name, .. })
|
SlashCommandEntry::Info(SlashCommandInfo { name, .. })
|
||||||
| SlashCommandEntry::Advert { name, .. } => name,
|
| SlashCommandEntry::Advert { name, .. } => name,
|
||||||
|
SlashCommandEntry::QuoteButton => "Quote Selection",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -145,16 +142,23 @@ impl PickerDelegate for SlashCommandDelegate {
|
|||||||
}
|
}
|
||||||
ret
|
ret
|
||||||
}
|
}
|
||||||
|
|
||||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||||
if let Some(command) = self.filtered_commands.get(self.selected_index) {
|
if let Some(command) = self.filtered_commands.get(self.selected_index) {
|
||||||
if let SlashCommandEntry::Info(info) = command {
|
match command {
|
||||||
self.active_context_editor
|
SlashCommandEntry::Info(info) => {
|
||||||
.update(cx, |context_editor, cx| {
|
self.active_context_editor
|
||||||
context_editor.insert_command(&info.name, cx)
|
.update(cx, |context_editor, cx| {
|
||||||
})
|
context_editor.insert_command(&info.name, cx)
|
||||||
.ok();
|
})
|
||||||
} else if let SlashCommandEntry::Advert { on_confirm, .. } = command {
|
.ok();
|
||||||
on_confirm(cx);
|
}
|
||||||
|
SlashCommandEntry::QuoteButton => {
|
||||||
|
cx.dispatch_action(Box::new(QuoteSelection));
|
||||||
|
}
|
||||||
|
SlashCommandEntry::Advert { on_confirm, .. } => {
|
||||||
|
on_confirm(cx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
cx.emit(DismissEvent);
|
cx.emit(DismissEvent);
|
||||||
}
|
}
|
||||||
@@ -178,53 +182,85 @@ impl PickerDelegate for SlashCommandDelegate {
|
|||||||
SlashCommandEntry::Info(info) => Some(
|
SlashCommandEntry::Info(info) => Some(
|
||||||
ListItem::new(ix)
|
ListItem::new(ix)
|
||||||
.inset(true)
|
.inset(true)
|
||||||
.spacing(ListItemSpacing::Sparse)
|
.spacing(ListItemSpacing::Dense)
|
||||||
.selected(selected)
|
.selected(selected)
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
v_flex()
|
||||||
.group(format!("command-entry-label-{ix}"))
|
.group(format!("command-entry-label-{ix}"))
|
||||||
.w_full()
|
.w_full()
|
||||||
.min_w(px(250.))
|
.min_w(px(250.))
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
h_flex()
|
||||||
.child(
|
.gap_1p5()
|
||||||
h_flex()
|
.child(Icon::new(info.icon).size(IconSize::XSmall))
|
||||||
.child(div().font_buffer(cx).child({
|
.child(div().font_buffer(cx).child({
|
||||||
let mut label = format!("/{}", info.name);
|
let mut label = format!("{}", info.name);
|
||||||
if let Some(args) =
|
if let Some(args) = info.args.as_ref().filter(|_| selected)
|
||||||
info.args.as_ref().filter(|_| selected)
|
{
|
||||||
{
|
label.push_str(&args);
|
||||||
label.push_str(&args);
|
}
|
||||||
}
|
Label::new(label).size(LabelSize::Small)
|
||||||
Label::new(label).size(LabelSize::Small)
|
}))
|
||||||
}))
|
.children(info.args.clone().filter(|_| !selected).map(
|
||||||
.children(info.args.clone().filter(|_| !selected).map(
|
|args| {
|
||||||
|args| {
|
div()
|
||||||
div()
|
.font_buffer(cx)
|
||||||
.font_buffer(cx)
|
.child(
|
||||||
.child(
|
Label::new(args)
|
||||||
Label::new(args)
|
.size(LabelSize::Small)
|
||||||
.size(LabelSize::Small)
|
.color(Color::Muted),
|
||||||
.color(Color::Muted),
|
)
|
||||||
)
|
.visible_on_hover(format!(
|
||||||
.visible_on_hover(format!(
|
"command-entry-label-{ix}"
|
||||||
"command-entry-label-{ix}"
|
))
|
||||||
))
|
},
|
||||||
},
|
)),
|
||||||
)),
|
)
|
||||||
)
|
.child(
|
||||||
.child(
|
Label::new(info.description.clone())
|
||||||
Label::new(info.description.clone())
|
.size(LabelSize::Small)
|
||||||
.size(LabelSize::Small)
|
.color(Color::Muted),
|
||||||
.color(Color::Muted),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
SlashCommandEntry::QuoteButton => {
|
||||||
|
let focus = cx.focus_handle();
|
||||||
|
let key_binding = KeyBinding::for_action_in(&QuoteSelection, &focus, cx);
|
||||||
|
|
||||||
|
Some(
|
||||||
|
ListItem::new(ix)
|
||||||
|
.inset(true)
|
||||||
|
.spacing(ListItemSpacing::Dense)
|
||||||
|
.selected(selected)
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1p5()
|
||||||
|
.child(Icon::new(IconName::Quote).size(IconSize::XSmall))
|
||||||
|
.child(
|
||||||
|
div().font_buffer(cx).child(
|
||||||
|
Label::new("selection").size(LabelSize::Small),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1p5()
|
||||||
|
.child(
|
||||||
|
Label::new("Insert editor selection")
|
||||||
|
.color(Color::Muted)
|
||||||
|
.size(LabelSize::Small),
|
||||||
|
)
|
||||||
|
.children(key_binding.map(|kb| kb.render(cx))),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
SlashCommandEntry::Advert { renderer, .. } => Some(
|
SlashCommandEntry::Advert { renderer, .. } => Some(
|
||||||
ListItem::new(ix)
|
ListItem::new(ix)
|
||||||
.inset(true)
|
.inset(true)
|
||||||
.spacing(ListItemSpacing::Sparse)
|
.spacing(ListItemSpacing::Dense)
|
||||||
.selected(selected)
|
.selected(selected)
|
||||||
.child(renderer(cx)),
|
.child(renderer(cx)),
|
||||||
),
|
),
|
||||||
@@ -251,31 +287,50 @@ impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
|
|||||||
name: command_name.into(),
|
name: command_name.into(),
|
||||||
description: menu_text,
|
description: menu_text,
|
||||||
args,
|
args,
|
||||||
|
icon: command.icon(),
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
.chain([SlashCommandEntry::Advert {
|
.chain([
|
||||||
name: "create-your-command".into(),
|
SlashCommandEntry::Advert {
|
||||||
renderer: |cx| {
|
name: "create-your-command".into(),
|
||||||
v_flex()
|
renderer: |cx| {
|
||||||
.child(
|
v_flex()
|
||||||
h_flex()
|
.w_full()
|
||||||
.font_buffer(cx)
|
.child(
|
||||||
.items_center()
|
h_flex()
|
||||||
.gap_1()
|
.w_full()
|
||||||
.child(div().font_buffer(cx).child(
|
.font_buffer(cx)
|
||||||
Label::new("create-your-command").size(LabelSize::Small),
|
.items_center()
|
||||||
))
|
.justify_between()
|
||||||
.child(Icon::new(IconName::ArrowUpRight).size(IconSize::XSmall)),
|
.child(
|
||||||
)
|
h_flex()
|
||||||
.child(
|
.items_center()
|
||||||
Label::new("Learn how to create a custom command")
|
.gap_1p5()
|
||||||
.size(LabelSize::Small)
|
.child(Icon::new(IconName::Plus).size(IconSize::XSmall))
|
||||||
.color(Color::Muted),
|
.child(
|
||||||
)
|
div().font_buffer(cx).child(
|
||||||
.into_any_element()
|
Label::new("create-your-command")
|
||||||
|
.size(LabelSize::Small),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Icon::new(IconName::ArrowUpRight)
|
||||||
|
.size(IconSize::XSmall)
|
||||||
|
.color(Color::Muted),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Label::new("Create your custom command")
|
||||||
|
.size(LabelSize::Small)
|
||||||
|
.color(Color::Muted),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
},
|
||||||
|
on_confirm: |cx| cx.open_url("https://zed.dev/docs/extensions/slash-commands"),
|
||||||
},
|
},
|
||||||
on_confirm: |cx| cx.open_url("https://zed.dev/docs/extensions/slash-commands"),
|
SlashCommandEntry::QuoteButton,
|
||||||
}])
|
])
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let delegate = SlashCommandDelegate {
|
let delegate = SlashCommandDelegate {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
humanize_token_count, prompts::PromptBuilder, AssistantPanel, AssistantPanelEvent,
|
humanize_token_count, prompts::PromptBuilder, AssistantPanel, AssistantPanelEvent,
|
||||||
ModelSelector, DEFAULT_CONTEXT_LINES,
|
ModelSelector, RequestType, DEFAULT_CONTEXT_LINES,
|
||||||
};
|
};
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
use client::telemetry::Telemetry;
|
use client::telemetry::Telemetry;
|
||||||
@@ -17,7 +17,8 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
use language::Buffer;
|
use language::Buffer;
|
||||||
use language_model::{
|
use language_model::{
|
||||||
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
|
logging::report_assistant_event, LanguageModelRegistry, LanguageModelRequest,
|
||||||
|
LanguageModelRequestMessage, Role,
|
||||||
};
|
};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::{
|
use std::{
|
||||||
@@ -251,7 +252,7 @@ impl TerminalInlineAssistant {
|
|||||||
.read(cx)
|
.read(cx)
|
||||||
.active_context(cx)?
|
.active_context(cx)?
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.to_completion_request(cx),
|
.to_completion_request(RequestType::Chat, cx),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@@ -306,6 +307,33 @@ impl TerminalInlineAssistant {
|
|||||||
this.focus_handle(cx).focus(cx);
|
this.focus_handle(cx).focus(cx);
|
||||||
})
|
})
|
||||||
.log_err();
|
.log_err();
|
||||||
|
|
||||||
|
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
|
||||||
|
let codegen = assist.codegen.read(cx);
|
||||||
|
let executor = cx.background_executor().clone();
|
||||||
|
report_assistant_event(
|
||||||
|
AssistantEvent {
|
||||||
|
conversation_id: None,
|
||||||
|
kind: AssistantKind::InlineTerminal,
|
||||||
|
message_id: codegen.message_id.clone(),
|
||||||
|
phase: if undo {
|
||||||
|
AssistantPhase::Rejected
|
||||||
|
} else {
|
||||||
|
AssistantPhase::Accepted
|
||||||
|
},
|
||||||
|
model: model.telemetry_id(),
|
||||||
|
model_provider: model.provider_id().to_string(),
|
||||||
|
response_latency: None,
|
||||||
|
error_message: None,
|
||||||
|
language_name: None,
|
||||||
|
},
|
||||||
|
codegen.telemetry.clone(),
|
||||||
|
cx.http_client(),
|
||||||
|
model.api_key(cx),
|
||||||
|
&executor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
assist.codegen.update(cx, |codegen, cx| {
|
assist.codegen.update(cx, |codegen, cx| {
|
||||||
if undo {
|
if undo {
|
||||||
codegen.undo(cx);
|
codegen.undo(cx);
|
||||||
@@ -1016,6 +1044,7 @@ pub struct Codegen {
|
|||||||
telemetry: Option<Arc<Telemetry>>,
|
telemetry: Option<Arc<Telemetry>>,
|
||||||
terminal: Model<Terminal>,
|
terminal: Model<Terminal>,
|
||||||
generation: Task<()>,
|
generation: Task<()>,
|
||||||
|
message_id: Option<String>,
|
||||||
transaction: Option<TerminalTransaction>,
|
transaction: Option<TerminalTransaction>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1026,6 +1055,7 @@ impl Codegen {
|
|||||||
telemetry,
|
telemetry,
|
||||||
status: CodegenStatus::Idle,
|
status: CodegenStatus::Idle,
|
||||||
generation: Task::ready(()),
|
generation: Task::ready(()),
|
||||||
|
message_id: None,
|
||||||
transaction: None,
|
transaction: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1035,6 +1065,8 @@ impl Codegen {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let model_api_key = model.api_key(cx);
|
||||||
|
let http_client = cx.http_client();
|
||||||
let telemetry = self.telemetry.clone();
|
let telemetry = self.telemetry.clone();
|
||||||
self.status = CodegenStatus::Pending;
|
self.status = CodegenStatus::Pending;
|
||||||
self.transaction = Some(TerminalTransaction::start(self.terminal.clone()));
|
self.transaction = Some(TerminalTransaction::start(self.terminal.clone()));
|
||||||
@@ -1043,44 +1075,62 @@ impl Codegen {
|
|||||||
let model_provider_id = model.provider_id();
|
let model_provider_id = model.provider_id();
|
||||||
let response = model.stream_completion_text(prompt, &cx).await;
|
let response = model.stream_completion_text(prompt, &cx).await;
|
||||||
let generate = async {
|
let generate = async {
|
||||||
|
let message_id = response
|
||||||
|
.as_ref()
|
||||||
|
.ok()
|
||||||
|
.and_then(|response| response.message_id.clone());
|
||||||
|
|
||||||
let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
|
let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
|
||||||
|
|
||||||
let task = cx.background_executor().spawn(async move {
|
let task = cx.background_executor().spawn({
|
||||||
let mut response_latency = None;
|
let message_id = message_id.clone();
|
||||||
let request_start = Instant::now();
|
let executor = cx.background_executor().clone();
|
||||||
let task = async {
|
async move {
|
||||||
let mut chunks = response?;
|
let mut response_latency = None;
|
||||||
while let Some(chunk) = chunks.next().await {
|
let request_start = Instant::now();
|
||||||
if response_latency.is_none() {
|
let task = async {
|
||||||
response_latency = Some(request_start.elapsed());
|
let mut chunks = response?.stream;
|
||||||
|
while let Some(chunk) = chunks.next().await {
|
||||||
|
if response_latency.is_none() {
|
||||||
|
response_latency = Some(request_start.elapsed());
|
||||||
|
}
|
||||||
|
let chunk = chunk?;
|
||||||
|
hunks_tx.send(chunk).await?;
|
||||||
}
|
}
|
||||||
let chunk = chunk?;
|
|
||||||
hunks_tx.send(chunk).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
anyhow::Ok(())
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = task.await;
|
||||||
|
|
||||||
|
let error_message = result.as_ref().err().map(|error| error.to_string());
|
||||||
|
report_assistant_event(
|
||||||
|
AssistantEvent {
|
||||||
|
conversation_id: None,
|
||||||
|
kind: AssistantKind::InlineTerminal,
|
||||||
|
message_id,
|
||||||
|
phase: AssistantPhase::Response,
|
||||||
|
model: model_telemetry_id,
|
||||||
|
model_provider: model_provider_id.to_string(),
|
||||||
|
response_latency,
|
||||||
|
error_message,
|
||||||
|
language_name: None,
|
||||||
|
},
|
||||||
|
telemetry,
|
||||||
|
http_client,
|
||||||
|
model_api_key,
|
||||||
|
&executor,
|
||||||
|
);
|
||||||
|
|
||||||
|
result?;
|
||||||
anyhow::Ok(())
|
anyhow::Ok(())
|
||||||
};
|
|
||||||
|
|
||||||
let result = task.await;
|
|
||||||
|
|
||||||
let error_message = result.as_ref().err().map(|error| error.to_string());
|
|
||||||
if let Some(telemetry) = telemetry {
|
|
||||||
telemetry.report_assistant_event(AssistantEvent {
|
|
||||||
conversation_id: None,
|
|
||||||
kind: AssistantKind::Inline,
|
|
||||||
phase: AssistantPhase::Response,
|
|
||||||
model: model_telemetry_id,
|
|
||||||
model_provider: model_provider_id.to_string(),
|
|
||||||
response_latency,
|
|
||||||
error_message,
|
|
||||||
language_name: None,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result?;
|
|
||||||
anyhow::Ok(())
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.update(&mut cx, |this, _| {
|
||||||
|
this.message_id = message_id;
|
||||||
|
})?;
|
||||||
|
|
||||||
while let Some(hunk) = hunks_rx.next().await {
|
while let Some(hunk) = hunks_rx.next().await {
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
if let Some(transaction) = &mut this.transaction {
|
if let Some(transaction) = &mut this.transaction {
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
|
pub mod context_server_tool;
|
||||||
pub mod now_tool;
|
pub mod now_tool;
|
||||||
|
|||||||
82
crates/assistant/src/tools/context_server_tool.rs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
use anyhow::{anyhow, bail};
|
||||||
|
use assistant_tool::Tool;
|
||||||
|
use context_servers::manager::ContextServerManager;
|
||||||
|
use context_servers::types;
|
||||||
|
use gpui::Task;
|
||||||
|
|
||||||
|
pub struct ContextServerTool {
|
||||||
|
server_id: String,
|
||||||
|
tool: types::Tool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContextServerTool {
|
||||||
|
pub fn new(server_id: impl Into<String>, tool: types::Tool) -> Self {
|
||||||
|
Self {
|
||||||
|
server_id: server_id.into(),
|
||||||
|
tool,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tool for ContextServerTool {
|
||||||
|
fn name(&self) -> String {
|
||||||
|
self.tool.name.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> String {
|
||||||
|
self.tool.description.clone().unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn input_schema(&self) -> serde_json::Value {
|
||||||
|
match &self.tool.input_schema {
|
||||||
|
serde_json::Value::Null => {
|
||||||
|
serde_json::json!({ "type": "object", "properties": [] })
|
||||||
|
}
|
||||||
|
serde_json::Value::Object(map) if map.is_empty() => {
|
||||||
|
serde_json::json!({ "type": "object", "properties": [] })
|
||||||
|
}
|
||||||
|
_ => self.tool.input_schema.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
self: std::sync::Arc<Self>,
|
||||||
|
input: serde_json::Value,
|
||||||
|
_workspace: gpui::WeakView<workspace::Workspace>,
|
||||||
|
cx: &mut ui::WindowContext,
|
||||||
|
) -> gpui::Task<gpui::Result<String>> {
|
||||||
|
let manager = ContextServerManager::global(cx);
|
||||||
|
let manager = manager.read(cx);
|
||||||
|
if let Some(server) = manager.get_server(&self.server_id) {
|
||||||
|
cx.foreground_executor().spawn({
|
||||||
|
let tool_name = self.tool.name.clone();
|
||||||
|
async move {
|
||||||
|
let Some(protocol) = server.client.read().clone() else {
|
||||||
|
bail!("Context server not initialized");
|
||||||
|
};
|
||||||
|
|
||||||
|
let arguments = if let serde_json::Value::Object(map) = input {
|
||||||
|
Some(map.into_iter().collect())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
log::trace!(
|
||||||
|
"Running tool: {} with arguments: {:?}",
|
||||||
|
tool_name,
|
||||||
|
arguments
|
||||||
|
);
|
||||||
|
let response = protocol.run_tool(tool_name, arguments).await?;
|
||||||
|
|
||||||
|
let tool_result = match response.tool_result {
|
||||||
|
serde_json::Value::String(s) => s,
|
||||||
|
_ => serde_json::to_string(&response.tool_result)?,
|
||||||
|
};
|
||||||
|
Ok(tool_result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Task::ready(Err(anyhow!("Context server not found")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,9 +15,15 @@ path = "src/assistant_slash_command.rs"
|
|||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
derive_more.workspace = true
|
derive_more.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
language.workspace = true
|
language.workspace = true
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
workspace.workspace = true
|
workspace.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
gpui = { workspace = true, features = ["test-support"] }
|
||||||
|
pretty_assertions.workspace = true
|
||||||
|
workspace = { workspace = true, features = ["test-support"] }
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
mod slash_command_registry;
|
mod slash_command_registry;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use futures::stream::{self, BoxStream};
|
||||||
|
use futures::StreamExt;
|
||||||
use gpui::{AnyElement, AppContext, ElementId, SharedString, Task, WeakView, WindowContext};
|
use gpui::{AnyElement, AppContext, ElementId, SharedString, Task, WeakView, WindowContext};
|
||||||
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt};
|
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -56,8 +58,13 @@ pub struct ArgumentCompletion {
|
|||||||
pub replace_previous_arguments: bool,
|
pub replace_previous_arguments: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub type SlashCommandResult = Result<BoxStream<'static, Result<SlashCommandEvent>>>;
|
||||||
|
|
||||||
pub trait SlashCommand: 'static + Send + Sync {
|
pub trait SlashCommand: 'static + Send + Sync {
|
||||||
fn name(&self) -> String;
|
fn name(&self) -> String;
|
||||||
|
fn icon(&self) -> IconName {
|
||||||
|
IconName::Slash
|
||||||
|
}
|
||||||
fn label(&self, _cx: &AppContext) -> CodeLabel {
|
fn label(&self, _cx: &AppContext) -> CodeLabel {
|
||||||
CodeLabel::plain(self.name(), None)
|
CodeLabel::plain(self.name(), None)
|
||||||
}
|
}
|
||||||
@@ -87,7 +94,7 @@ pub trait SlashCommand: 'static + Send + Sync {
|
|||||||
// perhaps another kind of delegate is needed here.
|
// perhaps another kind of delegate is needed here.
|
||||||
delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> Task<Result<SlashCommandOutput>>;
|
) -> Task<SlashCommandResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type RenderFoldPlaceholder = Arc<
|
pub type RenderFoldPlaceholder = Arc<
|
||||||
@@ -96,13 +103,146 @@ pub type RenderFoldPlaceholder = Arc<
|
|||||||
+ Fn(ElementId, Arc<dyn Fn(&mut WindowContext)>, &mut WindowContext) -> AnyElement,
|
+ Fn(ElementId, Arc<dyn Fn(&mut WindowContext)>, &mut WindowContext) -> AnyElement,
|
||||||
>;
|
>;
|
||||||
|
|
||||||
#[derive(Debug, Default, PartialEq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub enum SlashCommandContent {
|
||||||
|
Text {
|
||||||
|
text: String,
|
||||||
|
run_commands_in_text: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub enum SlashCommandEvent {
|
||||||
|
StartSection {
|
||||||
|
icon: IconName,
|
||||||
|
label: SharedString,
|
||||||
|
metadata: Option<serde_json::Value>,
|
||||||
|
},
|
||||||
|
Content(SlashCommandContent),
|
||||||
|
EndSection {
|
||||||
|
metadata: Option<serde_json::Value>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, PartialEq, Clone)]
|
||||||
pub struct SlashCommandOutput {
|
pub struct SlashCommandOutput {
|
||||||
pub text: String,
|
pub text: String,
|
||||||
pub sections: Vec<SlashCommandOutputSection<usize>>,
|
pub sections: Vec<SlashCommandOutputSection<usize>>,
|
||||||
pub run_commands_in_text: bool,
|
pub run_commands_in_text: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SlashCommandOutput {
|
||||||
|
pub fn ensure_valid_section_ranges(&mut self) {
|
||||||
|
for section in &mut self.sections {
|
||||||
|
section.range.start = section.range.start.min(self.text.len());
|
||||||
|
section.range.end = section.range.end.min(self.text.len());
|
||||||
|
while !self.text.is_char_boundary(section.range.start) {
|
||||||
|
section.range.start -= 1;
|
||||||
|
}
|
||||||
|
while !self.text.is_char_boundary(section.range.end) {
|
||||||
|
section.range.end += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns this [`SlashCommandOutput`] as a stream of [`SlashCommandEvent`]s.
|
||||||
|
pub fn to_event_stream(mut self) -> BoxStream<'static, Result<SlashCommandEvent>> {
|
||||||
|
self.ensure_valid_section_ranges();
|
||||||
|
|
||||||
|
let mut events = Vec::new();
|
||||||
|
let mut last_section_end = 0;
|
||||||
|
|
||||||
|
for section in self.sections {
|
||||||
|
if last_section_end < section.range.start {
|
||||||
|
events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text {
|
||||||
|
text: self
|
||||||
|
.text
|
||||||
|
.get(last_section_end..section.range.start)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string(),
|
||||||
|
run_commands_in_text: self.run_commands_in_text,
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
events.push(Ok(SlashCommandEvent::StartSection {
|
||||||
|
icon: section.icon,
|
||||||
|
label: section.label,
|
||||||
|
metadata: section.metadata.clone(),
|
||||||
|
}));
|
||||||
|
events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text {
|
||||||
|
text: self
|
||||||
|
.text
|
||||||
|
.get(section.range.start..section.range.end)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string(),
|
||||||
|
run_commands_in_text: self.run_commands_in_text,
|
||||||
|
})));
|
||||||
|
events.push(Ok(SlashCommandEvent::EndSection {
|
||||||
|
metadata: section.metadata,
|
||||||
|
}));
|
||||||
|
|
||||||
|
last_section_end = section.range.end;
|
||||||
|
}
|
||||||
|
|
||||||
|
if last_section_end < self.text.len() {
|
||||||
|
events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text {
|
||||||
|
text: self.text[last_section_end..].to_string(),
|
||||||
|
run_commands_in_text: self.run_commands_in_text,
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
stream::iter(events).boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn from_event_stream(
|
||||||
|
mut events: BoxStream<'static, Result<SlashCommandEvent>>,
|
||||||
|
) -> Result<SlashCommandOutput> {
|
||||||
|
let mut output = SlashCommandOutput::default();
|
||||||
|
let mut section_stack = Vec::new();
|
||||||
|
|
||||||
|
while let Some(event) = events.next().await {
|
||||||
|
match event? {
|
||||||
|
SlashCommandEvent::StartSection {
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
metadata,
|
||||||
|
} => {
|
||||||
|
let start = output.text.len();
|
||||||
|
section_stack.push(SlashCommandOutputSection {
|
||||||
|
range: start..start,
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
metadata,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
SlashCommandEvent::Content(SlashCommandContent::Text {
|
||||||
|
text,
|
||||||
|
run_commands_in_text,
|
||||||
|
}) => {
|
||||||
|
output.text.push_str(&text);
|
||||||
|
output.run_commands_in_text = run_commands_in_text;
|
||||||
|
|
||||||
|
if let Some(section) = section_stack.last_mut() {
|
||||||
|
section.range.end = output.text.len();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SlashCommandEvent::EndSection { metadata } => {
|
||||||
|
if let Some(mut section) = section_stack.pop() {
|
||||||
|
section.metadata = metadata;
|
||||||
|
output.sections.push(section);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some(section) = section_stack.pop() {
|
||||||
|
output.sections.push(section);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct SlashCommandOutputSection<T> {
|
pub struct SlashCommandOutputSection<T> {
|
||||||
pub range: Range<T>,
|
pub range: Range<T>,
|
||||||
@@ -116,3 +256,243 @@ impl SlashCommandOutputSection<language::Anchor> {
|
|||||||
self.range.start.is_valid(buffer) && !self.range.to_offset(buffer).is_empty()
|
self.range.start.is_valid(buffer) && !self.range.to_offset(buffer).is_empty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_slash_command_output_to_events_round_trip() {
|
||||||
|
// Test basic output consisting of a single section.
|
||||||
|
{
|
||||||
|
let text = "Hello, world!".to_string();
|
||||||
|
let range = 0..text.len();
|
||||||
|
let output = SlashCommandOutput {
|
||||||
|
text,
|
||||||
|
sections: vec![SlashCommandOutputSection {
|
||||||
|
range,
|
||||||
|
icon: IconName::Code,
|
||||||
|
label: "Section 1".into(),
|
||||||
|
metadata: None,
|
||||||
|
}],
|
||||||
|
run_commands_in_text: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let events = output.clone().to_event_stream().collect::<Vec<_>>().await;
|
||||||
|
let events = events
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|event| event.ok())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
events,
|
||||||
|
vec![
|
||||||
|
SlashCommandEvent::StartSection {
|
||||||
|
icon: IconName::Code,
|
||||||
|
label: "Section 1".into(),
|
||||||
|
metadata: None
|
||||||
|
},
|
||||||
|
SlashCommandEvent::Content(SlashCommandContent::Text {
|
||||||
|
text: "Hello, world!".into(),
|
||||||
|
run_commands_in_text: false
|
||||||
|
}),
|
||||||
|
SlashCommandEvent::EndSection { metadata: None }
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
let new_output =
|
||||||
|
SlashCommandOutput::from_event_stream(output.clone().to_event_stream())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(new_output, output);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test output where the sections do not comprise all of the text.
|
||||||
|
{
|
||||||
|
let text = "Apple\nCucumber\nBanana\n".to_string();
|
||||||
|
let output = SlashCommandOutput {
|
||||||
|
text,
|
||||||
|
sections: vec![
|
||||||
|
SlashCommandOutputSection {
|
||||||
|
range: 0..6,
|
||||||
|
icon: IconName::Check,
|
||||||
|
label: "Fruit".into(),
|
||||||
|
metadata: None,
|
||||||
|
},
|
||||||
|
SlashCommandOutputSection {
|
||||||
|
range: 15..22,
|
||||||
|
icon: IconName::Check,
|
||||||
|
label: "Fruit".into(),
|
||||||
|
metadata: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
run_commands_in_text: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let events = output.clone().to_event_stream().collect::<Vec<_>>().await;
|
||||||
|
let events = events
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|event| event.ok())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
events,
|
||||||
|
vec![
|
||||||
|
SlashCommandEvent::StartSection {
|
||||||
|
icon: IconName::Check,
|
||||||
|
label: "Fruit".into(),
|
||||||
|
metadata: None
|
||||||
|
},
|
||||||
|
SlashCommandEvent::Content(SlashCommandContent::Text {
|
||||||
|
text: "Apple\n".into(),
|
||||||
|
run_commands_in_text: false
|
||||||
|
}),
|
||||||
|
SlashCommandEvent::EndSection { metadata: None },
|
||||||
|
SlashCommandEvent::Content(SlashCommandContent::Text {
|
||||||
|
text: "Cucumber\n".into(),
|
||||||
|
run_commands_in_text: false
|
||||||
|
}),
|
||||||
|
SlashCommandEvent::StartSection {
|
||||||
|
icon: IconName::Check,
|
||||||
|
label: "Fruit".into(),
|
||||||
|
metadata: None
|
||||||
|
},
|
||||||
|
SlashCommandEvent::Content(SlashCommandContent::Text {
|
||||||
|
text: "Banana\n".into(),
|
||||||
|
run_commands_in_text: false
|
||||||
|
}),
|
||||||
|
SlashCommandEvent::EndSection { metadata: None }
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
let new_output =
|
||||||
|
SlashCommandOutput::from_event_stream(output.clone().to_event_stream())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(new_output, output);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test output consisting of multiple sections.
|
||||||
|
{
|
||||||
|
let text = "Line 1\nLine 2\nLine 3\nLine 4\n".to_string();
|
||||||
|
let output = SlashCommandOutput {
|
||||||
|
text,
|
||||||
|
sections: vec![
|
||||||
|
SlashCommandOutputSection {
|
||||||
|
range: 0..6,
|
||||||
|
icon: IconName::FileCode,
|
||||||
|
label: "Section 1".into(),
|
||||||
|
metadata: Some(json!({ "a": true })),
|
||||||
|
},
|
||||||
|
SlashCommandOutputSection {
|
||||||
|
range: 7..13,
|
||||||
|
icon: IconName::FileDoc,
|
||||||
|
label: "Section 2".into(),
|
||||||
|
metadata: Some(json!({ "b": true })),
|
||||||
|
},
|
||||||
|
SlashCommandOutputSection {
|
||||||
|
range: 14..20,
|
||||||
|
icon: IconName::FileGit,
|
||||||
|
label: "Section 3".into(),
|
||||||
|
metadata: Some(json!({ "c": true })),
|
||||||
|
},
|
||||||
|
SlashCommandOutputSection {
|
||||||
|
range: 21..27,
|
||||||
|
icon: IconName::FileToml,
|
||||||
|
label: "Section 4".into(),
|
||||||
|
metadata: Some(json!({ "d": true })),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
run_commands_in_text: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let events = output.clone().to_event_stream().collect::<Vec<_>>().await;
|
||||||
|
let events = events
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|event| event.ok())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
events,
|
||||||
|
vec![
|
||||||
|
SlashCommandEvent::StartSection {
|
||||||
|
icon: IconName::FileCode,
|
||||||
|
label: "Section 1".into(),
|
||||||
|
metadata: Some(json!({ "a": true }))
|
||||||
|
},
|
||||||
|
SlashCommandEvent::Content(SlashCommandContent::Text {
|
||||||
|
text: "Line 1".into(),
|
||||||
|
run_commands_in_text: false
|
||||||
|
}),
|
||||||
|
SlashCommandEvent::EndSection {
|
||||||
|
metadata: Some(json!({ "a": true }))
|
||||||
|
},
|
||||||
|
SlashCommandEvent::Content(SlashCommandContent::Text {
|
||||||
|
text: "\n".into(),
|
||||||
|
run_commands_in_text: false
|
||||||
|
}),
|
||||||
|
SlashCommandEvent::StartSection {
|
||||||
|
icon: IconName::FileDoc,
|
||||||
|
label: "Section 2".into(),
|
||||||
|
metadata: Some(json!({ "b": true }))
|
||||||
|
},
|
||||||
|
SlashCommandEvent::Content(SlashCommandContent::Text {
|
||||||
|
text: "Line 2".into(),
|
||||||
|
run_commands_in_text: false
|
||||||
|
}),
|
||||||
|
SlashCommandEvent::EndSection {
|
||||||
|
metadata: Some(json!({ "b": true }))
|
||||||
|
},
|
||||||
|
SlashCommandEvent::Content(SlashCommandContent::Text {
|
||||||
|
text: "\n".into(),
|
||||||
|
run_commands_in_text: false
|
||||||
|
}),
|
||||||
|
SlashCommandEvent::StartSection {
|
||||||
|
icon: IconName::FileGit,
|
||||||
|
label: "Section 3".into(),
|
||||||
|
metadata: Some(json!({ "c": true }))
|
||||||
|
},
|
||||||
|
SlashCommandEvent::Content(SlashCommandContent::Text {
|
||||||
|
text: "Line 3".into(),
|
||||||
|
run_commands_in_text: false
|
||||||
|
}),
|
||||||
|
SlashCommandEvent::EndSection {
|
||||||
|
metadata: Some(json!({ "c": true }))
|
||||||
|
},
|
||||||
|
SlashCommandEvent::Content(SlashCommandContent::Text {
|
||||||
|
text: "\n".into(),
|
||||||
|
run_commands_in_text: false
|
||||||
|
}),
|
||||||
|
SlashCommandEvent::StartSection {
|
||||||
|
icon: IconName::FileToml,
|
||||||
|
label: "Section 4".into(),
|
||||||
|
metadata: Some(json!({ "d": true }))
|
||||||
|
},
|
||||||
|
SlashCommandEvent::Content(SlashCommandContent::Text {
|
||||||
|
text: "Line 4".into(),
|
||||||
|
run_commands_in_text: false
|
||||||
|
}),
|
||||||
|
SlashCommandEvent::EndSection {
|
||||||
|
metadata: Some(json!({ "d": true }))
|
||||||
|
},
|
||||||
|
SlashCommandEvent::Content(SlashCommandContent::Text {
|
||||||
|
text: "\n".into(),
|
||||||
|
run_commands_in_text: false
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
let new_output =
|
||||||
|
SlashCommandOutput::from_event_stream(output.clone().to_event_stream())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(new_output, output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,4 +32,5 @@ settings.workspace = true
|
|||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
tempfile.workspace = true
|
tempfile.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
|
which.workspace = true
|
||||||
workspace.workspace = true
|
workspace.workspace = true
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
|
use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
|
||||||
|
use paths::remote_servers_dir;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_derive::Serialize;
|
use serde_derive::Serialize;
|
||||||
@@ -33,6 +34,7 @@ use std::{
|
|||||||
};
|
};
|
||||||
use update_notification::UpdateNotification;
|
use update_notification::UpdateNotification;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
use which::which;
|
||||||
use workspace::notifications::NotificationId;
|
use workspace::notifications::NotificationId;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
@@ -82,9 +84,9 @@ pub struct AutoUpdater {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct JsonRelease {
|
pub struct JsonRelease {
|
||||||
version: String,
|
pub version: String,
|
||||||
url: String,
|
pub url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct MacOsUnmounter {
|
struct MacOsUnmounter {
|
||||||
@@ -430,10 +432,11 @@ impl AutoUpdater {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_latest_remote_server_release(
|
pub async fn download_remote_server_release(
|
||||||
os: &str,
|
os: &str,
|
||||||
arch: &str,
|
arch: &str,
|
||||||
mut release_channel: ReleaseChannel,
|
release_channel: ReleaseChannel,
|
||||||
|
version: Option<SemanticVersion>,
|
||||||
cx: &mut AsyncAppContext,
|
cx: &mut AsyncAppContext,
|
||||||
) -> Result<PathBuf> {
|
) -> Result<PathBuf> {
|
||||||
let this = cx.update(|cx| {
|
let this = cx.update(|cx| {
|
||||||
@@ -443,15 +446,12 @@ impl AutoUpdater {
|
|||||||
.ok_or_else(|| anyhow!("auto-update not initialized"))
|
.ok_or_else(|| anyhow!("auto-update not initialized"))
|
||||||
})??;
|
})??;
|
||||||
|
|
||||||
if release_channel == ReleaseChannel::Dev {
|
let release = Self::get_release(
|
||||||
release_channel = ReleaseChannel::Nightly;
|
|
||||||
}
|
|
||||||
|
|
||||||
let release = Self::get_latest_release(
|
|
||||||
&this,
|
&this,
|
||||||
"zed-remote-server",
|
"zed-remote-server",
|
||||||
os,
|
os,
|
||||||
arch,
|
arch,
|
||||||
|
version,
|
||||||
Some(release_channel),
|
Some(release_channel),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
@@ -466,13 +466,97 @@ impl AutoUpdater {
|
|||||||
let client = this.read_with(cx, |this, _| this.http_client.clone())?;
|
let client = this.read_with(cx, |this, _| this.http_client.clone())?;
|
||||||
|
|
||||||
if smol::fs::metadata(&version_path).await.is_err() {
|
if smol::fs::metadata(&version_path).await.is_err() {
|
||||||
log::info!("downloading zed-remote-server {os} {arch}");
|
log::info!(
|
||||||
|
"downloading zed-remote-server {os} {arch} version {}",
|
||||||
|
release.version
|
||||||
|
);
|
||||||
download_remote_server_binary(&version_path, release, client, cx).await?;
|
download_remote_server_binary(&version_path, release, client, cx).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(version_path)
|
Ok(version_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_remote_server_release_url(
|
||||||
|
os: &str,
|
||||||
|
arch: &str,
|
||||||
|
release_channel: ReleaseChannel,
|
||||||
|
version: Option<SemanticVersion>,
|
||||||
|
cx: &mut AsyncAppContext,
|
||||||
|
) -> Result<(JsonRelease, String)> {
|
||||||
|
let this = cx.update(|cx| {
|
||||||
|
cx.default_global::<GlobalAutoUpdate>()
|
||||||
|
.0
|
||||||
|
.clone()
|
||||||
|
.ok_or_else(|| anyhow!("auto-update not initialized"))
|
||||||
|
})??;
|
||||||
|
|
||||||
|
let release = Self::get_release(
|
||||||
|
&this,
|
||||||
|
"zed-remote-server",
|
||||||
|
os,
|
||||||
|
arch,
|
||||||
|
version,
|
||||||
|
Some(release_channel),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let update_request_body = build_remote_server_update_request_body(cx)?;
|
||||||
|
let body = serde_json::to_string(&update_request_body)?;
|
||||||
|
|
||||||
|
Ok((release, body))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_release(
|
||||||
|
this: &Model<Self>,
|
||||||
|
asset: &str,
|
||||||
|
os: &str,
|
||||||
|
arch: &str,
|
||||||
|
version: Option<SemanticVersion>,
|
||||||
|
release_channel: Option<ReleaseChannel>,
|
||||||
|
cx: &mut AsyncAppContext,
|
||||||
|
) -> Result<JsonRelease> {
|
||||||
|
let client = this.read_with(cx, |this, _| this.http_client.clone())?;
|
||||||
|
|
||||||
|
if let Some(version) = version {
|
||||||
|
let channel = release_channel.map(|c| c.dev_name()).unwrap_or("stable");
|
||||||
|
|
||||||
|
let url = format!("/api/releases/{channel}/{version}/{asset}-{os}-{arch}.gz?update=1",);
|
||||||
|
|
||||||
|
Ok(JsonRelease {
|
||||||
|
version: version.to_string(),
|
||||||
|
url: client.build_url(&url),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let mut url_string = client.build_url(&format!(
|
||||||
|
"/api/releases/latest?asset={}&os={}&arch={}",
|
||||||
|
asset, os, arch
|
||||||
|
));
|
||||||
|
if let Some(param) = release_channel.and_then(|c| c.release_query_param()) {
|
||||||
|
url_string += "&";
|
||||||
|
url_string += param;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut response = client.get(&url_string, Default::default(), true).await?;
|
||||||
|
let mut body = Vec::new();
|
||||||
|
response.body_mut().read_to_end(&mut body).await?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"failed to fetch release: {:?}",
|
||||||
|
String::from_utf8_lossy(&body),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
serde_json::from_slice(body.as_slice()).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"error deserializing release {:?}",
|
||||||
|
String::from_utf8_lossy(&body),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn get_latest_release(
|
async fn get_latest_release(
|
||||||
this: &Model<Self>,
|
this: &Model<Self>,
|
||||||
asset: &str,
|
asset: &str,
|
||||||
@@ -481,38 +565,7 @@ impl AutoUpdater {
|
|||||||
release_channel: Option<ReleaseChannel>,
|
release_channel: Option<ReleaseChannel>,
|
||||||
cx: &mut AsyncAppContext,
|
cx: &mut AsyncAppContext,
|
||||||
) -> Result<JsonRelease> {
|
) -> Result<JsonRelease> {
|
||||||
let client = this.read_with(cx, |this, _| this.http_client.clone())?;
|
Self::get_release(this, asset, os, arch, None, release_channel, cx).await
|
||||||
let mut url_string = client.build_url(&format!(
|
|
||||||
"/api/releases/latest?asset={}&os={}&arch={}",
|
|
||||||
asset, os, arch
|
|
||||||
));
|
|
||||||
if let Some(param) = release_channel.and_then(|c| c.release_query_param()) {
|
|
||||||
url_string += "&";
|
|
||||||
url_string += param;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut response = client.get(&url_string, Default::default(), true).await?;
|
|
||||||
|
|
||||||
let mut body = Vec::new();
|
|
||||||
response
|
|
||||||
.body_mut()
|
|
||||||
.read_to_end(&mut body)
|
|
||||||
.await
|
|
||||||
.context("error reading release")?;
|
|
||||||
|
|
||||||
if !response.status().is_success() {
|
|
||||||
Err(anyhow!(
|
|
||||||
"failed to fetch release: {:?}",
|
|
||||||
String::from_utf8_lossy(&body),
|
|
||||||
))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
serde_json::from_slice(body.as_slice()).with_context(|| {
|
|
||||||
format!(
|
|
||||||
"error deserializing release {:?}",
|
|
||||||
String::from_utf8_lossy(&body),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update(this: Model<Self>, mut cx: AsyncAppContext) -> Result<()> {
|
async fn update(this: Model<Self>, mut cx: AsyncAppContext) -> Result<()> {
|
||||||
@@ -560,6 +613,12 @@ impl AutoUpdater {
|
|||||||
"linux" => Ok("zed.tar.gz"),
|
"linux" => Ok("zed.tar.gz"),
|
||||||
_ => Err(anyhow!("not supported: {:?}", OS)),
|
_ => Err(anyhow!("not supported: {:?}", OS)),
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
|
anyhow::ensure!(
|
||||||
|
which("rsync").is_ok(),
|
||||||
|
"Aborting. Could not find rsync which is required for auto-updates."
|
||||||
|
);
|
||||||
|
|
||||||
let downloaded_asset = temp_dir.path().join(filename);
|
let downloaded_asset = temp_dir.path().join(filename);
|
||||||
download_release(&downloaded_asset, release, client, &cx).await?;
|
download_release(&downloaded_asset, release, client, &cx).await?;
|
||||||
|
|
||||||
@@ -621,7 +680,25 @@ async fn download_remote_server_binary(
|
|||||||
client: Arc<HttpClientWithUrl>,
|
client: Arc<HttpClientWithUrl>,
|
||||||
cx: &AsyncAppContext,
|
cx: &AsyncAppContext,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut target_file = File::create(&target_path).await?;
|
let temp = tempfile::Builder::new().tempfile_in(remote_servers_dir())?;
|
||||||
|
let mut temp_file = File::create(&temp).await?;
|
||||||
|
let update_request_body = build_remote_server_update_request_body(cx)?;
|
||||||
|
let request_body = AsyncBody::from(serde_json::to_string(&update_request_body)?);
|
||||||
|
|
||||||
|
let mut response = client.get(&release.url, request_body, true).await?;
|
||||||
|
if !response.status().is_success() {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"failed to download remote server release: {:?}",
|
||||||
|
response.status()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
smol::io::copy(response.body_mut(), &mut temp_file).await?;
|
||||||
|
smol::fs::rename(&temp, &target_path).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_remote_server_update_request_body(cx: &AsyncAppContext) -> Result<UpdateRequestBody> {
|
||||||
let (installation_id, release_channel, telemetry_enabled, is_staff) = cx.update(|cx| {
|
let (installation_id, release_channel, telemetry_enabled, is_staff) = cx.update(|cx| {
|
||||||
let telemetry = Client::global(cx).telemetry().clone();
|
let telemetry = Client::global(cx).telemetry().clone();
|
||||||
let is_staff = telemetry.is_staff();
|
let is_staff = telemetry.is_staff();
|
||||||
@@ -637,17 +714,14 @@ async fn download_remote_server_binary(
|
|||||||
is_staff,
|
is_staff,
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody {
|
|
||||||
|
Ok(UpdateRequestBody {
|
||||||
installation_id,
|
installation_id,
|
||||||
release_channel,
|
release_channel,
|
||||||
telemetry: telemetry_enabled,
|
telemetry: telemetry_enabled,
|
||||||
is_staff,
|
is_staff,
|
||||||
destination: "remote",
|
destination: "remote",
|
||||||
})?);
|
})
|
||||||
|
|
||||||
let mut response = client.get(&release.url, request_body, true).await?;
|
|
||||||
smol::io::copy(response.body_mut(), &mut target_file).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn download_release(
|
async fn download_release(
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ path = "src/call.rs"
|
|||||||
doctest = false
|
doctest = false
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
no-webrtc = ["live_kit_client/no-webrtc"]
|
|
||||||
test-support = [
|
test-support = [
|
||||||
"client/test-support",
|
"client/test-support",
|
||||||
"collections/test-support",
|
"collections/test-support",
|
||||||
|
|||||||
@@ -1194,26 +1194,15 @@ impl Room {
|
|||||||
project: Model<Project>,
|
project: Model<Project>,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Result<u64>> {
|
) -> Task<Result<u64>> {
|
||||||
let request = if let Some(dev_server_project_id) = project.read(cx).dev_server_project_id()
|
if let Some(project_id) = project.read(cx).remote_id() {
|
||||||
{
|
return Task::ready(Ok(project_id));
|
||||||
self.client.request(proto::ShareProject {
|
}
|
||||||
room_id: self.id(),
|
|
||||||
worktrees: vec![],
|
|
||||||
dev_server_project_id: Some(dev_server_project_id.0),
|
|
||||||
is_ssh_project: false,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
if let Some(project_id) = project.read(cx).remote_id() {
|
|
||||||
return Task::ready(Ok(project_id));
|
|
||||||
}
|
|
||||||
|
|
||||||
self.client.request(proto::ShareProject {
|
let request = self.client.request(proto::ShareProject {
|
||||||
room_id: self.id(),
|
room_id: self.id(),
|
||||||
worktrees: project.read(cx).worktree_metadata_protos(cx),
|
worktrees: project.read(cx).worktree_metadata_protos(cx),
|
||||||
dev_server_project_id: None,
|
is_ssh_project: project.read(cx).is_via_ssh(),
|
||||||
is_ssh_project: project.read(cx).is_via_ssh(),
|
});
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
let response = request.await?;
|
let response = request.await?;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ mod channel_index;
|
|||||||
use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat, ChannelMessage};
|
use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat, ChannelMessage};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use channel_index::ChannelIndex;
|
use channel_index::ChannelIndex;
|
||||||
use client::{ChannelId, Client, ClientSettings, ProjectId, Subscription, User, UserId, UserStore};
|
use client::{ChannelId, Client, ClientSettings, Subscription, User, UserId, UserStore};
|
||||||
use collections::{hash_map, HashMap, HashSet};
|
use collections::{hash_map, HashMap, HashSet};
|
||||||
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
|
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
@@ -33,30 +33,11 @@ struct NotesVersion {
|
|||||||
version: clock::Global,
|
version: clock::Global,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct HostedProject {
|
|
||||||
project_id: ProjectId,
|
|
||||||
channel_id: ChannelId,
|
|
||||||
name: SharedString,
|
|
||||||
_visibility: proto::ChannelVisibility,
|
|
||||||
}
|
|
||||||
impl From<proto::HostedProject> for HostedProject {
|
|
||||||
fn from(project: proto::HostedProject) -> Self {
|
|
||||||
Self {
|
|
||||||
project_id: ProjectId(project.project_id),
|
|
||||||
channel_id: ChannelId(project.channel_id),
|
|
||||||
_visibility: project.visibility(),
|
|
||||||
name: project.name.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub struct ChannelStore {
|
pub struct ChannelStore {
|
||||||
pub channel_index: ChannelIndex,
|
pub channel_index: ChannelIndex,
|
||||||
channel_invitations: Vec<Arc<Channel>>,
|
channel_invitations: Vec<Arc<Channel>>,
|
||||||
channel_participants: HashMap<ChannelId, Vec<Arc<User>>>,
|
channel_participants: HashMap<ChannelId, Vec<Arc<User>>>,
|
||||||
channel_states: HashMap<ChannelId, ChannelState>,
|
channel_states: HashMap<ChannelId, ChannelState>,
|
||||||
hosted_projects: HashMap<ProjectId, HostedProject>,
|
|
||||||
|
|
||||||
outgoing_invites: HashSet<(ChannelId, UserId)>,
|
outgoing_invites: HashSet<(ChannelId, UserId)>,
|
||||||
update_channels_tx: mpsc::UnboundedSender<proto::UpdateChannels>,
|
update_channels_tx: mpsc::UnboundedSender<proto::UpdateChannels>,
|
||||||
opened_buffers: HashMap<ChannelId, OpenedModelHandle<ChannelBuffer>>,
|
opened_buffers: HashMap<ChannelId, OpenedModelHandle<ChannelBuffer>>,
|
||||||
@@ -85,7 +66,6 @@ pub struct ChannelState {
|
|||||||
observed_notes_version: NotesVersion,
|
observed_notes_version: NotesVersion,
|
||||||
observed_chat_message: Option<u64>,
|
observed_chat_message: Option<u64>,
|
||||||
role: Option<ChannelRole>,
|
role: Option<ChannelRole>,
|
||||||
projects: HashSet<ProjectId>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Channel {
|
impl Channel {
|
||||||
@@ -216,7 +196,6 @@ impl ChannelStore {
|
|||||||
channel_invitations: Vec::default(),
|
channel_invitations: Vec::default(),
|
||||||
channel_index: ChannelIndex::default(),
|
channel_index: ChannelIndex::default(),
|
||||||
channel_participants: Default::default(),
|
channel_participants: Default::default(),
|
||||||
hosted_projects: Default::default(),
|
|
||||||
outgoing_invites: Default::default(),
|
outgoing_invites: Default::default(),
|
||||||
opened_buffers: Default::default(),
|
opened_buffers: Default::default(),
|
||||||
opened_chats: Default::default(),
|
opened_chats: Default::default(),
|
||||||
@@ -316,19 +295,6 @@ impl ChannelStore {
|
|||||||
self.channel_index.by_id().get(&channel_id)
|
self.channel_index.by_id().get(&channel_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn projects_for_id(&self, channel_id: ChannelId) -> Vec<(SharedString, ProjectId)> {
|
|
||||||
let mut projects: Vec<(SharedString, ProjectId)> = self
|
|
||||||
.channel_states
|
|
||||||
.get(&channel_id)
|
|
||||||
.map(|state| state.projects.clone())
|
|
||||||
.unwrap_or_default()
|
|
||||||
.into_iter()
|
|
||||||
.flat_map(|id| Some((self.hosted_projects.get(&id)?.name.clone(), id)))
|
|
||||||
.collect();
|
|
||||||
projects.sort();
|
|
||||||
projects
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_open_channel_buffer(&self, channel_id: ChannelId, _cx: &AppContext) -> bool {
|
pub fn has_open_channel_buffer(&self, channel_id: ChannelId, _cx: &AppContext) -> bool {
|
||||||
if let Some(buffer) = self.opened_buffers.get(&channel_id) {
|
if let Some(buffer) = self.opened_buffers.get(&channel_id) {
|
||||||
if let OpenedModelHandle::Open(buffer) = buffer {
|
if let OpenedModelHandle::Open(buffer) = buffer {
|
||||||
@@ -1102,9 +1068,7 @@ impl ChannelStore {
|
|||||||
let channels_changed = !payload.channels.is_empty()
|
let channels_changed = !payload.channels.is_empty()
|
||||||
|| !payload.delete_channels.is_empty()
|
|| !payload.delete_channels.is_empty()
|
||||||
|| !payload.latest_channel_message_ids.is_empty()
|
|| !payload.latest_channel_message_ids.is_empty()
|
||||||
|| !payload.latest_channel_buffer_versions.is_empty()
|
|| !payload.latest_channel_buffer_versions.is_empty();
|
||||||
|| !payload.hosted_projects.is_empty()
|
|
||||||
|| !payload.deleted_hosted_projects.is_empty();
|
|
||||||
|
|
||||||
if channels_changed {
|
if channels_changed {
|
||||||
if !payload.delete_channels.is_empty() {
|
if !payload.delete_channels.is_empty() {
|
||||||
@@ -1161,34 +1125,6 @@ impl ChannelStore {
|
|||||||
.or_default()
|
.or_default()
|
||||||
.update_latest_message_id(latest_channel_message.message_id);
|
.update_latest_message_id(latest_channel_message.message_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
for hosted_project in payload.hosted_projects {
|
|
||||||
let hosted_project: HostedProject = hosted_project.into();
|
|
||||||
if let Some(old_project) = self
|
|
||||||
.hosted_projects
|
|
||||||
.insert(hosted_project.project_id, hosted_project.clone())
|
|
||||||
{
|
|
||||||
self.channel_states
|
|
||||||
.entry(old_project.channel_id)
|
|
||||||
.or_default()
|
|
||||||
.remove_hosted_project(old_project.project_id);
|
|
||||||
}
|
|
||||||
self.channel_states
|
|
||||||
.entry(hosted_project.channel_id)
|
|
||||||
.or_default()
|
|
||||||
.add_hosted_project(hosted_project.project_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
for hosted_project_id in payload.deleted_hosted_projects {
|
|
||||||
let hosted_project_id = ProjectId(hosted_project_id);
|
|
||||||
|
|
||||||
if let Some(old_project) = self.hosted_projects.remove(&hosted_project_id) {
|
|
||||||
self.channel_states
|
|
||||||
.entry(old_project.channel_id)
|
|
||||||
.or_default()
|
|
||||||
.remove_hosted_project(old_project.project_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -1295,12 +1231,4 @@ impl ChannelState {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_hosted_project(&mut self, project_id: ProjectId) {
|
|
||||||
self.projects.insert(project_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_hosted_project(&mut self, project_id: ProjectId) {
|
|
||||||
self.projects.remove(&project_id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ pub enum CliRequest {
|
|||||||
urls: Vec<String>,
|
urls: Vec<String>,
|
||||||
wait: bool,
|
wait: bool,
|
||||||
open_new_workspace: Option<bool>,
|
open_new_workspace: Option<bool>,
|
||||||
dev_server_token: Option<String>,
|
|
||||||
env: Option<HashMap<String, String>>,
|
env: Option<HashMap<String, String>>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,6 +151,12 @@ fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(_) = args.dev_server_token {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Dev servers were removed in v0.157.x please upgrade to SSH remoting: https://zed.dev/docs/remote-development"
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
|
||||||
let sender: JoinHandle<anyhow::Result<()>> = thread::spawn({
|
let sender: JoinHandle<anyhow::Result<()>> = thread::spawn({
|
||||||
let exit_status = exit_status.clone();
|
let exit_status = exit_status.clone();
|
||||||
move || {
|
move || {
|
||||||
@@ -162,7 +168,6 @@ fn main() -> Result<()> {
|
|||||||
urls,
|
urls,
|
||||||
wait: args.wait,
|
wait: args.wait,
|
||||||
open_new_workspace,
|
open_new_workspace,
|
||||||
dev_server_token: args.dev_server_token,
|
|
||||||
env,
|
env,
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ use schemars::JsonSchema;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::{Settings, SettingsSources};
|
use settings::{Settings, SettingsSources};
|
||||||
use socks::connect_socks_proxy_stream;
|
use socks::connect_socks_proxy_stream;
|
||||||
use std::fmt;
|
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::{
|
use std::{
|
||||||
any::TypeId,
|
any::TypeId,
|
||||||
@@ -54,15 +53,6 @@ pub use rpc::*;
|
|||||||
pub use telemetry_events::Event;
|
pub use telemetry_events::Event;
|
||||||
pub use user::*;
|
pub use user::*;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
|
||||||
pub struct DevServerToken(pub String);
|
|
||||||
|
|
||||||
impl fmt::Display for DevServerToken {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "{}", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static ZED_SERVER_URL: LazyLock<Option<String>> =
|
static ZED_SERVER_URL: LazyLock<Option<String>> =
|
||||||
LazyLock::new(|| std::env::var("ZED_SERVER_URL").ok());
|
LazyLock::new(|| std::env::var("ZED_SERVER_URL").ok());
|
||||||
static ZED_RPC_URL: LazyLock<Option<String>> = LazyLock::new(|| std::env::var("ZED_RPC_URL").ok());
|
static ZED_RPC_URL: LazyLock<Option<String>> = LazyLock::new(|| std::env::var("ZED_RPC_URL").ok());
|
||||||
@@ -304,20 +294,14 @@ struct ClientState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub enum Credentials {
|
pub struct Credentials {
|
||||||
DevServer { token: DevServerToken },
|
pub user_id: u64,
|
||||||
User { user_id: u64, access_token: String },
|
pub access_token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Credentials {
|
impl Credentials {
|
||||||
pub fn authorization_header(&self) -> String {
|
pub fn authorization_header(&self) -> String {
|
||||||
match self {
|
format!("{} {}", self.user_id, self.access_token)
|
||||||
Credentials::DevServer { token } => format!("dev-server-token {}", token),
|
|
||||||
Credentials::User {
|
|
||||||
user_id,
|
|
||||||
access_token,
|
|
||||||
} => format!("{} {}", user_id, access_token),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -600,11 +584,11 @@ impl Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn user_id(&self) -> Option<u64> {
|
pub fn user_id(&self) -> Option<u64> {
|
||||||
if let Some(Credentials::User { user_id, .. }) = self.state.read().credentials.as_ref() {
|
self.state
|
||||||
Some(*user_id)
|
.read()
|
||||||
} else {
|
.credentials
|
||||||
None
|
.as_ref()
|
||||||
}
|
.map(|credentials| credentials.user_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn peer_id(&self) -> Option<PeerId> {
|
pub fn peer_id(&self) -> Option<PeerId> {
|
||||||
@@ -793,11 +777,6 @@ impl Client {
|
|||||||
.is_some()
|
.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_dev_server_token(&self, token: DevServerToken) -> &Self {
|
|
||||||
self.state.write().credentials = Some(Credentials::DevServer { token });
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_recursion(?Send)]
|
#[async_recursion(?Send)]
|
||||||
pub async fn authenticate_and_connect(
|
pub async fn authenticate_and_connect(
|
||||||
self: &Arc<Self>,
|
self: &Arc<Self>,
|
||||||
@@ -848,9 +827,7 @@ impl Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let credentials = credentials.unwrap();
|
let credentials = credentials.unwrap();
|
||||||
if let Credentials::User { user_id, .. } = &credentials {
|
self.set_id(credentials.user_id);
|
||||||
self.set_id(*user_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if was_disconnected {
|
if was_disconnected {
|
||||||
self.set_status(Status::Connecting, cx);
|
self.set_status(Status::Connecting, cx);
|
||||||
@@ -866,9 +843,8 @@ impl Client {
|
|||||||
Ok(conn) => {
|
Ok(conn) => {
|
||||||
self.state.write().credentials = Some(credentials.clone());
|
self.state.write().credentials = Some(credentials.clone());
|
||||||
if !read_from_provider && IMPERSONATE_LOGIN.is_none() {
|
if !read_from_provider && IMPERSONATE_LOGIN.is_none() {
|
||||||
if let Credentials::User{user_id, access_token} = credentials {
|
self.credentials_provider.write_credentials(credentials.user_id, credentials.access_token, cx).await.log_err();
|
||||||
self.credentials_provider.write_credentials(user_id, access_token, cx).await.log_err();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
futures::select_biased! {
|
futures::select_biased! {
|
||||||
@@ -1301,7 +1277,7 @@ impl Client {
|
|||||||
.decrypt_string(&access_token)
|
.decrypt_string(&access_token)
|
||||||
.context("failed to decrypt access token")?;
|
.context("failed to decrypt access token")?;
|
||||||
|
|
||||||
Ok(Credentials::User {
|
Ok(Credentials {
|
||||||
user_id: user_id.parse()?,
|
user_id: user_id.parse()?,
|
||||||
access_token,
|
access_token,
|
||||||
})
|
})
|
||||||
@@ -1422,7 +1398,7 @@ impl Client {
|
|||||||
|
|
||||||
// Use the admin API token to authenticate as the impersonated user.
|
// Use the admin API token to authenticate as the impersonated user.
|
||||||
api_token.insert_str(0, "ADMIN_TOKEN:");
|
api_token.insert_str(0, "ADMIN_TOKEN:");
|
||||||
Ok(Credentials::User {
|
Ok(Credentials {
|
||||||
user_id: response.user.id,
|
user_id: response.user.id,
|
||||||
access_token: api_token,
|
access_token: api_token,
|
||||||
})
|
})
|
||||||
@@ -1667,7 +1643,7 @@ impl CredentialsProvider for DevelopmentCredentialsProvider {
|
|||||||
|
|
||||||
let credentials: DevelopmentCredentials = serde_json::from_slice(&json).log_err()?;
|
let credentials: DevelopmentCredentials = serde_json::from_slice(&json).log_err()?;
|
||||||
|
|
||||||
Some(Credentials::User {
|
Some(Credentials {
|
||||||
user_id: credentials.user_id,
|
user_id: credentials.user_id,
|
||||||
access_token: credentials.access_token,
|
access_token: credentials.access_token,
|
||||||
})
|
})
|
||||||
@@ -1721,7 +1697,7 @@ impl CredentialsProvider for KeychainCredentialsProvider {
|
|||||||
.await
|
.await
|
||||||
.log_err()??;
|
.log_err()??;
|
||||||
|
|
||||||
Some(Credentials::User {
|
Some(Credentials {
|
||||||
user_id: user_id.parse().ok()?,
|
user_id: user_id.parse().ok()?,
|
||||||
access_token: String::from_utf8(access_token).ok()?,
|
access_token: String::from_utf8(access_token).ok()?,
|
||||||
})
|
})
|
||||||
@@ -1855,7 +1831,7 @@ mod tests {
|
|||||||
// Time out when client tries to connect.
|
// Time out when client tries to connect.
|
||||||
client.override_authenticate(move |cx| {
|
client.override_authenticate(move |cx| {
|
||||||
cx.background_executor().spawn(async move {
|
cx.background_executor().spawn(async move {
|
||||||
Ok(Credentials::User {
|
Ok(Credentials {
|
||||||
user_id,
|
user_id,
|
||||||
access_token: "token".into(),
|
access_token: "token".into(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -341,6 +341,13 @@ impl Telemetry {
|
|||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn metrics_enabled(self: &Arc<Self>) -> bool {
|
||||||
|
let state = self.state.lock();
|
||||||
|
let enabled = state.settings.metrics;
|
||||||
|
drop(state);
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_authenticated_user_info(
|
pub fn set_authenticated_user_info(
|
||||||
self: &Arc<Self>,
|
self: &Arc<Self>,
|
||||||
metrics_id: Option<String>,
|
metrics_id: Option<String>,
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ impl FakeServer {
|
|||||||
let mut state = state.lock();
|
let mut state = state.lock();
|
||||||
state.auth_count += 1;
|
state.auth_count += 1;
|
||||||
let access_token = state.access_token.to_string();
|
let access_token = state.access_token.to_string();
|
||||||
Ok(Credentials::User {
|
Ok(Credentials {
|
||||||
user_id: client_user_id,
|
user_id: client_user_id,
|
||||||
access_token,
|
access_token,
|
||||||
})
|
})
|
||||||
@@ -73,7 +73,7 @@ impl FakeServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if credentials
|
if credentials
|
||||||
!= (Credentials::User {
|
!= (Credentials {
|
||||||
user_id: client_user_id,
|
user_id: client_user_id,
|
||||||
access_token: state.lock().access_token.to_string(),
|
access_token: state.lock().access_token.to_string(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -28,9 +28,6 @@ impl std::fmt::Display for ChannelId {
|
|||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||||
pub struct ProjectId(pub u64);
|
pub struct ProjectId(pub u64);
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
|
||||||
pub struct DevServerId(pub u64);
|
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize,
|
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize,
|
||||||
)]
|
)]
|
||||||
@@ -51,6 +48,7 @@ pub struct Collaborator {
|
|||||||
pub peer_id: proto::PeerId,
|
pub peer_id: proto::PeerId,
|
||||||
pub replica_id: ReplicaId,
|
pub replica_id: ReplicaId,
|
||||||
pub user_id: UserId,
|
pub user_id: UserId,
|
||||||
|
pub is_host: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialOrd for User {
|
impl PartialOrd for User {
|
||||||
@@ -827,6 +825,7 @@ impl Collaborator {
|
|||||||
peer_id: message.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?,
|
peer_id: message.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?,
|
||||||
replica_id: message.replica_id as ReplicaId,
|
replica_id: message.replica_id as ReplicaId,
|
||||||
user_id: message.user_id as UserId,
|
user_id: message.user_id as UserId,
|
||||||
|
is_host: message.is_host,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,6 @@ client = { workspace = true, features = ["test-support"] }
|
|||||||
collab_ui = { workspace = true, features = ["test-support"] }
|
collab_ui = { workspace = true, features = ["test-support"] }
|
||||||
collections = { workspace = true, features = ["test-support"] }
|
collections = { workspace = true, features = ["test-support"] }
|
||||||
ctor.workspace = true
|
ctor.workspace = true
|
||||||
dev_server_projects.workspace = true
|
|
||||||
editor = { workspace = true, features = ["test-support"] }
|
editor = { workspace = true, features = ["test-support"] }
|
||||||
env_logger.workspace = true
|
env_logger.workspace = true
|
||||||
file_finder.workspace = true
|
file_finder.workspace = true
|
||||||
@@ -94,7 +93,6 @@ fs = { workspace = true, features = ["test-support"] }
|
|||||||
git = { workspace = true, features = ["test-support"] }
|
git = { workspace = true, features = ["test-support"] }
|
||||||
git_hosting_providers.workspace = true
|
git_hosting_providers.workspace = true
|
||||||
gpui = { workspace = true, features = ["test-support"] }
|
gpui = { workspace = true, features = ["test-support"] }
|
||||||
headless.workspace = true
|
|
||||||
hyper.workspace = true
|
hyper.workspace = true
|
||||||
indoc.workspace = true
|
indoc.workspace = true
|
||||||
language = { workspace = true, features = ["test-support"] }
|
language = { workspace = true, features = ["test-support"] }
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ CREATE TABLE "users" (
|
|||||||
"metrics_id" TEXT,
|
"metrics_id" TEXT,
|
||||||
"github_user_id" INTEGER NOT NULL,
|
"github_user_id" INTEGER NOT NULL,
|
||||||
"accepted_tos_at" TIMESTAMP WITHOUT TIME ZONE,
|
"accepted_tos_at" TIMESTAMP WITHOUT TIME ZONE,
|
||||||
"github_user_created_at" TIMESTAMP WITHOUT TIME ZONE
|
"github_user_created_at" TIMESTAMP WITHOUT TIME ZONE,
|
||||||
|
"custom_llm_monthly_allowance_in_cents" INTEGER
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX "index_users_github_login" ON "users" ("github_login");
|
CREATE UNIQUE INDEX "index_users_github_login" ON "users" ("github_login");
|
||||||
CREATE UNIQUE INDEX "index_invite_code_users" ON "users" ("invite_code");
|
CREATE UNIQUE INDEX "index_invite_code_users" ON "users" ("invite_code");
|
||||||
@@ -51,9 +52,7 @@ CREATE TABLE "projects" (
|
|||||||
"host_user_id" INTEGER REFERENCES users (id),
|
"host_user_id" INTEGER REFERENCES users (id),
|
||||||
"host_connection_id" INTEGER,
|
"host_connection_id" INTEGER,
|
||||||
"host_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE CASCADE,
|
"host_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE CASCADE,
|
||||||
"unregistered" BOOLEAN NOT NULL DEFAULT FALSE,
|
"unregistered" BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
"hosted_project_id" INTEGER REFERENCES hosted_projects (id),
|
|
||||||
"dev_server_project_id" INTEGER REFERENCES dev_server_projects(id)
|
|
||||||
);
|
);
|
||||||
CREATE INDEX "index_projects_on_host_connection_server_id" ON "projects" ("host_connection_server_id");
|
CREATE INDEX "index_projects_on_host_connection_server_id" ON "projects" ("host_connection_server_id");
|
||||||
CREATE INDEX "index_projects_on_host_connection_id_and_host_connection_server_id" ON "projects" ("host_connection_id", "host_connection_server_id");
|
CREATE INDEX "index_projects_on_host_connection_id_and_host_connection_server_id" ON "projects" ("host_connection_id", "host_connection_server_id");
|
||||||
@@ -398,30 +397,6 @@ CREATE TABLE rate_buckets (
|
|||||||
);
|
);
|
||||||
CREATE INDEX idx_user_id_rate_limit ON rate_buckets (user_id, rate_limit_name);
|
CREATE INDEX idx_user_id_rate_limit ON rate_buckets (user_id, rate_limit_name);
|
||||||
|
|
||||||
CREATE TABLE hosted_projects (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
channel_id INTEGER NOT NULL REFERENCES channels(id),
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
visibility TEXT NOT NULL,
|
|
||||||
deleted_at TIMESTAMP NULL
|
|
||||||
);
|
|
||||||
CREATE INDEX idx_hosted_projects_on_channel_id ON hosted_projects (channel_id);
|
|
||||||
CREATE UNIQUE INDEX uix_hosted_projects_on_channel_id_and_name ON hosted_projects (channel_id, name) WHERE (deleted_at IS NULL);
|
|
||||||
|
|
||||||
CREATE TABLE dev_servers (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
ssh_connection_string TEXT,
|
|
||||||
hashed_token TEXT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE dev_server_projects (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
dev_server_id INTEGER NOT NULL REFERENCES dev_servers(id),
|
|
||||||
paths TEXT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS billing_preferences (
|
CREATE TABLE IF NOT EXISTS billing_preferences (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table users add column custom_llm_monthly_allowance_in_cents integer;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
ALTER TABLE projects DROP COLUMN dev_server_project_id;
|
||||||
|
ALTER TABLE projects DROP COLUMN hosted_project_id;
|
||||||
|
|
||||||
|
DROP TABLE hosted_projects;
|
||||||
|
DROP TABLE dev_server_projects;
|
||||||
|
DROP TABLE dev_servers;
|
||||||
@@ -34,7 +34,7 @@ use crate::{
|
|||||||
db::{billing_subscription::StripeSubscriptionStatus, UserId},
|
db::{billing_subscription::StripeSubscriptionStatus, UserId},
|
||||||
llm::db::LlmDatabase,
|
llm::db::LlmDatabase,
|
||||||
};
|
};
|
||||||
use crate::{AppState, Error, Result};
|
use crate::{AppState, Cents, Error, Result};
|
||||||
|
|
||||||
pub fn router() -> Router {
|
pub fn router() -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
@@ -226,6 +226,13 @@ async fn create_billing_subscription(
|
|||||||
))?
|
))?
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if app.db.has_active_billing_subscription(user.id).await? {
|
||||||
|
return Err(Error::http(
|
||||||
|
StatusCode::CONFLICT,
|
||||||
|
"user already has an active subscription".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let customer_id =
|
let customer_id =
|
||||||
if let Some(existing_customer) = app.db.get_billing_customer_by_user_id(user.id).await? {
|
if let Some(existing_customer) = app.db.get_billing_customer_by_user_id(user.id).await? {
|
||||||
CustomerId::from_str(&existing_customer.stripe_customer_id)
|
CustomerId::from_str(&existing_customer.stripe_customer_id)
|
||||||
@@ -245,7 +252,10 @@ async fn create_billing_subscription(
|
|||||||
|
|
||||||
let default_model = llm_db.model(rpc::LanguageModelProvider::Anthropic, "claude-3-5-sonnet")?;
|
let default_model = llm_db.model(rpc::LanguageModelProvider::Anthropic, "claude-3-5-sonnet")?;
|
||||||
let stripe_model = stripe_billing.register_model(default_model).await?;
|
let stripe_model = stripe_billing.register_model(default_model).await?;
|
||||||
let success_url = format!("{}/account", app.config.zed_dot_dev_url());
|
let success_url = format!(
|
||||||
|
"{}/account?checkout_complete=1",
|
||||||
|
app.config.zed_dot_dev_url()
|
||||||
|
);
|
||||||
let checkout_session_url = stripe_billing
|
let checkout_session_url = stripe_billing
|
||||||
.checkout(customer_id, &user.github_login, &stripe_model, &success_url)
|
.checkout(customer_id, &user.github_login, &stripe_model, &success_url)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -655,6 +665,33 @@ async fn handle_customer_subscription_event(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
} else {
|
} else {
|
||||||
|
// If the user already has an active billing subscription, ignore the
|
||||||
|
// event and return an `Ok` to signal that it was processed
|
||||||
|
// successfully.
|
||||||
|
//
|
||||||
|
// There is the possibility that this could cause us to not create a
|
||||||
|
// subscription in the following scenario:
|
||||||
|
//
|
||||||
|
// 1. User has an active subscription A
|
||||||
|
// 2. User cancels subscription A
|
||||||
|
// 3. User creates a new subscription B
|
||||||
|
// 4. We process the new subscription B before the cancellation of subscription A
|
||||||
|
// 5. User ends up with no subscriptions
|
||||||
|
//
|
||||||
|
// In theory this situation shouldn't arise as we try to process the events in the order they occur.
|
||||||
|
if app
|
||||||
|
.db
|
||||||
|
.has_active_billing_subscription(billing_customer.user_id)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
log::info!(
|
||||||
|
"user {user_id} already has an active subscription, skipping creation of subscription {subscription_id}",
|
||||||
|
user_id = billing_customer.user_id,
|
||||||
|
subscription_id = subscription.id
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
app.db
|
app.db
|
||||||
.create_billing_subscription(&CreateBillingSubscriptionParams {
|
.create_billing_subscription(&CreateBillingSubscriptionParams {
|
||||||
billing_customer_id: billing_customer.id,
|
billing_customer_id: billing_customer.id,
|
||||||
@@ -680,7 +717,9 @@ struct GetMonthlySpendParams {
|
|||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
struct GetMonthlySpendResponse {
|
struct GetMonthlySpendResponse {
|
||||||
monthly_spend_in_cents: i32,
|
monthly_free_tier_spend_in_cents: u32,
|
||||||
|
monthly_free_tier_allowance_in_cents: u32,
|
||||||
|
monthly_spend_in_cents: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_monthly_spend(
|
async fn get_monthly_spend(
|
||||||
@@ -700,13 +739,22 @@ async fn get_monthly_spend(
|
|||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
let monthly_spend = llm_db
|
let free_tier = user
|
||||||
|
.custom_llm_monthly_allowance_in_cents
|
||||||
|
.map(|allowance| Cents(allowance as u32))
|
||||||
|
.unwrap_or(FREE_TIER_MONTHLY_SPENDING_LIMIT);
|
||||||
|
|
||||||
|
let spending_for_month = llm_db
|
||||||
.get_user_spending_for_month(user.id, Utc::now())
|
.get_user_spending_for_month(user.id, Utc::now())
|
||||||
.await?
|
.await?;
|
||||||
.saturating_sub(FREE_TIER_MONTHLY_SPENDING_LIMIT);
|
|
||||||
|
let free_tier_spend = Cents::min(spending_for_month, free_tier);
|
||||||
|
let monthly_spend = spending_for_month.saturating_sub(free_tier);
|
||||||
|
|
||||||
Ok(Json(GetMonthlySpendResponse {
|
Ok(Json(GetMonthlySpendResponse {
|
||||||
monthly_spend_in_cents: monthly_spend.0 as i32,
|
monthly_free_tier_spend_in_cents: free_tier_spend.0,
|
||||||
|
monthly_free_tier_allowance_in_cents: free_tier.0,
|
||||||
|
monthly_spend_in_cents: monthly_spend.0,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
db::{self, dev_server, AccessTokenId, Database, DevServerId, UserId},
|
db::{self, AccessTokenId, Database, UserId},
|
||||||
rpc::Principal,
|
rpc::Principal,
|
||||||
AppState, Error, Result,
|
AppState, Error, Result,
|
||||||
};
|
};
|
||||||
@@ -44,19 +44,10 @@ pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl Into
|
|||||||
|
|
||||||
let first = auth_header.next().unwrap_or("");
|
let first = auth_header.next().unwrap_or("");
|
||||||
if first == "dev-server-token" {
|
if first == "dev-server-token" {
|
||||||
let dev_server_token = auth_header.next().ok_or_else(|| {
|
Err(Error::http(
|
||||||
Error::http(
|
StatusCode::UNAUTHORIZED,
|
||||||
StatusCode::BAD_REQUEST,
|
"Dev servers were removed in Zed 0.157 please upgrade to SSH remoting".to_string(),
|
||||||
"missing dev-server-token token in authorization header".to_string(),
|
))?;
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let dev_server = verify_dev_server_token(dev_server_token, &state.db)
|
|
||||||
.await
|
|
||||||
.map_err(|e| Error::http(StatusCode::UNAUTHORIZED, format!("{}", e)))?;
|
|
||||||
|
|
||||||
req.extensions_mut()
|
|
||||||
.insert(Principal::DevServer(dev_server));
|
|
||||||
return Ok::<_, Error>(next.run(req).await);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_id = UserId(first.parse().map_err(|_| {
|
let user_id = UserId(first.parse().map_err(|_| {
|
||||||
@@ -240,41 +231,6 @@ pub async fn verify_access_token(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_dev_server_token(id: usize, access_token: String) -> String {
|
|
||||||
format!("{}.{}", id, access_token)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn verify_dev_server_token(
|
|
||||||
dev_server_token: &str,
|
|
||||||
db: &Arc<Database>,
|
|
||||||
) -> anyhow::Result<dev_server::Model> {
|
|
||||||
let (id, token) = split_dev_server_token(dev_server_token)?;
|
|
||||||
let token_hash = hash_access_token(token);
|
|
||||||
let server = db.get_dev_server(id).await?;
|
|
||||||
|
|
||||||
if server
|
|
||||||
.hashed_token
|
|
||||||
.as_bytes()
|
|
||||||
.ct_eq(token_hash.as_ref())
|
|
||||||
.into()
|
|
||||||
{
|
|
||||||
Ok(server)
|
|
||||||
} else {
|
|
||||||
Err(anyhow!("wrong token for dev server"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// a dev_server_token has the format <id>.<base64>. This is to make them
|
|
||||||
// relatively easy to copy/paste around.
|
|
||||||
pub fn split_dev_server_token(dev_server_token: &str) -> anyhow::Result<(DevServerId, &str)> {
|
|
||||||
let mut parts = dev_server_token.splitn(2, '.');
|
|
||||||
let id = DevServerId(parts.next().unwrap_or_default().parse()?);
|
|
||||||
let token = parts
|
|
||||||
.next()
|
|
||||||
.ok_or_else(|| anyhow!("invalid dev server token format"))?;
|
|
||||||
Ok((id, token))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use rand::thread_rng;
|
use rand::thread_rng;
|
||||||
|
|||||||
@@ -617,7 +617,6 @@ pub struct ChannelsForUser {
|
|||||||
pub channels: Vec<Channel>,
|
pub channels: Vec<Channel>,
|
||||||
pub channel_memberships: Vec<channel_member::Model>,
|
pub channel_memberships: Vec<channel_member::Model>,
|
||||||
pub channel_participants: HashMap<ChannelId, Vec<UserId>>,
|
pub channel_participants: HashMap<ChannelId, Vec<UserId>>,
|
||||||
pub hosted_projects: Vec<proto::HostedProject>,
|
|
||||||
pub invited_channels: Vec<Channel>,
|
pub invited_channels: Vec<Channel>,
|
||||||
|
|
||||||
pub observed_buffer_versions: Vec<proto::ChannelBufferVersion>,
|
pub observed_buffer_versions: Vec<proto::ChannelBufferVersion>,
|
||||||
@@ -726,7 +725,6 @@ pub struct Project {
|
|||||||
pub collaborators: Vec<ProjectCollaborator>,
|
pub collaborators: Vec<ProjectCollaborator>,
|
||||||
pub worktrees: BTreeMap<u64, Worktree>,
|
pub worktrees: BTreeMap<u64, Worktree>,
|
||||||
pub language_servers: Vec<proto::LanguageServer>,
|
pub language_servers: Vec<proto::LanguageServer>,
|
||||||
pub dev_server_project_id: Option<DevServerProjectId>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ProjectCollaborator {
|
pub struct ProjectCollaborator {
|
||||||
@@ -742,6 +740,7 @@ impl ProjectCollaborator {
|
|||||||
peer_id: Some(self.connection_id.into()),
|
peer_id: Some(self.connection_id.into()),
|
||||||
replica_id: self.replica_id.0 as u32,
|
replica_id: self.replica_id.0 as u32,
|
||||||
user_id: self.user_id.to_proto(),
|
user_id: self.user_id.to_proto(),
|
||||||
|
is_host: self.is_host,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,7 +79,6 @@ id_type!(ChannelChatParticipantId);
|
|||||||
id_type!(ChannelId);
|
id_type!(ChannelId);
|
||||||
id_type!(ChannelMemberId);
|
id_type!(ChannelMemberId);
|
||||||
id_type!(ContactId);
|
id_type!(ContactId);
|
||||||
id_type!(DevServerId);
|
|
||||||
id_type!(ExtensionId);
|
id_type!(ExtensionId);
|
||||||
id_type!(FlagId);
|
id_type!(FlagId);
|
||||||
id_type!(FollowerId);
|
id_type!(FollowerId);
|
||||||
@@ -89,7 +88,6 @@ id_type!(NotificationId);
|
|||||||
id_type!(NotificationKindId);
|
id_type!(NotificationKindId);
|
||||||
id_type!(ProjectCollaboratorId);
|
id_type!(ProjectCollaboratorId);
|
||||||
id_type!(ProjectId);
|
id_type!(ProjectId);
|
||||||
id_type!(DevServerProjectId);
|
|
||||||
id_type!(ReplicaId);
|
id_type!(ReplicaId);
|
||||||
id_type!(RoomId);
|
id_type!(RoomId);
|
||||||
id_type!(RoomParticipantId);
|
id_type!(RoomParticipantId);
|
||||||
@@ -277,12 +275,6 @@ impl From<ChannelVisibility> for i32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, PartialEq)]
|
|
||||||
pub enum PrincipalId {
|
|
||||||
UserId(UserId),
|
|
||||||
DevServerId(DevServerId),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Indicate whether a [Buffer] has permissions to edit.
|
/// Indicate whether a [Buffer] has permissions to edit.
|
||||||
#[derive(PartialEq, Clone, Copy, Debug)]
|
#[derive(PartialEq, Clone, Copy, Debug)]
|
||||||
pub enum Capability {
|
pub enum Capability {
|
||||||
|
|||||||
@@ -8,11 +8,8 @@ pub mod buffers;
|
|||||||
pub mod channels;
|
pub mod channels;
|
||||||
pub mod contacts;
|
pub mod contacts;
|
||||||
pub mod contributors;
|
pub mod contributors;
|
||||||
pub mod dev_server_projects;
|
|
||||||
pub mod dev_servers;
|
|
||||||
pub mod embeddings;
|
pub mod embeddings;
|
||||||
pub mod extensions;
|
pub mod extensions;
|
||||||
pub mod hosted_projects;
|
|
||||||
pub mod messages;
|
pub mod messages;
|
||||||
pub mod notifications;
|
pub mod notifications;
|
||||||
pub mod processed_stripe_events;
|
pub mod processed_stripe_events;
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ impl Database {
|
|||||||
peer_id: Some(collaborator.connection().into()),
|
peer_id: Some(collaborator.connection().into()),
|
||||||
user_id: collaborator.user_id.to_proto(),
|
user_id: collaborator.user_id.to_proto(),
|
||||||
replica_id: collaborator.replica_id.0 as u32,
|
replica_id: collaborator.replica_id.0 as u32,
|
||||||
|
is_host: false,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
})
|
})
|
||||||
@@ -222,6 +223,7 @@ impl Database {
|
|||||||
peer_id: Some(collaborator.connection().into()),
|
peer_id: Some(collaborator.connection().into()),
|
||||||
user_id: collaborator.user_id.to_proto(),
|
user_id: collaborator.user_id.to_proto(),
|
||||||
replica_id: collaborator.replica_id.0 as u32,
|
replica_id: collaborator.replica_id.0 as u32,
|
||||||
|
is_host: false,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
},
|
},
|
||||||
@@ -257,6 +259,7 @@ impl Database {
|
|||||||
peer_id: Some(db_collaborator.connection().into()),
|
peer_id: Some(db_collaborator.connection().into()),
|
||||||
replica_id: db_collaborator.replica_id.0 as u32,
|
replica_id: db_collaborator.replica_id.0 as u32,
|
||||||
user_id: db_collaborator.user_id.to_proto(),
|
user_id: db_collaborator.user_id.to_proto(),
|
||||||
|
is_host: false,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
collaborator_ids_to_remove.push(db_collaborator.id);
|
collaborator_ids_to_remove.push(db_collaborator.id);
|
||||||
@@ -385,6 +388,7 @@ impl Database {
|
|||||||
peer_id: Some(connection.into()),
|
peer_id: Some(connection.into()),
|
||||||
replica_id: row.replica_id.0 as u32,
|
replica_id: row.replica_id.0 as u32,
|
||||||
user_id: row.user_id.to_proto(),
|
user_id: row.user_id.to_proto(),
|
||||||
|
is_host: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -615,15 +615,10 @@ impl Database {
|
|||||||
.observed_channel_messages(&channel_ids, user_id, tx)
|
.observed_channel_messages(&channel_ids, user_id, tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let hosted_projects = self
|
|
||||||
.get_hosted_projects(&channel_ids, &roles_by_channel_id, tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(ChannelsForUser {
|
Ok(ChannelsForUser {
|
||||||
channel_memberships,
|
channel_memberships,
|
||||||
channels,
|
channels,
|
||||||
invited_channels,
|
invited_channels,
|
||||||
hosted_projects,
|
|
||||||
channel_participants,
|
channel_participants,
|
||||||
latest_buffer_versions,
|
latest_buffer_versions,
|
||||||
latest_channel_messages,
|
latest_channel_messages,
|
||||||
|
|||||||
@@ -1,365 +1 @@
|
|||||||
use anyhow::anyhow;
|
|
||||||
use rpc::{
|
|
||||||
proto::{self},
|
|
||||||
ConnectionId,
|
|
||||||
};
|
|
||||||
use sea_orm::{
|
|
||||||
ActiveModelTrait, ActiveValue, ColumnTrait, Condition, DatabaseTransaction, EntityTrait,
|
|
||||||
IntoActiveModel, ModelTrait, QueryFilter,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::db::ProjectId;
|
|
||||||
|
|
||||||
use super::{
|
|
||||||
dev_server, dev_server_project, project, project_collaborator, worktree, Database, DevServerId,
|
|
||||||
DevServerProjectId, RejoinedProject, ResharedProject, ServerId, UserId,
|
|
||||||
};
|
|
||||||
|
|
||||||
impl Database {
|
|
||||||
pub async fn get_dev_server_project(
|
|
||||||
&self,
|
|
||||||
dev_server_project_id: DevServerProjectId,
|
|
||||||
) -> crate::Result<dev_server_project::Model> {
|
|
||||||
self.transaction(|tx| async move {
|
|
||||||
Ok(
|
|
||||||
dev_server_project::Entity::find_by_id(dev_server_project_id)
|
|
||||||
.one(&*tx)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| {
|
|
||||||
anyhow!("no dev server project with id {}", dev_server_project_id)
|
|
||||||
})?,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_projects_for_dev_server(
|
|
||||||
&self,
|
|
||||||
dev_server_id: DevServerId,
|
|
||||||
) -> crate::Result<Vec<proto::DevServerProject>> {
|
|
||||||
self.transaction(|tx| async move {
|
|
||||||
self.get_projects_for_dev_server_internal(dev_server_id, &tx)
|
|
||||||
.await
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_projects_for_dev_server_internal(
|
|
||||||
&self,
|
|
||||||
dev_server_id: DevServerId,
|
|
||||||
tx: &DatabaseTransaction,
|
|
||||||
) -> crate::Result<Vec<proto::DevServerProject>> {
|
|
||||||
let servers = dev_server_project::Entity::find()
|
|
||||||
.filter(dev_server_project::Column::DevServerId.eq(dev_server_id))
|
|
||||||
.find_also_related(project::Entity)
|
|
||||||
.all(tx)
|
|
||||||
.await?;
|
|
||||||
Ok(servers
|
|
||||||
.into_iter()
|
|
||||||
.map(|(dev_server_project, project)| dev_server_project.to_proto(project))
|
|
||||||
.collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn dev_server_project_ids_for_user(
|
|
||||||
&self,
|
|
||||||
user_id: UserId,
|
|
||||||
tx: &DatabaseTransaction,
|
|
||||||
) -> crate::Result<Vec<DevServerProjectId>> {
|
|
||||||
let dev_servers = dev_server::Entity::find()
|
|
||||||
.filter(dev_server::Column::UserId.eq(user_id))
|
|
||||||
.find_with_related(dev_server_project::Entity)
|
|
||||||
.all(tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(dev_servers
|
|
||||||
.into_iter()
|
|
||||||
.flat_map(|(_, projects)| projects.into_iter().map(|p| p.id))
|
|
||||||
.collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn owner_for_dev_server_project(
|
|
||||||
&self,
|
|
||||||
dev_server_project_id: DevServerProjectId,
|
|
||||||
tx: &DatabaseTransaction,
|
|
||||||
) -> crate::Result<UserId> {
|
|
||||||
let dev_server = dev_server_project::Entity::find_by_id(dev_server_project_id)
|
|
||||||
.find_also_related(dev_server::Entity)
|
|
||||||
.one(tx)
|
|
||||||
.await?
|
|
||||||
.and_then(|(_, dev_server)| dev_server)
|
|
||||||
.ok_or_else(|| anyhow!("no dev server project"))?;
|
|
||||||
|
|
||||||
Ok(dev_server.user_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_stale_dev_server_projects(
|
|
||||||
&self,
|
|
||||||
connection: ConnectionId,
|
|
||||||
) -> crate::Result<Vec<ProjectId>> {
|
|
||||||
self.transaction(|tx| async move {
|
|
||||||
let projects = project::Entity::find()
|
|
||||||
.filter(
|
|
||||||
Condition::all()
|
|
||||||
.add(project::Column::HostConnectionId.eq(connection.id))
|
|
||||||
.add(project::Column::HostConnectionServerId.eq(connection.owner_id)),
|
|
||||||
)
|
|
||||||
.all(&*tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(projects.into_iter().map(|p| p.id).collect())
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn create_dev_server_project(
|
|
||||||
&self,
|
|
||||||
dev_server_id: DevServerId,
|
|
||||||
path: &str,
|
|
||||||
user_id: UserId,
|
|
||||||
) -> crate::Result<(dev_server_project::Model, proto::DevServerProjectsUpdate)> {
|
|
||||||
self.transaction(|tx| async move {
|
|
||||||
let dev_server = dev_server::Entity::find_by_id(dev_server_id)
|
|
||||||
.one(&*tx)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| anyhow!("no dev server with id {}", dev_server_id))?;
|
|
||||||
if dev_server.user_id != user_id {
|
|
||||||
return Err(anyhow!("not your dev server"))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let project = dev_server_project::Entity::insert(dev_server_project::ActiveModel {
|
|
||||||
id: ActiveValue::NotSet,
|
|
||||||
dev_server_id: ActiveValue::Set(dev_server_id),
|
|
||||||
paths: ActiveValue::Set(dev_server_project::JSONPaths(vec![path.to_string()])),
|
|
||||||
})
|
|
||||||
.exec_with_returning(&*tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let status = self
|
|
||||||
.dev_server_projects_update_internal(user_id, &tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok((project, status))
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_dev_server_project(
|
|
||||||
&self,
|
|
||||||
id: DevServerProjectId,
|
|
||||||
paths: &[String],
|
|
||||||
user_id: UserId,
|
|
||||||
) -> crate::Result<(dev_server_project::Model, proto::DevServerProjectsUpdate)> {
|
|
||||||
self.transaction(move |tx| async move {
|
|
||||||
let paths = paths.to_owned();
|
|
||||||
let Some((project, Some(dev_server))) = dev_server_project::Entity::find_by_id(id)
|
|
||||||
.find_also_related(dev_server::Entity)
|
|
||||||
.one(&*tx)
|
|
||||||
.await?
|
|
||||||
else {
|
|
||||||
return Err(anyhow!("no such dev server project"))?;
|
|
||||||
};
|
|
||||||
|
|
||||||
if dev_server.user_id != user_id {
|
|
||||||
return Err(anyhow!("not your dev server"))?;
|
|
||||||
}
|
|
||||||
let mut project = project.into_active_model();
|
|
||||||
project.paths = ActiveValue::Set(dev_server_project::JSONPaths(paths));
|
|
||||||
let project = project.update(&*tx).await?;
|
|
||||||
|
|
||||||
let status = self
|
|
||||||
.dev_server_projects_update_internal(user_id, &tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok((project, status))
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn delete_dev_server_project(
|
|
||||||
&self,
|
|
||||||
dev_server_project_id: DevServerProjectId,
|
|
||||||
dev_server_id: DevServerId,
|
|
||||||
user_id: UserId,
|
|
||||||
) -> crate::Result<(Vec<proto::DevServerProject>, proto::DevServerProjectsUpdate)> {
|
|
||||||
self.transaction(|tx| async move {
|
|
||||||
project::Entity::delete_many()
|
|
||||||
.filter(project::Column::DevServerProjectId.eq(dev_server_project_id))
|
|
||||||
.exec(&*tx)
|
|
||||||
.await?;
|
|
||||||
let result = dev_server_project::Entity::delete_by_id(dev_server_project_id)
|
|
||||||
.exec(&*tx)
|
|
||||||
.await?;
|
|
||||||
if result.rows_affected != 1 {
|
|
||||||
return Err(anyhow!(
|
|
||||||
"no dev server project with id {}",
|
|
||||||
dev_server_project_id
|
|
||||||
))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let status = self
|
|
||||||
.dev_server_projects_update_internal(user_id, &tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let projects = self
|
|
||||||
.get_projects_for_dev_server_internal(dev_server_id, &tx)
|
|
||||||
.await?;
|
|
||||||
Ok((projects, status))
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn share_dev_server_project(
|
|
||||||
&self,
|
|
||||||
dev_server_project_id: DevServerProjectId,
|
|
||||||
dev_server_id: DevServerId,
|
|
||||||
connection: ConnectionId,
|
|
||||||
worktrees: &[proto::WorktreeMetadata],
|
|
||||||
) -> crate::Result<(
|
|
||||||
proto::DevServerProject,
|
|
||||||
UserId,
|
|
||||||
proto::DevServerProjectsUpdate,
|
|
||||||
)> {
|
|
||||||
self.transaction(|tx| async move {
|
|
||||||
let dev_server = dev_server::Entity::find_by_id(dev_server_id)
|
|
||||||
.one(&*tx)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| anyhow!("no dev server with id {}", dev_server_id))?;
|
|
||||||
|
|
||||||
let dev_server_project = dev_server_project::Entity::find_by_id(dev_server_project_id)
|
|
||||||
.one(&*tx)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| {
|
|
||||||
anyhow!("no dev server project with id {}", dev_server_project_id)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if dev_server_project.dev_server_id != dev_server_id {
|
|
||||||
return Err(anyhow!("dev server project shared from wrong server"))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let project = project::ActiveModel {
|
|
||||||
room_id: ActiveValue::Set(None),
|
|
||||||
host_user_id: ActiveValue::Set(None),
|
|
||||||
host_connection_id: ActiveValue::set(Some(connection.id as i32)),
|
|
||||||
host_connection_server_id: ActiveValue::set(Some(ServerId(
|
|
||||||
connection.owner_id as i32,
|
|
||||||
))),
|
|
||||||
id: ActiveValue::NotSet,
|
|
||||||
hosted_project_id: ActiveValue::Set(None),
|
|
||||||
dev_server_project_id: ActiveValue::Set(Some(dev_server_project_id)),
|
|
||||||
}
|
|
||||||
.insert(&*tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if !worktrees.is_empty() {
|
|
||||||
worktree::Entity::insert_many(worktrees.iter().map(|worktree| {
|
|
||||||
worktree::ActiveModel {
|
|
||||||
id: ActiveValue::set(worktree.id as i64),
|
|
||||||
project_id: ActiveValue::set(project.id),
|
|
||||||
abs_path: ActiveValue::set(worktree.abs_path.clone()),
|
|
||||||
root_name: ActiveValue::set(worktree.root_name.clone()),
|
|
||||||
visible: ActiveValue::set(worktree.visible),
|
|
||||||
scan_id: ActiveValue::set(0),
|
|
||||||
completed_scan_id: ActiveValue::set(0),
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
.exec(&*tx)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let status = self
|
|
||||||
.dev_server_projects_update_internal(dev_server.user_id, &tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok((
|
|
||||||
dev_server_project.to_proto(Some(project)),
|
|
||||||
dev_server.user_id,
|
|
||||||
status,
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn reshare_dev_server_projects(
|
|
||||||
&self,
|
|
||||||
reshared_projects: &Vec<proto::UpdateProject>,
|
|
||||||
dev_server_id: DevServerId,
|
|
||||||
connection: ConnectionId,
|
|
||||||
) -> crate::Result<Vec<ResharedProject>> {
|
|
||||||
self.transaction(|tx| async move {
|
|
||||||
let mut ret = Vec::new();
|
|
||||||
for reshared_project in reshared_projects {
|
|
||||||
let project_id = ProjectId::from_proto(reshared_project.project_id);
|
|
||||||
let (project, dev_server_project) = project::Entity::find_by_id(project_id)
|
|
||||||
.find_also_related(dev_server_project::Entity)
|
|
||||||
.one(&*tx)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| anyhow!("project does not exist"))?;
|
|
||||||
|
|
||||||
if dev_server_project.map(|rp| rp.dev_server_id) != Some(dev_server_id) {
|
|
||||||
return Err(anyhow!("dev server project reshared from wrong server"))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Ok(old_connection_id) = project.host_connection() else {
|
|
||||||
return Err(anyhow!("dev server project was not shared"))?;
|
|
||||||
};
|
|
||||||
|
|
||||||
project::Entity::update(project::ActiveModel {
|
|
||||||
id: ActiveValue::set(project_id),
|
|
||||||
host_connection_id: ActiveValue::set(Some(connection.id as i32)),
|
|
||||||
host_connection_server_id: ActiveValue::set(Some(ServerId(
|
|
||||||
connection.owner_id as i32,
|
|
||||||
))),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.exec(&*tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let collaborators = project
|
|
||||||
.find_related(project_collaborator::Entity)
|
|
||||||
.all(&*tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
self.update_project_worktrees(project_id, &reshared_project.worktrees, &tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
ret.push(super::ResharedProject {
|
|
||||||
id: project_id,
|
|
||||||
old_connection_id,
|
|
||||||
collaborators: collaborators
|
|
||||||
.iter()
|
|
||||||
.map(|collaborator| super::ProjectCollaborator {
|
|
||||||
connection_id: collaborator.connection(),
|
|
||||||
user_id: collaborator.user_id,
|
|
||||||
replica_id: collaborator.replica_id,
|
|
||||||
is_host: collaborator.is_host,
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
worktrees: reshared_project.worktrees.clone(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(ret)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn rejoin_dev_server_projects(
|
|
||||||
&self,
|
|
||||||
rejoined_projects: &Vec<proto::RejoinProject>,
|
|
||||||
user_id: UserId,
|
|
||||||
connection_id: ConnectionId,
|
|
||||||
) -> crate::Result<Vec<RejoinedProject>> {
|
|
||||||
self.transaction(|tx| async move {
|
|
||||||
let mut ret = Vec::new();
|
|
||||||
for rejoined_project in rejoined_projects {
|
|
||||||
if let Some(project) = self
|
|
||||||
.rejoin_project_internal(&tx, rejoined_project, user_id, connection_id)
|
|
||||||
.await?
|
|
||||||
{
|
|
||||||
ret.push(project);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(ret)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,222 +1 @@
|
|||||||
use rpc::proto;
|
|
||||||
use sea_orm::{
|
|
||||||
ActiveValue, ColumnTrait, DatabaseTransaction, EntityTrait, IntoActiveModel, QueryFilter,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{dev_server, dev_server_project, Database, DevServerId, UserId};
|
|
||||||
|
|
||||||
impl Database {
|
|
||||||
pub async fn get_dev_server(
|
|
||||||
&self,
|
|
||||||
dev_server_id: DevServerId,
|
|
||||||
) -> crate::Result<dev_server::Model> {
|
|
||||||
self.transaction(|tx| async move {
|
|
||||||
Ok(dev_server::Entity::find_by_id(dev_server_id)
|
|
||||||
.one(&*tx)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("no dev server with id {}", dev_server_id))?)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_dev_server_for_user(
|
|
||||||
&self,
|
|
||||||
dev_server_id: DevServerId,
|
|
||||||
user_id: UserId,
|
|
||||||
) -> crate::Result<dev_server::Model> {
|
|
||||||
self.transaction(|tx| async move {
|
|
||||||
let server = dev_server::Entity::find_by_id(dev_server_id)
|
|
||||||
.one(&*tx)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("no dev server with id {}", dev_server_id))?;
|
|
||||||
if server.user_id != user_id {
|
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"dev server {} is not owned by user {}",
|
|
||||||
dev_server_id,
|
|
||||||
user_id
|
|
||||||
))?;
|
|
||||||
}
|
|
||||||
Ok(server)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_dev_servers(&self, user_id: UserId) -> crate::Result<Vec<dev_server::Model>> {
|
|
||||||
self.transaction(|tx| async move {
|
|
||||||
Ok(dev_server::Entity::find()
|
|
||||||
.filter(dev_server::Column::UserId.eq(user_id))
|
|
||||||
.all(&*tx)
|
|
||||||
.await?)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn dev_server_projects_update(
|
|
||||||
&self,
|
|
||||||
user_id: UserId,
|
|
||||||
) -> crate::Result<proto::DevServerProjectsUpdate> {
|
|
||||||
self.transaction(|tx| async move {
|
|
||||||
self.dev_server_projects_update_internal(user_id, &tx).await
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn dev_server_projects_update_internal(
|
|
||||||
&self,
|
|
||||||
user_id: UserId,
|
|
||||||
tx: &DatabaseTransaction,
|
|
||||||
) -> crate::Result<proto::DevServerProjectsUpdate> {
|
|
||||||
let dev_servers = dev_server::Entity::find()
|
|
||||||
.filter(dev_server::Column::UserId.eq(user_id))
|
|
||||||
.all(tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let dev_server_projects = dev_server_project::Entity::find()
|
|
||||||
.filter(
|
|
||||||
dev_server_project::Column::DevServerId
|
|
||||||
.is_in(dev_servers.iter().map(|d| d.id).collect::<Vec<_>>()),
|
|
||||||
)
|
|
||||||
.find_also_related(super::project::Entity)
|
|
||||||
.all(tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(proto::DevServerProjectsUpdate {
|
|
||||||
dev_servers: dev_servers
|
|
||||||
.into_iter()
|
|
||||||
.map(|d| d.to_proto(proto::DevServerStatus::Offline))
|
|
||||||
.collect(),
|
|
||||||
dev_server_projects: dev_server_projects
|
|
||||||
.into_iter()
|
|
||||||
.map(|(dev_server_project, project)| dev_server_project.to_proto(project))
|
|
||||||
.collect(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn create_dev_server(
|
|
||||||
&self,
|
|
||||||
name: &str,
|
|
||||||
ssh_connection_string: Option<&str>,
|
|
||||||
hashed_access_token: &str,
|
|
||||||
user_id: UserId,
|
|
||||||
) -> crate::Result<(dev_server::Model, proto::DevServerProjectsUpdate)> {
|
|
||||||
self.transaction(|tx| async move {
|
|
||||||
if name.trim().is_empty() {
|
|
||||||
return Err(anyhow::anyhow!(proto::ErrorCode::Forbidden))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let dev_server = dev_server::Entity::insert(dev_server::ActiveModel {
|
|
||||||
id: ActiveValue::NotSet,
|
|
||||||
hashed_token: ActiveValue::Set(hashed_access_token.to_string()),
|
|
||||||
name: ActiveValue::Set(name.trim().to_string()),
|
|
||||||
user_id: ActiveValue::Set(user_id),
|
|
||||||
ssh_connection_string: ActiveValue::Set(
|
|
||||||
ssh_connection_string.map(ToOwned::to_owned),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
.exec_with_returning(&*tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let dev_server_projects = self
|
|
||||||
.dev_server_projects_update_internal(user_id, &tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok((dev_server, dev_server_projects))
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_dev_server_token(
|
|
||||||
&self,
|
|
||||||
id: DevServerId,
|
|
||||||
hashed_token: &str,
|
|
||||||
user_id: UserId,
|
|
||||||
) -> crate::Result<proto::DevServerProjectsUpdate> {
|
|
||||||
self.transaction(|tx| async move {
|
|
||||||
let Some(dev_server) = dev_server::Entity::find_by_id(id).one(&*tx).await? else {
|
|
||||||
return Err(anyhow::anyhow!("no dev server with id {}", id))?;
|
|
||||||
};
|
|
||||||
if dev_server.user_id != user_id {
|
|
||||||
return Err(anyhow::anyhow!(proto::ErrorCode::Forbidden))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
dev_server::Entity::update(dev_server::ActiveModel {
|
|
||||||
hashed_token: ActiveValue::Set(hashed_token.to_string()),
|
|
||||||
..dev_server.clone().into_active_model()
|
|
||||||
})
|
|
||||||
.exec(&*tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let dev_server_projects = self
|
|
||||||
.dev_server_projects_update_internal(user_id, &tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(dev_server_projects)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn rename_dev_server(
|
|
||||||
&self,
|
|
||||||
id: DevServerId,
|
|
||||||
name: &str,
|
|
||||||
ssh_connection_string: Option<&str>,
|
|
||||||
user_id: UserId,
|
|
||||||
) -> crate::Result<proto::DevServerProjectsUpdate> {
|
|
||||||
self.transaction(|tx| async move {
|
|
||||||
let Some(dev_server) = dev_server::Entity::find_by_id(id).one(&*tx).await? else {
|
|
||||||
return Err(anyhow::anyhow!("no dev server with id {}", id))?;
|
|
||||||
};
|
|
||||||
if dev_server.user_id != user_id || name.trim().is_empty() {
|
|
||||||
return Err(anyhow::anyhow!(proto::ErrorCode::Forbidden))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
dev_server::Entity::update(dev_server::ActiveModel {
|
|
||||||
name: ActiveValue::Set(name.trim().to_string()),
|
|
||||||
ssh_connection_string: ActiveValue::Set(
|
|
||||||
ssh_connection_string.map(ToOwned::to_owned),
|
|
||||||
),
|
|
||||||
..dev_server.clone().into_active_model()
|
|
||||||
})
|
|
||||||
.exec(&*tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let dev_server_projects = self
|
|
||||||
.dev_server_projects_update_internal(user_id, &tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(dev_server_projects)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn delete_dev_server(
|
|
||||||
&self,
|
|
||||||
id: DevServerId,
|
|
||||||
user_id: UserId,
|
|
||||||
) -> crate::Result<proto::DevServerProjectsUpdate> {
|
|
||||||
self.transaction(|tx| async move {
|
|
||||||
let Some(dev_server) = dev_server::Entity::find_by_id(id).one(&*tx).await? else {
|
|
||||||
return Err(anyhow::anyhow!("no dev server with id {}", id))?;
|
|
||||||
};
|
|
||||||
if dev_server.user_id != user_id {
|
|
||||||
return Err(anyhow::anyhow!(proto::ErrorCode::Forbidden))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
dev_server_project::Entity::delete_many()
|
|
||||||
.filter(dev_server_project::Column::DevServerId.eq(id))
|
|
||||||
.exec(&*tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
dev_server::Entity::delete(dev_server.into_active_model())
|
|
||||||
.exec(&*tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let dev_server_projects = self
|
|
||||||
.dev_server_projects_update_internal(user_id, &tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(dev_server_projects)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
use rpc::{proto, ErrorCode};
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
impl Database {
|
|
||||||
pub async fn get_hosted_projects(
|
|
||||||
&self,
|
|
||||||
channel_ids: &[ChannelId],
|
|
||||||
roles: &HashMap<ChannelId, ChannelRole>,
|
|
||||||
tx: &DatabaseTransaction,
|
|
||||||
) -> Result<Vec<proto::HostedProject>> {
|
|
||||||
let projects = hosted_project::Entity::find()
|
|
||||||
.find_also_related(project::Entity)
|
|
||||||
.filter(hosted_project::Column::ChannelId.is_in(channel_ids.iter().map(|id| id.0)))
|
|
||||||
.all(tx)
|
|
||||||
.await?
|
|
||||||
.into_iter()
|
|
||||||
.flat_map(|(hosted_project, project)| {
|
|
||||||
if hosted_project.deleted_at.is_some() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
match hosted_project.visibility {
|
|
||||||
ChannelVisibility::Public => {}
|
|
||||||
ChannelVisibility::Members => {
|
|
||||||
let is_visible = roles
|
|
||||||
.get(&hosted_project.channel_id)
|
|
||||||
.map(|role| role.can_see_all_descendants())
|
|
||||||
.unwrap_or(false);
|
|
||||||
if !is_visible {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Some(proto::HostedProject {
|
|
||||||
project_id: project?.id.to_proto(),
|
|
||||||
channel_id: hosted_project.channel_id.to_proto(),
|
|
||||||
name: hosted_project.name.clone(),
|
|
||||||
visibility: hosted_project.visibility.into(),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok(projects)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_hosted_project(
|
|
||||||
&self,
|
|
||||||
hosted_project_id: HostedProjectId,
|
|
||||||
user_id: UserId,
|
|
||||||
tx: &DatabaseTransaction,
|
|
||||||
) -> Result<(hosted_project::Model, ChannelRole)> {
|
|
||||||
let project = hosted_project::Entity::find_by_id(hosted_project_id)
|
|
||||||
.one(tx)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| anyhow!(ErrorCode::NoSuchProject))?;
|
|
||||||
let channel = channel::Entity::find_by_id(project.channel_id)
|
|
||||||
.one(tx)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| anyhow!(ErrorCode::NoSuchChannel))?;
|
|
||||||
|
|
||||||
let role = match project.visibility {
|
|
||||||
ChannelVisibility::Public => {
|
|
||||||
self.check_user_is_channel_participant(&channel, user_id, tx)
|
|
||||||
.await?
|
|
||||||
}
|
|
||||||
ChannelVisibility::Members => {
|
|
||||||
self.check_user_is_channel_member(&channel, user_id, tx)
|
|
||||||
.await?
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok((project, role))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn is_hosted_project(&self, project_id: ProjectId) -> Result<bool> {
|
|
||||||
self.transaction(|tx| async move {
|
|
||||||
Ok(project::Entity::find_by_id(project_id)
|
|
||||||
.one(&*tx)
|
|
||||||
.await?
|
|
||||||
.map(|project| project.hosted_project_id.is_some())
|
|
||||||
.ok_or_else(|| anyhow!(ErrorCode::NoSuchProject))?)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -32,7 +32,6 @@ impl Database {
|
|||||||
connection: ConnectionId,
|
connection: ConnectionId,
|
||||||
worktrees: &[proto::WorktreeMetadata],
|
worktrees: &[proto::WorktreeMetadata],
|
||||||
is_ssh_project: bool,
|
is_ssh_project: bool,
|
||||||
dev_server_project_id: Option<DevServerProjectId>,
|
|
||||||
) -> Result<TransactionGuard<(ProjectId, proto::Room)>> {
|
) -> Result<TransactionGuard<(ProjectId, proto::Room)>> {
|
||||||
self.room_transaction(room_id, |tx| async move {
|
self.room_transaction(room_id, |tx| async move {
|
||||||
let participant = room_participant::Entity::find()
|
let participant = room_participant::Entity::find()
|
||||||
@@ -61,38 +60,6 @@ impl Database {
|
|||||||
return Err(anyhow!("guests cannot share projects"))?;
|
return Err(anyhow!("guests cannot share projects"))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(dev_server_project_id) = dev_server_project_id {
|
|
||||||
let project = project::Entity::find()
|
|
||||||
.filter(project::Column::DevServerProjectId.eq(Some(dev_server_project_id)))
|
|
||||||
.one(&*tx)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| anyhow!("no remote project"))?;
|
|
||||||
|
|
||||||
let (_, dev_server) = dev_server_project::Entity::find_by_id(dev_server_project_id)
|
|
||||||
.find_also_related(dev_server::Entity)
|
|
||||||
.one(&*tx)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| anyhow!("no dev_server_project"))?;
|
|
||||||
|
|
||||||
if !dev_server.is_some_and(|dev_server| dev_server.user_id == participant.user_id) {
|
|
||||||
return Err(anyhow!("not your dev server"))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if project.room_id.is_some() {
|
|
||||||
return Err(anyhow!("project already shared"))?;
|
|
||||||
};
|
|
||||||
|
|
||||||
let project = project::Entity::update(project::ActiveModel {
|
|
||||||
room_id: ActiveValue::Set(Some(room_id)),
|
|
||||||
..project.into_active_model()
|
|
||||||
})
|
|
||||||
.exec(&*tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let room = self.get_room(room_id, &tx).await?;
|
|
||||||
return Ok((project.id, room));
|
|
||||||
}
|
|
||||||
|
|
||||||
let project = project::ActiveModel {
|
let project = project::ActiveModel {
|
||||||
room_id: ActiveValue::set(Some(participant.room_id)),
|
room_id: ActiveValue::set(Some(participant.room_id)),
|
||||||
host_user_id: ActiveValue::set(Some(participant.user_id)),
|
host_user_id: ActiveValue::set(Some(participant.user_id)),
|
||||||
@@ -101,8 +68,6 @@ impl Database {
|
|||||||
connection.owner_id as i32,
|
connection.owner_id as i32,
|
||||||
))),
|
))),
|
||||||
id: ActiveValue::NotSet,
|
id: ActiveValue::NotSet,
|
||||||
hosted_project_id: ActiveValue::Set(None),
|
|
||||||
dev_server_project_id: ActiveValue::Set(None),
|
|
||||||
}
|
}
|
||||||
.insert(&*tx)
|
.insert(&*tx)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -156,7 +121,6 @@ impl Database {
|
|||||||
&self,
|
&self,
|
||||||
project_id: ProjectId,
|
project_id: ProjectId,
|
||||||
connection: ConnectionId,
|
connection: ConnectionId,
|
||||||
user_id: Option<UserId>,
|
|
||||||
) -> Result<TransactionGuard<(bool, Option<proto::Room>, Vec<ConnectionId>)>> {
|
) -> Result<TransactionGuard<(bool, Option<proto::Room>, Vec<ConnectionId>)>> {
|
||||||
self.project_transaction(project_id, |tx| async move {
|
self.project_transaction(project_id, |tx| async move {
|
||||||
let guest_connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
|
let guest_connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
|
||||||
@@ -172,25 +136,6 @@ impl Database {
|
|||||||
if project.host_connection()? == connection {
|
if project.host_connection()? == connection {
|
||||||
return Ok((true, room, guest_connection_ids));
|
return Ok((true, room, guest_connection_ids));
|
||||||
}
|
}
|
||||||
if let Some(dev_server_project_id) = project.dev_server_project_id {
|
|
||||||
if let Some(user_id) = user_id {
|
|
||||||
if user_id
|
|
||||||
!= self
|
|
||||||
.owner_for_dev_server_project(dev_server_project_id, &tx)
|
|
||||||
.await?
|
|
||||||
{
|
|
||||||
Err(anyhow!("cannot unshare a project hosted by another user"))?
|
|
||||||
}
|
|
||||||
project::Entity::update(project::ActiveModel {
|
|
||||||
room_id: ActiveValue::Set(None),
|
|
||||||
..project.into_active_model()
|
|
||||||
})
|
|
||||||
.exec(&*tx)
|
|
||||||
.await?;
|
|
||||||
return Ok((false, room, guest_connection_ids));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(anyhow!("cannot unshare a project hosted by another user"))?
|
Err(anyhow!("cannot unshare a project hosted by another user"))?
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -272,6 +217,16 @@ impl Database {
|
|||||||
update: &proto::UpdateWorktree,
|
update: &proto::UpdateWorktree,
|
||||||
connection: ConnectionId,
|
connection: ConnectionId,
|
||||||
) -> Result<TransactionGuard<Vec<ConnectionId>>> {
|
) -> Result<TransactionGuard<Vec<ConnectionId>>> {
|
||||||
|
if update.removed_entries.len() > proto::MAX_WORKTREE_UPDATE_MAX_CHUNK_SIZE
|
||||||
|
|| update.updated_entries.len() > proto::MAX_WORKTREE_UPDATE_MAX_CHUNK_SIZE
|
||||||
|
{
|
||||||
|
return Err(anyhow!(
|
||||||
|
"invalid worktree update. removed entries: {}, updated entries: {}",
|
||||||
|
update.removed_entries.len(),
|
||||||
|
update.updated_entries.len()
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
|
||||||
let project_id = ProjectId::from_proto(update.project_id);
|
let project_id = ProjectId::from_proto(update.project_id);
|
||||||
let worktree_id = update.worktree_id as i64;
|
let worktree_id = update.worktree_id as i64;
|
||||||
self.project_transaction(project_id, |tx| async move {
|
self.project_transaction(project_id, |tx| async move {
|
||||||
@@ -580,39 +535,6 @@ impl Database {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds the given connection to the specified hosted project
|
|
||||||
pub async fn join_hosted_project(
|
|
||||||
&self,
|
|
||||||
id: ProjectId,
|
|
||||||
user_id: UserId,
|
|
||||||
connection: ConnectionId,
|
|
||||||
) -> Result<(Project, ReplicaId)> {
|
|
||||||
self.transaction(|tx| async move {
|
|
||||||
let (project, hosted_project) = project::Entity::find_by_id(id)
|
|
||||||
.find_also_related(hosted_project::Entity)
|
|
||||||
.one(&*tx)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| anyhow!("hosted project is no longer shared"))?;
|
|
||||||
|
|
||||||
let Some(hosted_project) = hosted_project else {
|
|
||||||
return Err(anyhow!("project is not hosted"))?;
|
|
||||||
};
|
|
||||||
|
|
||||||
let channel = channel::Entity::find_by_id(hosted_project.channel_id)
|
|
||||||
.one(&*tx)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| anyhow!("no such channel"))?;
|
|
||||||
|
|
||||||
let role = self
|
|
||||||
.check_user_is_channel_participant(&channel, user_id, &tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
self.join_project_internal(project, user_id, connection, role, &tx)
|
|
||||||
.await
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_project(&self, id: ProjectId) -> Result<project::Model> {
|
pub async fn get_project(&self, id: ProjectId) -> Result<project::Model> {
|
||||||
self.transaction(|tx| async move {
|
self.transaction(|tx| async move {
|
||||||
Ok(project::Entity::find_by_id(id)
|
Ok(project::Entity::find_by_id(id)
|
||||||
@@ -623,17 +545,6 @@ impl Database {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn find_dev_server_project(&self, id: DevServerProjectId) -> Result<project::Model> {
|
|
||||||
self.transaction(|tx| async move {
|
|
||||||
Ok(project::Entity::find()
|
|
||||||
.filter(project::Column::DevServerProjectId.eq(id))
|
|
||||||
.one(&*tx)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| anyhow!("no such project"))?)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds the given connection to the specified project
|
/// Adds the given connection to the specified project
|
||||||
/// in the current room.
|
/// in the current room.
|
||||||
pub async fn join_project(
|
pub async fn join_project(
|
||||||
@@ -644,13 +555,7 @@ impl Database {
|
|||||||
) -> Result<TransactionGuard<(Project, ReplicaId)>> {
|
) -> Result<TransactionGuard<(Project, ReplicaId)>> {
|
||||||
self.project_transaction(project_id, |tx| async move {
|
self.project_transaction(project_id, |tx| async move {
|
||||||
let (project, role) = self
|
let (project, role) = self
|
||||||
.access_project(
|
.access_project(project_id, connection, Capability::ReadOnly, &tx)
|
||||||
project_id,
|
|
||||||
connection,
|
|
||||||
PrincipalId::UserId(user_id),
|
|
||||||
Capability::ReadOnly,
|
|
||||||
&tx,
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
self.join_project_internal(project, user_id, connection, role, &tx)
|
self.join_project_internal(project, user_id, connection, role, &tx)
|
||||||
.await
|
.await
|
||||||
@@ -841,54 +746,10 @@ impl Database {
|
|||||||
worktree_id: None,
|
worktree_id: None,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
dev_server_project_id: project.dev_server_project_id,
|
|
||||||
};
|
};
|
||||||
Ok((project, replica_id as ReplicaId))
|
Ok((project, replica_id as ReplicaId))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn leave_hosted_project(
|
|
||||||
&self,
|
|
||||||
project_id: ProjectId,
|
|
||||||
connection: ConnectionId,
|
|
||||||
) -> Result<LeftProject> {
|
|
||||||
self.transaction(|tx| async move {
|
|
||||||
let result = project_collaborator::Entity::delete_many()
|
|
||||||
.filter(
|
|
||||||
Condition::all()
|
|
||||||
.add(project_collaborator::Column::ProjectId.eq(project_id))
|
|
||||||
.add(project_collaborator::Column::ConnectionId.eq(connection.id as i32))
|
|
||||||
.add(
|
|
||||||
project_collaborator::Column::ConnectionServerId
|
|
||||||
.eq(connection.owner_id as i32),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.exec(&*tx)
|
|
||||||
.await?;
|
|
||||||
if result.rows_affected == 0 {
|
|
||||||
return Err(anyhow!("not in the project"))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let project = project::Entity::find_by_id(project_id)
|
|
||||||
.one(&*tx)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| anyhow!("no such project"))?;
|
|
||||||
let collaborators = project
|
|
||||||
.find_related(project_collaborator::Entity)
|
|
||||||
.all(&*tx)
|
|
||||||
.await?;
|
|
||||||
let connection_ids = collaborators
|
|
||||||
.into_iter()
|
|
||||||
.map(|collaborator| collaborator.connection())
|
|
||||||
.collect();
|
|
||||||
Ok(LeftProject {
|
|
||||||
id: project.id,
|
|
||||||
connection_ids,
|
|
||||||
should_unshare: false,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Removes the given connection from the specified project.
|
/// Removes the given connection from the specified project.
|
||||||
pub async fn leave_project(
|
pub async fn leave_project(
|
||||||
&self,
|
&self,
|
||||||
@@ -997,29 +858,14 @@ impl Database {
|
|||||||
&self,
|
&self,
|
||||||
project_id: ProjectId,
|
project_id: ProjectId,
|
||||||
connection_id: ConnectionId,
|
connection_id: ConnectionId,
|
||||||
principal_id: PrincipalId,
|
|
||||||
capability: Capability,
|
capability: Capability,
|
||||||
tx: &DatabaseTransaction,
|
tx: &DatabaseTransaction,
|
||||||
) -> Result<(project::Model, ChannelRole)> {
|
) -> Result<(project::Model, ChannelRole)> {
|
||||||
let (mut project, dev_server_project) = project::Entity::find_by_id(project_id)
|
let project = project::Entity::find_by_id(project_id)
|
||||||
.find_also_related(dev_server_project::Entity)
|
|
||||||
.one(tx)
|
.one(tx)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| anyhow!("no such project"))?;
|
.ok_or_else(|| anyhow!("no such project"))?;
|
||||||
|
|
||||||
let user_id = match principal_id {
|
|
||||||
PrincipalId::DevServerId(_) => {
|
|
||||||
if project
|
|
||||||
.host_connection()
|
|
||||||
.is_ok_and(|connection| connection == connection_id)
|
|
||||||
{
|
|
||||||
return Ok((project, ChannelRole::Admin));
|
|
||||||
}
|
|
||||||
return Err(anyhow!("not the project host"))?;
|
|
||||||
}
|
|
||||||
PrincipalId::UserId(user_id) => user_id,
|
|
||||||
};
|
|
||||||
|
|
||||||
let role_from_room = if let Some(room_id) = project.room_id {
|
let role_from_room = if let Some(room_id) = project.room_id {
|
||||||
room_participant::Entity::find()
|
room_participant::Entity::find()
|
||||||
.filter(room_participant::Column::RoomId.eq(room_id))
|
.filter(room_participant::Column::RoomId.eq(room_id))
|
||||||
@@ -1030,34 +876,8 @@ impl Database {
|
|||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
let role_from_dev_server = if let Some(dev_server_project) = dev_server_project {
|
|
||||||
let dev_server = dev_server::Entity::find_by_id(dev_server_project.dev_server_id)
|
|
||||||
.one(tx)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| anyhow!("no such channel"))?;
|
|
||||||
if user_id == dev_server.user_id {
|
|
||||||
// If the user left the room "uncleanly" they may rejoin the
|
|
||||||
// remote project before leave_room runs. IN that case kick
|
|
||||||
// the project out of the room pre-emptively.
|
|
||||||
if role_from_room.is_none() {
|
|
||||||
project = project::Entity::update(project::ActiveModel {
|
|
||||||
room_id: ActiveValue::Set(None),
|
|
||||||
..project.into_active_model()
|
|
||||||
})
|
|
||||||
.exec(tx)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
Some(ChannelRole::Admin)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let role = role_from_dev_server
|
let role = role_from_room.unwrap_or(ChannelRole::Banned);
|
||||||
.or(role_from_room)
|
|
||||||
.unwrap_or(ChannelRole::Banned);
|
|
||||||
|
|
||||||
match capability {
|
match capability {
|
||||||
Capability::ReadWrite => {
|
Capability::ReadWrite => {
|
||||||
@@ -1080,17 +900,10 @@ impl Database {
|
|||||||
&self,
|
&self,
|
||||||
project_id: ProjectId,
|
project_id: ProjectId,
|
||||||
connection_id: ConnectionId,
|
connection_id: ConnectionId,
|
||||||
user_id: UserId,
|
|
||||||
) -> Result<ConnectionId> {
|
) -> Result<ConnectionId> {
|
||||||
self.project_transaction(project_id, |tx| async move {
|
self.project_transaction(project_id, |tx| async move {
|
||||||
let (project, _) = self
|
let (project, _) = self
|
||||||
.access_project(
|
.access_project(project_id, connection_id, Capability::ReadOnly, &tx)
|
||||||
project_id,
|
|
||||||
connection_id,
|
|
||||||
PrincipalId::UserId(user_id),
|
|
||||||
Capability::ReadOnly,
|
|
||||||
&tx,
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
project.host_connection()
|
project.host_connection()
|
||||||
})
|
})
|
||||||
@@ -1103,17 +916,10 @@ impl Database {
|
|||||||
&self,
|
&self,
|
||||||
project_id: ProjectId,
|
project_id: ProjectId,
|
||||||
connection_id: ConnectionId,
|
connection_id: ConnectionId,
|
||||||
user_id: UserId,
|
|
||||||
) -> Result<ConnectionId> {
|
) -> Result<ConnectionId> {
|
||||||
self.project_transaction(project_id, |tx| async move {
|
self.project_transaction(project_id, |tx| async move {
|
||||||
let (project, _) = self
|
let (project, _) = self
|
||||||
.access_project(
|
.access_project(project_id, connection_id, Capability::ReadWrite, &tx)
|
||||||
project_id,
|
|
||||||
connection_id,
|
|
||||||
PrincipalId::UserId(user_id),
|
|
||||||
Capability::ReadWrite,
|
|
||||||
&tx,
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
project.host_connection()
|
project.host_connection()
|
||||||
})
|
})
|
||||||
@@ -1121,47 +927,16 @@ impl Database {
|
|||||||
.map(|guard| guard.into_inner())
|
.map(|guard| guard.into_inner())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the host connection for a request to join a shared project.
|
|
||||||
pub async fn host_for_owner_project_request(
|
|
||||||
&self,
|
|
||||||
project_id: ProjectId,
|
|
||||||
_connection_id: ConnectionId,
|
|
||||||
user_id: UserId,
|
|
||||||
) -> Result<ConnectionId> {
|
|
||||||
self.project_transaction(project_id, |tx| async move {
|
|
||||||
let (project, dev_server_project) = project::Entity::find_by_id(project_id)
|
|
||||||
.find_also_related(dev_server_project::Entity)
|
|
||||||
.one(&*tx)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| anyhow!("no such project"))?;
|
|
||||||
|
|
||||||
let Some(dev_server_project) = dev_server_project else {
|
|
||||||
return Err(anyhow!("not a dev server project"))?;
|
|
||||||
};
|
|
||||||
let dev_server = dev_server::Entity::find_by_id(dev_server_project.dev_server_id)
|
|
||||||
.one(&*tx)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| anyhow!("no such dev server"))?;
|
|
||||||
if dev_server.user_id != user_id {
|
|
||||||
return Err(anyhow!("not your project"))?;
|
|
||||||
}
|
|
||||||
project.host_connection()
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.map(|guard| guard.into_inner())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn connections_for_buffer_update(
|
pub async fn connections_for_buffer_update(
|
||||||
&self,
|
&self,
|
||||||
project_id: ProjectId,
|
project_id: ProjectId,
|
||||||
principal_id: PrincipalId,
|
|
||||||
connection_id: ConnectionId,
|
connection_id: ConnectionId,
|
||||||
capability: Capability,
|
capability: Capability,
|
||||||
) -> Result<TransactionGuard<(ConnectionId, Vec<ConnectionId>)>> {
|
) -> Result<TransactionGuard<(ConnectionId, Vec<ConnectionId>)>> {
|
||||||
self.project_transaction(project_id, |tx| async move {
|
self.project_transaction(project_id, |tx| async move {
|
||||||
// Authorize
|
// Authorize
|
||||||
let (project, _) = self
|
let (project, _) = self
|
||||||
.access_project(project_id, connection_id, principal_id, capability, &tx)
|
.access_project(project_id, connection_id, capability, &tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let host_connection_id = project.host_connection()?;
|
let host_connection_id = project.host_connection()?;
|
||||||
|
|||||||
@@ -858,25 +858,6 @@ impl Database {
|
|||||||
.all(&*tx)
|
.all(&*tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// if any project in the room has a remote-project-id that belongs to a dev server that this user owns.
|
|
||||||
let dev_server_projects_for_user = self
|
|
||||||
.dev_server_project_ids_for_user(leaving_participant.user_id, &tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let dev_server_projects_to_unshare = project::Entity::find()
|
|
||||||
.filter(
|
|
||||||
Condition::all()
|
|
||||||
.add(project::Column::RoomId.eq(room_id))
|
|
||||||
.add(
|
|
||||||
project::Column::DevServerProjectId
|
|
||||||
.is_in(dev_server_projects_for_user.clone()),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.all(&*tx)
|
|
||||||
.await?
|
|
||||||
.into_iter()
|
|
||||||
.map(|project| project.id)
|
|
||||||
.collect::<HashSet<_>>();
|
|
||||||
let mut left_projects = HashMap::default();
|
let mut left_projects = HashMap::default();
|
||||||
let mut collaborators = project_collaborator::Entity::find()
|
let mut collaborators = project_collaborator::Entity::find()
|
||||||
.filter(project_collaborator::Column::ProjectId.is_in(project_ids))
|
.filter(project_collaborator::Column::ProjectId.is_in(project_ids))
|
||||||
@@ -899,9 +880,7 @@ impl Database {
|
|||||||
left_project.connection_ids.push(collaborator_connection_id);
|
left_project.connection_ids.push(collaborator_connection_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (collaborator.is_host && collaborator.connection() == connection)
|
if collaborator.is_host && collaborator.connection() == connection {
|
||||||
|| dev_server_projects_to_unshare.contains(&collaborator.project_id)
|
|
||||||
{
|
|
||||||
left_project.should_unshare = true;
|
left_project.should_unshare = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -944,17 +923,6 @@ impl Database {
|
|||||||
.exec(&*tx)
|
.exec(&*tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if !dev_server_projects_to_unshare.is_empty() {
|
|
||||||
project::Entity::update_many()
|
|
||||||
.filter(project::Column::Id.is_in(dev_server_projects_to_unshare))
|
|
||||||
.set(project::ActiveModel {
|
|
||||||
room_id: ActiveValue::Set(None),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.exec(&*tx)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let (channel, room) = self.get_channel_room(room_id, &tx).await?;
|
let (channel, room) = self.get_channel_room(room_id, &tx).await?;
|
||||||
let deleted = if room.participants.is_empty() {
|
let deleted = if room.participants.is_empty() {
|
||||||
let result = room::Entity::delete_by_id(room_id).exec(&*tx).await?;
|
let result = room::Entity::delete_by_id(room_id).exec(&*tx).await?;
|
||||||
@@ -1323,26 +1291,6 @@ impl Database {
|
|||||||
project.worktree_root_names.push(db_worktree.root_name);
|
project.worktree_root_names.push(db_worktree.root_name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if let Some(dev_server_project_id) = db_project.dev_server_project_id {
|
|
||||||
let host = self
|
|
||||||
.owner_for_dev_server_project(dev_server_project_id, tx)
|
|
||||||
.await?;
|
|
||||||
if let Some((_, participant)) = participants
|
|
||||||
.iter_mut()
|
|
||||||
.find(|(_, v)| v.user_id == host.to_proto())
|
|
||||||
{
|
|
||||||
participant.projects.push(proto::ParticipantProject {
|
|
||||||
id: db_project.id.to_proto(),
|
|
||||||
worktree_root_names: Default::default(),
|
|
||||||
});
|
|
||||||
let project = participant.projects.last_mut().unwrap();
|
|
||||||
|
|
||||||
for db_worktree in db_worktrees {
|
|
||||||
if db_worktree.visible {
|
|
||||||
project.worktree_root_names.push(db_worktree.root_name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,14 +13,11 @@ pub mod channel_message;
|
|||||||
pub mod channel_message_mention;
|
pub mod channel_message_mention;
|
||||||
pub mod contact;
|
pub mod contact;
|
||||||
pub mod contributor;
|
pub mod contributor;
|
||||||
pub mod dev_server;
|
|
||||||
pub mod dev_server_project;
|
|
||||||
pub mod embedding;
|
pub mod embedding;
|
||||||
pub mod extension;
|
pub mod extension;
|
||||||
pub mod extension_version;
|
pub mod extension_version;
|
||||||
pub mod feature_flag;
|
pub mod feature_flag;
|
||||||
pub mod follower;
|
pub mod follower;
|
||||||
pub mod hosted_project;
|
|
||||||
pub mod language_server;
|
pub mod language_server;
|
||||||
pub mod notification;
|
pub mod notification;
|
||||||
pub mod notification_kind;
|
pub mod notification_kind;
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
use crate::db::{DevServerId, UserId};
|
|
||||||
use rpc::proto;
|
|
||||||
use sea_orm::entity::prelude::*;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
|
||||||
#[sea_orm(table_name = "dev_servers")]
|
|
||||||
pub struct Model {
|
|
||||||
#[sea_orm(primary_key)]
|
|
||||||
pub id: DevServerId,
|
|
||||||
pub name: String,
|
|
||||||
pub user_id: UserId,
|
|
||||||
pub hashed_token: String,
|
|
||||||
pub ssh_connection_string: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
|
||||||
pub enum Relation {
|
|
||||||
#[sea_orm(has_many = "super::dev_server_project::Entity")]
|
|
||||||
RemoteProject,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Related<super::dev_server_project::Entity> for Entity {
|
|
||||||
fn to() -> RelationDef {
|
|
||||||
Relation::RemoteProject.def()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Model {
|
|
||||||
pub fn to_proto(&self, status: proto::DevServerStatus) -> proto::DevServer {
|
|
||||||
proto::DevServer {
|
|
||||||
dev_server_id: self.id.to_proto(),
|
|
||||||
name: self.name.clone(),
|
|
||||||
status: status as i32,
|
|
||||||
ssh_connection_string: self.ssh_connection_string.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
use super::project;
|
|
||||||
use crate::db::{DevServerId, DevServerProjectId};
|
|
||||||
use rpc::proto;
|
|
||||||
use sea_orm::{entity::prelude::*, FromJsonQueryResult};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
|
||||||
#[sea_orm(table_name = "dev_server_projects")]
|
|
||||||
pub struct Model {
|
|
||||||
#[sea_orm(primary_key)]
|
|
||||||
pub id: DevServerProjectId,
|
|
||||||
pub dev_server_id: DevServerId,
|
|
||||||
pub paths: JSONPaths,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, FromJsonQueryResult)]
|
|
||||||
pub struct JSONPaths(pub Vec<String>);
|
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
|
||||||
pub enum Relation {
|
|
||||||
#[sea_orm(has_one = "super::project::Entity")]
|
|
||||||
Project,
|
|
||||||
#[sea_orm(
|
|
||||||
belongs_to = "super::dev_server::Entity",
|
|
||||||
from = "Column::DevServerId",
|
|
||||||
to = "super::dev_server::Column::Id"
|
|
||||||
)]
|
|
||||||
DevServer,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Related<super::project::Entity> for Entity {
|
|
||||||
fn to() -> RelationDef {
|
|
||||||
Relation::Project.def()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Related<super::dev_server::Entity> for Entity {
|
|
||||||
fn to() -> RelationDef {
|
|
||||||
Relation::DevServer.def()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Model {
|
|
||||||
pub fn to_proto(&self, project: Option<project::Model>) -> proto::DevServerProject {
|
|
||||||
proto::DevServerProject {
|
|
||||||
id: self.id.to_proto(),
|
|
||||||
project_id: project.map(|p| p.id.to_proto()),
|
|
||||||
dev_server_id: self.dev_server_id.to_proto(),
|
|
||||||
path: self.paths().first().cloned().unwrap_or_default(),
|
|
||||||
paths: self.paths().clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn paths(&self) -> &Vec<String> {
|
|
||||||
&self.paths.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||