Compare commits
256 Commits
room-rejoi
...
smaller-oc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8107e6d9db | ||
|
|
91a0923fc4 | ||
|
|
b4ddc83e85 | ||
|
|
3bd9d14420 | ||
|
|
95b311cb90 | ||
|
|
8eea281288 | ||
|
|
373a4e7614 | ||
|
|
f9f9f0670f | ||
|
|
20d5f5e8da | ||
|
|
f066dd268f | ||
|
|
0be20d0817 | ||
|
|
a04932c4eb | ||
|
|
ceadb39c38 | ||
|
|
2244419dfd | ||
|
|
a8fa1f7363 | ||
|
|
eb5e18c66d | ||
|
|
830e107921 | ||
|
|
45c4d35da8 | ||
|
|
298314d526 | ||
|
|
2f6c78b0c0 | ||
|
|
4700d33728 | ||
|
|
9afd78b35e | ||
|
|
9ff3cff6f8 | ||
|
|
d66f8f99bd | ||
|
|
269848775c | ||
|
|
39bd12a557 | ||
|
|
e1f8a1e8b2 | ||
|
|
41dc5fc412 | ||
|
|
f4a86e6fea | ||
|
|
597465b0f5 | ||
|
|
ccc939124f | ||
|
|
a03fecafbb | ||
|
|
ca696fd5f6 | ||
|
|
456efb53ad | ||
|
|
d4ec78f328 | ||
|
|
efe5203a09 | ||
|
|
146971fb02 | ||
|
|
347178039c | ||
|
|
6a7a3b257a | ||
|
|
8a6264d933 | ||
|
|
5abcc1c3c5 | ||
|
|
977af37cfe | ||
|
|
1756c1fc1e | ||
|
|
be953b78ef | ||
|
|
51ebe0eb01 | ||
|
|
a550b9cecf | ||
|
|
bf295eac90 | ||
|
|
fa5dfe19f8 | ||
|
|
7b73e2824b | ||
|
|
1081ba7a62 | ||
|
|
ed8aa6d200 | ||
|
|
af564242e1 | ||
|
|
aa7be4b5d8 | ||
|
|
866d791760 | ||
|
|
f67abd2943 | ||
|
|
d247086b21 | ||
|
|
467a179837 | ||
|
|
b50f86735f | ||
|
|
e85d484952 | ||
|
|
2d83580df4 | ||
|
|
a90a667fd0 | ||
|
|
35c7b5d7dd | ||
|
|
ffebe2e4a6 | ||
|
|
e85f190128 | ||
|
|
284a57d4d1 | ||
|
|
9068911eb4 | ||
|
|
27518f4280 | ||
|
|
86748a09e7 | ||
|
|
b5370cd15a | ||
|
|
85e6bc94e9 | ||
|
|
105e654dce | ||
|
|
6b8984279f | ||
|
|
bc7fb9f253 | ||
|
|
d450fde1ed | ||
|
|
4c9c9df730 | ||
|
|
3a9ec906af | ||
|
|
01fe3eec4d | ||
|
|
0a07746381 | ||
|
|
026cdc617c | ||
|
|
4238793d16 | ||
|
|
1a9387035d | ||
|
|
4f53e6e9a0 | ||
|
|
75a42c27db | ||
|
|
4d2156e2ad | ||
|
|
9481b346e2 | ||
|
|
8a92d28663 | ||
|
|
8be4b4d75d | ||
|
|
c0edb5bd6c | ||
|
|
c8e03ce42a | ||
|
|
74e7611ceb | ||
|
|
0b87be71e6 | ||
|
|
ae5ec9224c | ||
|
|
ca37d39109 | ||
|
|
675ae24964 | ||
|
|
e273198ada | ||
|
|
af87fb98d0 | ||
|
|
effc317a06 | ||
|
|
6bbd09e28e | ||
|
|
06035dadea | ||
|
|
8357039419 | ||
|
|
59faef5800 | ||
|
|
a0fac3866a | ||
|
|
8352f39ff9 | ||
|
|
850ddddcac | ||
|
|
563c4db350 | ||
|
|
8eb0239d5a | ||
|
|
deb86a1ffc | ||
|
|
b622dcbc64 | ||
|
|
2f15676b7c | ||
|
|
4a60326c1c | ||
|
|
567fee4219 | ||
|
|
6036830049 | ||
|
|
c8383e3b18 | ||
|
|
334c3c670b | ||
|
|
e3a7192c11 | ||
|
|
6327f3ced8 | ||
|
|
c58422cb3f | ||
|
|
6d53846824 | ||
|
|
01e5e4224a | ||
|
|
4de80688b9 | ||
|
|
ce6bde5a24 | ||
|
|
35c516fda9 | ||
|
|
bca98caa07 | ||
|
|
b68a277b5e | ||
|
|
703c9655a0 | ||
|
|
addfcdea8d | ||
|
|
d112bcfadf | ||
|
|
9e66d48ccd | ||
|
|
d98b61e3d6 | ||
|
|
ad0c5731e5 | ||
|
|
cfffa29f9a | ||
|
|
9a2ed4bf1a | ||
|
|
b6af393e6d | ||
|
|
a25edcc5a8 | ||
|
|
22fe03913c | ||
|
|
52f750b216 | ||
|
|
36c4831806 | ||
|
|
7c9f680b1b | ||
|
|
2b8b913b6b | ||
|
|
d286c56ebb | ||
|
|
0b34b1de7b | ||
|
|
537d92533c | ||
|
|
fae5e83d93 | ||
|
|
6a268e959f | ||
|
|
4b2e774594 | ||
|
|
e0c66b30c8 | ||
|
|
27c5343707 | ||
|
|
4167c66b86 | ||
|
|
d223fe446d | ||
|
|
b742db65fe | ||
|
|
f53823c840 | ||
|
|
2201b9b116 | ||
|
|
b2f18cfe71 | ||
|
|
95e532c56d | ||
|
|
d7b5c883fe | ||
|
|
78fa596839 | ||
|
|
1dd4c1b057 | ||
|
|
9ea50ed649 | ||
|
|
9c6a0d98ed | ||
|
|
09760340ca | ||
|
|
12980dd88f | ||
|
|
a860530a2e | ||
|
|
996f1036fc | ||
|
|
33ef5b7731 | ||
|
|
16be391211 | ||
|
|
94593dca4b | ||
|
|
98a1e87fbe | ||
|
|
6121c286b7 | ||
|
|
c91969d828 | ||
|
|
538298378a | ||
|
|
3a184bbadd | ||
|
|
38f106cde3 | ||
|
|
20acc123af | ||
|
|
ff65008316 | ||
|
|
1442fcb497 | ||
|
|
49f378ead3 | ||
|
|
0717d30389 | ||
|
|
36d9b3d483 | ||
|
|
a5eab29662 | ||
|
|
6a6dbe3aa1 | ||
|
|
37ffa86043 | ||
|
|
9095a6b04e | ||
|
|
69e0474ebb | ||
|
|
a88df2c103 | ||
|
|
53630dc74c | ||
|
|
1fa9496334 | ||
|
|
08f9c3f568 | ||
|
|
56f0418c93 | ||
|
|
2964a01d73 | ||
|
|
83f6a1ea49 | ||
|
|
6685d3ac97 | ||
|
|
bfc648553f | ||
|
|
5fad319cb5 | ||
|
|
fe04f69caf | ||
|
|
20d133322a | ||
|
|
a6dbaac653 | ||
|
|
33790b81fc | ||
|
|
373e18bc88 | ||
|
|
191fcf67d1 | ||
|
|
2f876471a1 | ||
|
|
659974411d | ||
|
|
6a9e8faad2 | ||
|
|
ea68f86476 | ||
|
|
d19957b705 | ||
|
|
328c8a94b3 | ||
|
|
3ab16d8012 | ||
|
|
ca9d5a2f6b | ||
|
|
bd00aed7db | ||
|
|
4097e8870b | ||
|
|
008c5053e6 | ||
|
|
2bfc646f33 | ||
|
|
3e287911c3 | ||
|
|
503bebaacc | ||
|
|
f79f56f8b4 | ||
|
|
a17c207217 | ||
|
|
fc8e515fe8 | ||
|
|
eaf2fbb21b | ||
|
|
8bc35c33c5 | ||
|
|
52052f342b | ||
|
|
d7962aa2d3 | ||
|
|
9735912965 | ||
|
|
5935681c5c | ||
|
|
12440d5e0d | ||
|
|
4b81b15cad | ||
|
|
87efb75e53 | ||
|
|
bf666af3a2 | ||
|
|
c9a509c805 | ||
|
|
c19587d4e4 | ||
|
|
0ac203bde0 | ||
|
|
5de7492146 | ||
|
|
f9dc871422 | ||
|
|
6fcd57ac53 | ||
|
|
0903062933 | ||
|
|
e5e6c7f09d | ||
|
|
ca2cda8d2a | ||
|
|
5c2bd816ae | ||
|
|
26fdd149e1 | ||
|
|
486f0ae454 | ||
|
|
cfe90c37fc | ||
|
|
e3608af992 | ||
|
|
8c3ae8b264 | ||
|
|
b2cc617886 | ||
|
|
a84a3c0ebe | ||
|
|
03f18053bb | ||
|
|
26103e8bb9 | ||
|
|
b7784d414a | ||
|
|
268fa1cbaf | ||
|
|
f3f2225a8e | ||
|
|
242f032d74 | ||
|
|
5523a510c5 | ||
|
|
3efb871cd4 | ||
|
|
37f7957826 | ||
|
|
400fb12f7e | ||
|
|
64460e492a | ||
|
|
cdf702aeff | ||
|
|
91d1146d97 |
@@ -10,3 +10,6 @@
|
||||
# in one spot, that's going to trigger a rebuild of all of the artifacts. Using ci-config.toml we can define these overrides for CI in one spot and not worry about it.
|
||||
[build]
|
||||
rustflags = ["-D", "warnings"]
|
||||
|
||||
[alias]
|
||||
xtask = "run --package xtask --"
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
[build]
|
||||
# v0 mangling scheme provides more detailed backtraces around closures
|
||||
rustflags = ["-C", "symbol-mangling-version=v0"]
|
||||
rustflags = ["-C", "symbol-mangling-version=v0", "--cfg", "tokio_unstable"]
|
||||
|
||||
[alias]
|
||||
xtask = "run --package xtask --"
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/1_bug_report.yml
vendored
6
.github/ISSUE_TEMPLATE/1_bug_report.yml
vendored
@@ -32,9 +32,9 @@ body:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: |
|
||||
If applicable, attach your `~/Library/Logs/Zed/Zed.log` file to this issue.
|
||||
label: If applicable, attach your `~/Library/Logs/Zed/Zed.log` file to this issue.
|
||||
description: |
|
||||
Drag Zed.log into the text input below.
|
||||
If you only need the most recent lines, you can run the `zed: open log` command palette action to see the last 1000.
|
||||
description: Drag Zed.log into the text input below
|
||||
validations:
|
||||
required: false
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/2_crash_report.yml
vendored
6
.github/ISSUE_TEMPLATE/2_crash_report.yml
vendored
@@ -31,9 +31,9 @@ body:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: |
|
||||
If applicable, attach your `~/Library/Logs/Zed/Zed.log` file to this issue.
|
||||
label: If applicable, attach your `~/Library/Logs/Zed/Zed.log` file to this issue.
|
||||
description: |
|
||||
Drag Zed.log into the text input below.
|
||||
If you only need the most recent lines, you can run the `zed: open log` command palette action to see the last 1000.
|
||||
description: Drag Zed.log into the text input below
|
||||
validations:
|
||||
required: false
|
||||
|
||||
31
.github/ISSUE_TEMPLATE/config.yml
vendored
31
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,16 +1,17 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Language Request
|
||||
url: https://github.com/zed-industries/extensions/issues/new?assignees=&labels=language&projects=&template=1_language_request.yml&title=%3Cname_of_language%3E
|
||||
about: Request a language in the extensions repository
|
||||
- name: Theme Request
|
||||
url: https://github.com/zed-industries/extensions/issues/new?assignees=&labels=theme&projects=&template=0_theme_request.yml&title=%3Cname_of_theme%3E+theme
|
||||
about: Request a theme in the extensions repository
|
||||
- name: Top-Ranking Issues
|
||||
url: https://github.com/zed-industries/zed/issues/5393
|
||||
about: See an overview of the most popular Zed issues
|
||||
- name: Platform Support
|
||||
url: https://github.com/zed-industries/zed/issues/5391
|
||||
about: A quick note on platform support
|
||||
- name: Positive Feedback
|
||||
url: https://github.com/zed-industries/zed/discussions/5397
|
||||
about: A central location for kind words about Zed
|
||||
- name: Language Request
|
||||
url: https://github.com/zed-industries/extensions/issues/new?assignees=&labels=language&projects=&template=1_language_request.yml&title=%3Cname_of_language%3E
|
||||
about: Request a language in the extensions repository
|
||||
- name: Theme Request
|
||||
url: https://github.com/zed-industries/extensions/issues/new?assignees=&labels=theme&projects=&template=0_theme_request.yml&title=%3Cname_of_theme%3E+theme
|
||||
about: Request a theme in the extensions repository
|
||||
- name: Top-Ranking Issues
|
||||
url: https://github.com/zed-industries/zed/issues/5393
|
||||
about: See an overview of the most popular Zed issues
|
||||
- name: Platform Support
|
||||
url: https://github.com/zed-industries/zed/issues/5391
|
||||
about: A quick note on platform support
|
||||
- name: Positive Feedback
|
||||
url: https://github.com/zed-industries/zed/discussions/5397
|
||||
about: A central location for kind words about Zed
|
||||
|
||||
101
.github/workflows/ci.yml
vendored
101
.github/workflows/ci.yml
vendored
@@ -87,8 +87,7 @@ jobs:
|
||||
submodules: "recursive"
|
||||
|
||||
- name: cargo clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: script/clippy
|
||||
run: cargo xtask clippy
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
@@ -120,8 +119,7 @@ jobs:
|
||||
run: script/linux
|
||||
|
||||
- name: cargo clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: script/clippy
|
||||
run: cargo xtask clippy
|
||||
|
||||
- name: Build Zed
|
||||
run: cargo build -p zed
|
||||
@@ -143,18 +141,17 @@ jobs:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: cargo clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: script/clippy
|
||||
run: cargo xtask clippy
|
||||
|
||||
- name: Build Zed
|
||||
run: cargo build -p zed
|
||||
|
||||
bundle:
|
||||
name: Bundle macOS app
|
||||
bundle-mac:
|
||||
name: Create a macOS bundle
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- bundle
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
needs: [macos_tests]
|
||||
env:
|
||||
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
@@ -209,12 +206,12 @@ jobs:
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
|
||||
- name: Create app bundle
|
||||
run: script/bundle
|
||||
- name: Create macOS app bundle
|
||||
run: script/bundle-mac
|
||||
|
||||
- name: Upload app bundle to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@v3
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg
|
||||
path: target/release/Zed.dmg
|
||||
@@ -229,3 +226,81 @@ jobs:
|
||||
body: ""
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
bundle-deb:
|
||||
name: Create a *.deb Linux bundle
|
||||
runs-on: ubuntu-22.04 # keep the version fixed to avoid libc and dynamic linked library issues
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
needs: [linux_tests]
|
||||
env:
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Configure linux
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: script/linux
|
||||
|
||||
- name: Determine version and release channel
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
version=$(script/get-crate-version zed)
|
||||
channel=$(cat crates/zed/RELEASE_CHANNEL)
|
||||
echo "Publishing version: ${version} on release channel ${channel}"
|
||||
echo "RELEASE_CHANNEL=${channel}" >> $GITHUB_ENV
|
||||
|
||||
expected_tag_name=""
|
||||
case ${channel} in
|
||||
stable)
|
||||
expected_tag_name="v${version}";;
|
||||
preview)
|
||||
expected_tag_name="v${version}-pre";;
|
||||
nightly)
|
||||
expected_tag_name="v${version}-nightly";;
|
||||
*)
|
||||
echo "can't publish a release on channel ${channel}"
|
||||
exit 1;;
|
||||
esac
|
||||
if [[ $GITHUB_REF_NAME != $expected_tag_name ]]; then
|
||||
echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# TODO linux : Find a way to add licenses to the final bundle
|
||||
# - name: Generate license file
|
||||
# run: script/generate-licenses
|
||||
|
||||
- name: Create Linux *.deb bundle
|
||||
run: script/bundle-linux
|
||||
|
||||
- name: Upload app bundle to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
with:
|
||||
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.deb
|
||||
path: target/release/*.deb
|
||||
|
||||
# TODO linux : make it stable enough to be uploaded as a release
|
||||
# - uses: softprops/action-gh-release@v1
|
||||
# name: Upload app bundle to release
|
||||
# if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }}
|
||||
# with:
|
||||
# draft: true
|
||||
# prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||
# files: target/release/Zed.dmg
|
||||
# body: ""
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
9
.github/workflows/deploy_collab.yml
vendored
9
.github/workflows/deploy_collab.yml
vendored
@@ -28,8 +28,7 @@ jobs:
|
||||
uses: ./.github/actions/check_style
|
||||
|
||||
- name: Run clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: script/clippy
|
||||
run: cargo xtask clippy
|
||||
|
||||
tests:
|
||||
name: Run tests
|
||||
@@ -105,8 +104,12 @@ jobs:
|
||||
set -eu
|
||||
if [[ $GITHUB_REF_NAME = "collab-production" ]]; then
|
||||
export ZED_KUBE_NAMESPACE=production
|
||||
export ZED_COLLAB_LOAD_BALANCER_SIZE_UNIT=10
|
||||
export ZED_API_LOAD_BALANCER_SIZE_UNIT=2
|
||||
elif [[ $GITHUB_REF_NAME = "collab-staging" ]]; then
|
||||
export ZED_KUBE_NAMESPACE=staging
|
||||
export ZED_COLLAB_LOAD_BALANCER_SIZE_UNIT=1
|
||||
export ZED_API_LOAD_BALANCER_SIZE_UNIT=1
|
||||
else
|
||||
echo "cowardly refusing to deploy from an unknown branch"
|
||||
exit 1
|
||||
@@ -121,11 +124,13 @@ jobs:
|
||||
export ZED_IMAGE_ID="registry.digitalocean.com/zed/collab:${GITHUB_SHA}"
|
||||
|
||||
export ZED_SERVICE_NAME=collab
|
||||
export ZED_LOAD_BALANCER_SIZE_UNIT=$ZED_COLLAB_LOAD_BALANCER_SIZE_UNIT
|
||||
envsubst < crates/collab/k8s/collab.template.yml | kubectl apply -f -
|
||||
kubectl -n "$ZED_KUBE_NAMESPACE" rollout status deployment/$ZED_SERVICE_NAME --watch
|
||||
echo "deployed ${ZED_SERVICE_NAME} to ${ZED_KUBE_NAMESPACE}"
|
||||
|
||||
export ZED_SERVICE_NAME=api
|
||||
export ZED_LOAD_BALANCER_SIZE_UNIT=$ZED_API_LOAD_BALANCER_SIZE_UNIT
|
||||
envsubst < crates/collab/k8s/collab.template.yml | kubectl apply -f -
|
||||
kubectl -n "$ZED_KUBE_NAMESPACE" rollout status deployment/$ZED_SERVICE_NAME --watch
|
||||
echo "deployed ${ZED_SERVICE_NAME} to ${ZED_KUBE_NAMESPACE}"
|
||||
|
||||
58
.github/workflows/release_nightly.yml
vendored
58
.github/workflows/release_nightly.yml
vendored
@@ -32,8 +32,7 @@ jobs:
|
||||
uses: ./.github/actions/check_style
|
||||
|
||||
- name: Run clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: script/clippy
|
||||
run: cargo xtask clippy
|
||||
tests:
|
||||
name: Run tests
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
@@ -51,8 +50,8 @@ jobs:
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
|
||||
bundle:
|
||||
name: Bundle app
|
||||
bundle-mac:
|
||||
name: Create a macOS bundle
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on:
|
||||
- self-hosted
|
||||
@@ -78,9 +77,6 @@ jobs:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 100
|
||||
|
||||
- name: Set release channel to nightly
|
||||
run: |
|
||||
set -eu
|
||||
@@ -91,8 +87,50 @@ jobs:
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
|
||||
- name: Create app bundle
|
||||
run: script/bundle
|
||||
- name: Create macOS app bundle
|
||||
run: script/bundle-mac
|
||||
|
||||
- name: Upload Zed Nightly
|
||||
run: script/upload-nightly
|
||||
run: script/upload-nightly macos
|
||||
|
||||
bundle-deb:
|
||||
name: Create a *.deb Linux bundle
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-22.04 # keep the version fixed to avoid libc and dynamic linked library issues
|
||||
needs: tests
|
||||
env:
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Configure linux
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: script/linux
|
||||
|
||||
- name: Set release channel to nightly
|
||||
run: |
|
||||
set -euo pipefail
|
||||
version=$(git rev-parse --short HEAD)
|
||||
echo "Publishing version: ${version} on release channel nightly"
|
||||
echo "nightly" > crates/zed/RELEASE_CHANNEL
|
||||
|
||||
# TODO linux : find a way to add licenses to the final bundle
|
||||
# - name: Generate license file
|
||||
# run: script/generate-licenses
|
||||
|
||||
- name: Create Linux *.deb bundle
|
||||
run: script/bundle-linux
|
||||
|
||||
- name: Upload Zed Nightly
|
||||
run: script/upload-nightly linux-deb
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -10,6 +10,7 @@
|
||||
/assets/*licenses.md
|
||||
**/venv
|
||||
.build
|
||||
*.wasm
|
||||
Packages
|
||||
*.xcodeproj
|
||||
xcuserdata/
|
||||
@@ -22,3 +23,4 @@ DerivedData/
|
||||
.pytest_cache
|
||||
.venv
|
||||
.blob_store
|
||||
.vscode
|
||||
|
||||
1620
Cargo.lock
generated
1620
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
102
Cargo.toml
102
Cargo.toml
@@ -23,6 +23,7 @@ members = [
|
||||
"crates/diagnostics",
|
||||
"crates/editor",
|
||||
"crates/extension",
|
||||
"crates/extension_api",
|
||||
"crates/extensions_ui",
|
||||
"crates/feature_flags",
|
||||
"crates/feedback",
|
||||
@@ -91,6 +92,11 @@ members = [
|
||||
"crates/workspace",
|
||||
"crates/zed",
|
||||
"crates/zed_actions",
|
||||
|
||||
"extensions/gleam",
|
||||
"extensions/uiua",
|
||||
|
||||
"tooling/xtask",
|
||||
]
|
||||
default-members = ["crates/zed"]
|
||||
resolver = "2"
|
||||
@@ -102,6 +108,7 @@ assets = { path = "crates/assets" }
|
||||
assistant = { path = "crates/assistant" }
|
||||
audio = { path = "crates/audio" }
|
||||
auto_update = { path = "crates/auto_update" }
|
||||
base64 = "0.13"
|
||||
breadcrumbs = { path = "crates/breadcrumbs" }
|
||||
call = { path = "crates/call" }
|
||||
channel = { path = "crates/channel" }
|
||||
@@ -193,15 +200,19 @@ anyhow = "1.0.57"
|
||||
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
|
||||
async-tar = "0.4.2"
|
||||
async-trait = "0.1"
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e9d93a4d41f3946a03ffb76136290d6ccf7f2b80" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "e9d93a4d41f3946a03ffb76136290d6ccf7f2b80" }
|
||||
bitflags = "2.4.2"
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "43721bf42d298b7cbee2195ee66f73a5f1c7b2fc" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "43721bf42d298b7cbee2195ee66f73a5f1c7b2fc" }
|
||||
blade-rwh = { package = "raw-window-handle", version = "0.5" }
|
||||
cap-std = "2.0"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
clap = "4.4"
|
||||
clickhouse = { version = "0.11.6" }
|
||||
ctor = "0.2.6"
|
||||
core-foundation = { version = "0.9.3" }
|
||||
core-foundation-sys = "0.8.6"
|
||||
derive_more = "0.99.17"
|
||||
emojis = "0.6.1"
|
||||
env_logger = "0.9"
|
||||
futures = "0.3"
|
||||
git2 = { version = "0.15", default-features = false }
|
||||
@@ -210,7 +221,10 @@ hex = "0.4.3"
|
||||
ignore = "0.4.22"
|
||||
indoc = "1"
|
||||
# We explicitly disable http2 support in isahc.
|
||||
isahc = { version = "1.7.2", default-features = false, features = ["static-curl", "text-decoding"] }
|
||||
isahc = { version = "1.7.2", default-features = false, features = [
|
||||
"static-curl",
|
||||
"text-decoding",
|
||||
] }
|
||||
itertools = "0.11.0"
|
||||
lazy_static = "1.4.0"
|
||||
linkify = "0.10.0"
|
||||
@@ -233,18 +247,26 @@ semver = "1.0"
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
|
||||
serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
|
||||
serde_json_lenient = { version = "0.1", features = ["preserve_order", "raw_value"] }
|
||||
serde_json_lenient = { version = "0.1", features = [
|
||||
"preserve_order",
|
||||
"raw_value",
|
||||
] }
|
||||
serde_repr = "0.1"
|
||||
sha2 = "0.10"
|
||||
shellexpand = "2.1.0"
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
smol = "1.2"
|
||||
strum = { version = "0.25.0", features = ["derive"] }
|
||||
subtle = "2.5.0"
|
||||
sysinfo = "0.29.10"
|
||||
tempfile = "3.9.0"
|
||||
thiserror = "1.0.29"
|
||||
tiktoken-rs = "0.5.7"
|
||||
time = { version = "0.3", features = ["serde", "serde-well-known", "formatting"] }
|
||||
time = { version = "0.3", features = [
|
||||
"serde",
|
||||
"serde-well-known",
|
||||
"formatting",
|
||||
] }
|
||||
toml = "0.8"
|
||||
tower-http = "0.4.4"
|
||||
tree-sitter = { version = "0.20", features = ["wasm"] }
|
||||
@@ -261,7 +283,6 @@ tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir"
|
||||
tree-sitter-elm = { git = "https://github.com/elm-tooling/tree-sitter-elm", rev = "692c50c0b961364c40299e73c1306aecb5d20f40" }
|
||||
tree-sitter-embedded-template = "0.20.0"
|
||||
tree-sitter-erlang = "0.4.0"
|
||||
tree-sitter-gitcommit = { git = "https://github.com/gbprod/tree-sitter-gitcommit" }
|
||||
tree-sitter-gleam = { git = "https://github.com/gleam-lang/tree-sitter-gleam", rev = "58b7cac8fc14c92b0677c542610d8738c373fa81" }
|
||||
tree-sitter-glsl = { git = "https://github.com/theHamsta/tree-sitter-glsl", rev = "2a56fb7bc8bb03a1892b4741279dd0a8758b7fb3" }
|
||||
tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" }
|
||||
@@ -269,6 +290,7 @@ tree-sitter-gomod = { git = "https://github.com/camdencheek/tree-sitter-go-mod"
|
||||
tree-sitter-gowork = { git = "https://github.com/d1y/tree-sitter-go-work" }
|
||||
tree-sitter-haskell = { git = "https://github.com/tree-sitter/tree-sitter-haskell", rev = "8a99848fc734f9c4ea523b3f2a07df133cbbcec2" }
|
||||
tree-sitter-hcl = { git = "https://github.com/MichaHoffmann/tree-sitter-hcl", rev = "v1.1.0" }
|
||||
rustc-demangle = "0.1.23"
|
||||
tree-sitter-heex = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "2e1348c3cf2c9323e87c2744796cf3f3868aa82a" }
|
||||
tree-sitter-html = "0.19.0"
|
||||
tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "40a81c01a40ac48744e0c8ccabbaba1920441199" }
|
||||
@@ -289,7 +311,6 @@ tree-sitter-scheme = { git = "https://github.com/6cdh/tree-sitter-scheme", rev =
|
||||
tree-sitter-svelte = { git = "https://github.com/Himujjal/tree-sitter-svelte", rev = "bd60db7d3d06f89b6ec3b287c9a6e9190b5564bd" }
|
||||
tree-sitter-toml = { git = "https://github.com/tree-sitter/tree-sitter-toml", rev = "342d9be207c2dba869b9967124c679b5e6fd0ebe" }
|
||||
tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259" }
|
||||
tree-sitter-uiua = { git = "https://github.com/shnarazk/tree-sitter-uiua", rev = "21dc2db39494585bf29a3f86d5add6e9d11a22ba" }
|
||||
tree-sitter-vue = { git = "https://github.com/zed-industries/tree-sitter-vue", rev = "6608d9d60c386f19d80af7d8132322fa11199c42" }
|
||||
tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "f545a41f57502e1b5ddf2a6668896c1b0620f930" }
|
||||
tree-sitter-zig = { git = "https://github.com/maxxnino/tree-sitter-zig", rev = "0d08703e4c3f426ec61695d7617415fff97029bd" }
|
||||
@@ -297,10 +318,38 @@ unindent = "0.1.7"
|
||||
unicase = "2.6"
|
||||
url = "2.2"
|
||||
uuid = { version = "1.1.2", features = ["v4"] }
|
||||
wasmtime = "18.0"
|
||||
wasmparser = "0.121"
|
||||
wasm-encoder = "0.41"
|
||||
wasmtime = { version = "18.0", default-features = false, features = ["async", "demangle", "runtime", "cranelift", "component-model"] }
|
||||
wasmtime-wasi = "18.0"
|
||||
which = "6.0.0"
|
||||
wit-component = "0.20"
|
||||
sys-locale = "0.3.1"
|
||||
|
||||
[workspace.dependencies.windows]
|
||||
version = "0.53.0"
|
||||
features = [
|
||||
"implement",
|
||||
"Wdk_System_SystemServices",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_Graphics_DirectComposition",
|
||||
"Win32_UI_Controls",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
"Win32_UI_Input_KeyboardAndMouse",
|
||||
"Win32_UI_Shell",
|
||||
"Win32_System_Com",
|
||||
"Win32_System_SystemInformation",
|
||||
"Win32_System_SystemServices",
|
||||
"Win32_System_Time",
|
||||
"Win32_Security",
|
||||
"Win32_System_Com",
|
||||
"Win32_System_Com_StructuredStorage",
|
||||
"Win32_System_Threading",
|
||||
"Win32_System_DataExchange",
|
||||
"Win32_System_Ole",
|
||||
"Win32_System_Com",
|
||||
]
|
||||
|
||||
[patch.crates-io]
|
||||
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "e4a23971ec3071a09c1e84816954c98f96e98e52" }
|
||||
# Workaround for a broken nightly build of gpui: See #7644 and revisit once 0.5.3 is released.
|
||||
@@ -318,6 +367,8 @@ debug = "full"
|
||||
[profile.dev.package]
|
||||
taffy = { opt-level = 3 }
|
||||
cranelift-codegen = { opt-level = 3 }
|
||||
rustybuzz = { opt-level = 3 }
|
||||
ttf-parser = { opt-level = 3 }
|
||||
wasmtime-cranelift = { opt-level = 3 }
|
||||
|
||||
[profile.release]
|
||||
@@ -325,5 +376,40 @@ debug = "limited"
|
||||
lto = "thin"
|
||||
codegen-units = 1
|
||||
|
||||
[workspace.lints.clippy]
|
||||
dbg_macro = "deny"
|
||||
todo = "deny"
|
||||
|
||||
# These are all of the rules that currently have violations in the Zed
|
||||
# codebase.
|
||||
#
|
||||
# We'll want to drive this list down by either:
|
||||
# 1. fixing violations of the rule and begin enforcing it
|
||||
# 2. deciding we want to allow the rule permanently, at which point
|
||||
# we should codify that separately above.
|
||||
#
|
||||
# This list shouldn't be added to; it should only get shorter.
|
||||
# =============================================================================
|
||||
|
||||
# There are a bunch of rules currently failing in the `style` group, so
|
||||
# allow all of those, for now.
|
||||
style = "allow"
|
||||
|
||||
# Individual rules that have violations in the codebase:
|
||||
almost_complete_range = "allow"
|
||||
arc_with_non_send_sync = "allow"
|
||||
await_holding_lock = "allow"
|
||||
borrowed_box = "allow"
|
||||
derive_ord_xor_partial_ord = "allow"
|
||||
eq_op = "allow"
|
||||
let_underscore_future = "allow"
|
||||
map_entry = "allow"
|
||||
never_loop = "allow"
|
||||
non_canonical_clone_impl = "allow"
|
||||
non_canonical_partial_ord_impl = "allow"
|
||||
reversed_empty_ranges = "allow"
|
||||
single_range_in_vec_init = "allow"
|
||||
type_complexity = "allow"
|
||||
|
||||
[workspace.metadata.cargo-machete]
|
||||
ignored = ["bindgen", "cbindgen", "prost_build", "serde"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax = docker/dockerfile:1.2
|
||||
|
||||
FROM rust:1.76-bullseye as builder
|
||||
FROM rust:1.76-bookworm as builder
|
||||
WORKDIR app
|
||||
COPY . .
|
||||
|
||||
@@ -20,9 +20,10 @@ RUN --mount=type=cache,target=./target \
|
||||
cp /app/target/release/collab /app/collab
|
||||
|
||||
# Copy collab server binary to the runtime image
|
||||
FROM debian:bullseye-slim as runtime
|
||||
FROM debian:bookworm-slim as runtime
|
||||
RUN apt-get update; \
|
||||
apt-get install -y --no-install-recommends libcurl4-openssl-dev ca-certificates
|
||||
apt-get install -y --no-install-recommends libcurl4-openssl-dev ca-certificates \
|
||||
linux-perf binutils
|
||||
WORKDIR app
|
||||
COPY --from=builder /app/collab /app/collab
|
||||
COPY --from=builder /app/crates/collab/migrations /app/migrations
|
||||
|
||||
2
Procfile
2
Procfile
@@ -1,3 +1,3 @@
|
||||
collab: RUST_LOG=${RUST_LOG:-warn,tower_http=info,collab=info} cargo run --package=collab serve
|
||||
livekit: livekit-server --dev
|
||||
blob_store: MINIO_ROOT_USER=the-blob-store-access-key MINIO_ROOT_PASSWORD=the-blob-store-secret-key minio server .blob_store
|
||||
blob_store: ./script/run-local-minio
|
||||
|
||||
@@ -10,7 +10,7 @@ You can [download](https://zed.dev/download) Zed today for macOS (v10.15+).
|
||||
|
||||
Support for additional platforms is on our [roadmap](https://zed.dev/roadmap):
|
||||
|
||||
- Linux ([tracking issue](https://github.com/zed-industries/zed/issues/5395))
|
||||
- Linux ([tracking issue](https://github.com/zed-industries/zed/issues/7015))
|
||||
- Windows ([tracking issue](https://github.com/zed-industries/zed/issues/5394))
|
||||
- Web ([tracking issue](https://github.com/zed-industries/zed/issues/5396))
|
||||
|
||||
@@ -23,6 +23,7 @@ brew install zed
|
||||
|
||||
- [Building Zed for macOS](./docs/src/developing_zed__building_zed_macos.md)
|
||||
- [Building Zed for Linux](./docs/src/developing_zed__building_zed_linux.md)
|
||||
- [Building Zed for Windows](./docs/src/developing_zed__building_zed_windows.md)
|
||||
- [Running Collaboration Locally](./docs/src/developing_zed__local_collaboration.md)
|
||||
|
||||
## Contributing
|
||||
|
||||
4
assets/icons/reply_arrow.svg
Normal file
4
assets/icons/reply_arrow.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 17V15.8C20 14.1198 20 13.2798 19.673 12.638C19.3854 12.0735 18.9265 11.6146 18.362 11.327C17.7202 11 16.8802 11 15.2 11H4M4 11L8 7M4 11L8 15" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 468 B |
@@ -118,7 +118,8 @@
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
],
|
||||
"ctrl-;": "editor::ToggleLineNumbers"
|
||||
"ctrl-;": "editor::ToggleLineNumbers",
|
||||
"ctrl-alt-z": "editor::RevertSelectedHunks"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -135,10 +136,21 @@
|
||||
"focus": true
|
||||
}
|
||||
],
|
||||
"alt-\\": "copilot::Suggest",
|
||||
"ctrl->": "assistant::QuoteSelection"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full && copilot_suggestion",
|
||||
"bindings": {
|
||||
"alt-]": "copilot::NextSuggestion",
|
||||
"alt-[": "copilot::PreviousSuggestion",
|
||||
"ctrl->": "assistant::QuoteSelection"
|
||||
"alt-right": "editor::AcceptPartialCopilotSuggestion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && !copilot_suggestion",
|
||||
"bindings": {
|
||||
"alt-\\": "copilot::Suggest"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -343,10 +355,18 @@
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
// Change the default action on `menu::Confirm` by setting the parameter
|
||||
// "alt-cmd-o": [
|
||||
// "projects::OpenRecent",
|
||||
// {
|
||||
// "create_new_window": true
|
||||
// }
|
||||
// ]
|
||||
"ctrl-alt-o": "projects::OpenRecent",
|
||||
"ctrl-alt-b": "branches::OpenRecent",
|
||||
"ctrl-~": "workspace::NewTerminal",
|
||||
"ctrl-s": "workspace::Save",
|
||||
"ctrl-k s": "workspace::SaveWithoutFormat",
|
||||
"ctrl-shift-s": "workspace::SaveAs",
|
||||
"ctrl-n": "workspace::NewFile",
|
||||
"ctrl-shift-n": "workspace::NewWindow",
|
||||
|
||||
@@ -153,7 +153,8 @@
|
||||
}
|
||||
],
|
||||
"ctrl-cmd-space": "editor::ShowCharacterPalette",
|
||||
"cmd-;": "editor::ToggleLineNumbers"
|
||||
"cmd-;": "editor::ToggleLineNumbers",
|
||||
"cmd-alt-z": "editor::RevertSelectedHunks"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -176,10 +177,21 @@
|
||||
"focus": false
|
||||
}
|
||||
],
|
||||
"alt-\\": "copilot::Suggest",
|
||||
"cmd->": "assistant::QuoteSelection"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full && copilot_suggestion",
|
||||
"bindings": {
|
||||
"alt-]": "copilot::NextSuggestion",
|
||||
"alt-[": "copilot::PreviousSuggestion",
|
||||
"cmd->": "assistant::QuoteSelection"
|
||||
"alt-right": "editor::AcceptPartialCopilotSuggestion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && !copilot_suggestion",
|
||||
"bindings": {
|
||||
"alt-\\": "copilot::Suggest"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -304,6 +316,18 @@
|
||||
"cmd-ctrl-p": "editor::AddSelectionAbove",
|
||||
"cmd-alt-down": "editor::AddSelectionBelow",
|
||||
"cmd-ctrl-n": "editor::AddSelectionBelow",
|
||||
"cmd-shift-k": "editor::DeleteLine",
|
||||
"alt-up": "editor::MoveLineUp",
|
||||
"alt-down": "editor::MoveLineDown",
|
||||
"alt-shift-up": [
|
||||
"editor::DuplicateLine",
|
||||
{
|
||||
"move_upwards": true
|
||||
}
|
||||
],
|
||||
"alt-shift-down": "editor::DuplicateLine",
|
||||
"ctrl-shift-right": "editor::SelectLargerSyntaxNode",
|
||||
"ctrl-shift-left": "editor::SelectSmallerSyntaxNode",
|
||||
"cmd-d": [
|
||||
"editor::SelectNext",
|
||||
{
|
||||
@@ -336,8 +360,6 @@
|
||||
"advance_downwards": false
|
||||
}
|
||||
],
|
||||
"alt-up": "editor::SelectLargerSyntaxNode",
|
||||
"alt-down": "editor::SelectSmallerSyntaxNode",
|
||||
"cmd-u": "editor::UndoSelection",
|
||||
"cmd-shift-u": "editor::RedoSelection",
|
||||
"f8": "editor::GoToDiagnostic",
|
||||
@@ -386,10 +408,18 @@
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
// Change the default action on `menu::Confirm` by setting the parameter
|
||||
// "alt-cmd-o": [
|
||||
// "projects::OpenRecent",
|
||||
// {
|
||||
// "create_new_window": true
|
||||
// }
|
||||
// ]
|
||||
"alt-cmd-o": "projects::OpenRecent",
|
||||
"alt-cmd-b": "branches::OpenRecent",
|
||||
"ctrl-~": "workspace::NewTerminal",
|
||||
"cmd-s": "workspace::Save",
|
||||
"cmd-k s": "workspace::SaveWithoutFormat",
|
||||
"cmd-shift-s": "workspace::SaveAs",
|
||||
"cmd-n": "workspace::NewFile",
|
||||
"cmd-shift-n": "workspace::NewWindow",
|
||||
@@ -408,8 +438,8 @@
|
||||
"cmd-j": "workspace::ToggleBottomDock",
|
||||
"alt-cmd-y": "workspace::CloseAllDocks",
|
||||
"cmd-shift-f": "pane::DeploySearch",
|
||||
"cmd-k cmd-t": "theme_selector::Toggle",
|
||||
"cmd-k cmd-s": "zed::OpenKeymap",
|
||||
"cmd-k cmd-t": "theme_selector::Toggle",
|
||||
"cmd-t": "project_symbols::Toggle",
|
||||
"cmd-p": "file_finder::Toggle",
|
||||
"cmd-shift-p": "command_palette::Toggle",
|
||||
@@ -435,11 +465,7 @@
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl-shift-k": "editor::DeleteLine",
|
||||
"cmd-shift-d": "editor::DuplicateLine",
|
||||
"ctrl-j": "editor::JoinLines",
|
||||
"ctrl-cmd-up": "editor::MoveLineUp",
|
||||
"ctrl-cmd-down": "editor::MoveLineDown",
|
||||
"ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
|
||||
"ctrl-alt-h": "editor::DeleteToPreviousSubwordStart",
|
||||
"ctrl-alt-delete": "editor::DeleteToNextSubwordEnd",
|
||||
|
||||
@@ -39,6 +39,8 @@
|
||||
"advance_downwards": true
|
||||
}
|
||||
],
|
||||
"alt-up": "editor::SelectLargerSyntaxNode",
|
||||
"alt-down": "editor::SelectSmallerSyntaxNode",
|
||||
"shift-alt-up": "editor::MoveLineUp",
|
||||
"shift-alt-down": "editor::MoveLineDown",
|
||||
"cmd-alt-l": "editor::Format",
|
||||
|
||||
@@ -37,30 +37,42 @@
|
||||
"_": "vim::StartOfLineDownward",
|
||||
"g _": "vim::EndOfLineDownward",
|
||||
"shift-g": "vim::EndOfDocument",
|
||||
"w": "vim::NextWordStart",
|
||||
"{": "vim::StartOfParagraph",
|
||||
"}": "vim::EndOfParagraph",
|
||||
"|": "vim::GoToColumn",
|
||||
|
||||
// Word motions
|
||||
"w": "vim::NextWordStart",
|
||||
"e": "vim::NextWordEnd",
|
||||
"b": "vim::PreviousWordStart",
|
||||
"g e": "vim::PreviousWordEnd",
|
||||
|
||||
// Subword motions
|
||||
// "w": "vim::NextSubwordStart",
|
||||
// "b": "vim::PreviousSubwordStart",
|
||||
// "e": "vim::NextSubwordEnd",
|
||||
// "g e": "vim::PreviousSubwordEnd",
|
||||
|
||||
"shift-w": [
|
||||
"vim::NextWordStart",
|
||||
{
|
||||
"ignorePunctuation": true
|
||||
}
|
||||
],
|
||||
"e": "vim::NextWordEnd",
|
||||
"shift-e": [
|
||||
"vim::NextWordEnd",
|
||||
{
|
||||
"ignorePunctuation": true
|
||||
}
|
||||
],
|
||||
"b": "vim::PreviousWordStart",
|
||||
"shift-b": [
|
||||
"vim::PreviousWordStart",
|
||||
{
|
||||
"ignorePunctuation": true
|
||||
}
|
||||
],
|
||||
"g shift-e": ["vim::PreviousWordEnd", { "ignorePunctuation": true }],
|
||||
|
||||
"n": "search::SelectNextMatch",
|
||||
"shift-n": "search::SelectPrevMatch",
|
||||
"%": "vim::Matching",
|
||||
@@ -117,8 +129,6 @@
|
||||
"ctrl-e": "vim::LineDown",
|
||||
"ctrl-y": "vim::LineUp",
|
||||
// "g" commands
|
||||
"g e": "vim::PreviousWordEnd",
|
||||
"g shift-e": ["vim::PreviousWordEnd", { "ignorePunctuation": true }],
|
||||
"g g": "vim::StartOfDocument",
|
||||
"g h": "editor::Hover",
|
||||
"g t": "pane::ActivateNextItem",
|
||||
@@ -218,6 +228,7 @@
|
||||
// z commands
|
||||
"z t": "editor::ScrollCursorTop",
|
||||
"z z": "editor::ScrollCursorCenter",
|
||||
"z .": ["workspace::SendKeystrokes", "z z ^"],
|
||||
"z b": "editor::ScrollCursorBottom",
|
||||
"z c": "editor::Fold",
|
||||
"z o": "editor::UnfoldLines",
|
||||
@@ -352,7 +363,9 @@
|
||||
"> >": "vim::Indent",
|
||||
"< <": "vim::Outdent",
|
||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||
"ctrl-pageup": "pane::ActivatePrevItem"
|
||||
"ctrl-pageup": "pane::ActivatePrevItem",
|
||||
"[ x": "editor::SelectLargerSyntaxNode",
|
||||
"] x": "editor::SelectSmallerSyntaxNode"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -392,6 +405,7 @@
|
||||
],
|
||||
"t": "vim::Tag",
|
||||
"s": "vim::Sentence",
|
||||
"p": "vim::Paragraph",
|
||||
"'": "vim::Quotes",
|
||||
"`": "vim::BackQuotes",
|
||||
"\"": "vim::DoubleQuotes",
|
||||
|
||||
@@ -211,6 +211,11 @@
|
||||
// Default width of the channels panel.
|
||||
"default_width": 240
|
||||
},
|
||||
"message_editor": {
|
||||
// Whether to automatically replace emoji shortcodes with emoji characters.
|
||||
// For example: typing `:wave:` gets replaced with `👋`.
|
||||
"auto_replace_emoji_shortcode": true
|
||||
},
|
||||
"notification_panel": {
|
||||
// Whether to show the collaboration panel button in the status bar.
|
||||
"button": true,
|
||||
@@ -472,6 +477,10 @@
|
||||
// Can also be 'csh', 'fish', and `nushell`
|
||||
"activate_script": "default"
|
||||
}
|
||||
},
|
||||
"toolbar": {
|
||||
// Whether to display the terminal title in its toolbar.
|
||||
"title": true
|
||||
}
|
||||
// Set the terminal's font size. If this option is not included,
|
||||
// the terminal will default to matching the buffer's font size.
|
||||
@@ -586,7 +595,8 @@
|
||||
// Vim settings
|
||||
"vim": {
|
||||
"use_system_clipboard": "always",
|
||||
"use_multiline_find": false
|
||||
"use_multiline_find": false,
|
||||
"use_smartcase_find": false
|
||||
},
|
||||
// The server to connect to. If the environment variable
|
||||
// ZED_SERVER_URL is set, it will override this setting.
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
[
|
||||
{
|
||||
"label": "Example task",
|
||||
"command": "bash",
|
||||
// rest of the parameters are optional
|
||||
"args": ["-c", "for i in {1..5}; do echo \"Hello $i/5\"; sleep 1; done"],
|
||||
"command": "for i in {1..5}; do echo \"Hello $i/5\"; sleep 1; done",
|
||||
// Env overrides for the command, will be appended to the terminal's environment from the settings.
|
||||
"env": { "foo": "bar" },
|
||||
// Current working directory to spawn the command into, defaults to current project root.
|
||||
|
||||
@@ -5,6 +5,9 @@ edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/activity_indicator.rs"
|
||||
doctest = false
|
||||
|
||||
@@ -6,7 +6,7 @@ use gpui::{
|
||||
ParentElement as _, Render, SharedString, StatefulInteractiveElement, Styled, View,
|
||||
ViewContext, VisualContext as _,
|
||||
};
|
||||
use language::{LanguageRegistry, LanguageServerBinaryStatus};
|
||||
use language::{LanguageRegistry, LanguageServerBinaryStatus, LanguageServerName};
|
||||
use project::{LanguageServerProgress, Project};
|
||||
use smallvec::SmallVec;
|
||||
use std::{cmp::Reverse, fmt::Write, sync::Arc};
|
||||
@@ -30,7 +30,7 @@ pub struct ActivityIndicator {
|
||||
}
|
||||
|
||||
struct LspStatus {
|
||||
name: Arc<str>,
|
||||
name: LanguageServerName,
|
||||
status: LanguageServerBinaryStatus,
|
||||
}
|
||||
|
||||
@@ -58,13 +58,10 @@ impl ActivityIndicator {
|
||||
let this = cx.new_view(|cx: &mut ViewContext<Self>| {
|
||||
let mut status_events = languages.language_server_binary_statuses();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
while let Some((language, event)) = status_events.next().await {
|
||||
while let Some((name, status)) = status_events.next().await {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.statuses.retain(|s| s.name != language.name());
|
||||
this.statuses.push(LspStatus {
|
||||
name: language.name(),
|
||||
status: event,
|
||||
});
|
||||
this.statuses.retain(|s| s.name != name);
|
||||
this.statuses.push(LspStatus { name, status });
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
@@ -114,7 +111,7 @@ impl ActivityIndicator {
|
||||
self.statuses.retain(|status| {
|
||||
if let LanguageServerBinaryStatus::Failed { error } = &status.status {
|
||||
cx.emit(Event::ShowError {
|
||||
lsp_name: status.name.clone(),
|
||||
lsp_name: status.name.0.clone(),
|
||||
error: error.clone(),
|
||||
});
|
||||
false
|
||||
@@ -202,11 +199,12 @@ impl ActivityIndicator {
|
||||
let mut checking_for_update = SmallVec::<[_; 3]>::new();
|
||||
let mut failed = SmallVec::<[_; 3]>::new();
|
||||
for status in &self.statuses {
|
||||
let name = status.name.clone();
|
||||
match status.status {
|
||||
LanguageServerBinaryStatus::CheckingForUpdate => checking_for_update.push(name),
|
||||
LanguageServerBinaryStatus::Downloading => downloading.push(name),
|
||||
LanguageServerBinaryStatus::Failed { .. } => failed.push(name),
|
||||
LanguageServerBinaryStatus::CheckingForUpdate => {
|
||||
checking_for_update.push(status.name.0.as_ref())
|
||||
}
|
||||
LanguageServerBinaryStatus::Downloading => downloading.push(status.name.0.as_ref()),
|
||||
LanguageServerBinaryStatus::Failed { .. } => failed.push(status.name.0.as_ref()),
|
||||
LanguageServerBinaryStatus::Downloaded | LanguageServerBinaryStatus::Cached => {}
|
||||
}
|
||||
}
|
||||
@@ -214,34 +212,28 @@ impl ActivityIndicator {
|
||||
if !downloading.is_empty() {
|
||||
return Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: format!(
|
||||
"Downloading {} language server{}...",
|
||||
downloading.join(", "),
|
||||
if downloading.len() > 1 { "s" } else { "" }
|
||||
),
|
||||
message: format!("Downloading {}...", downloading.join(", "),),
|
||||
on_click: None,
|
||||
};
|
||||
} else if !checking_for_update.is_empty() {
|
||||
}
|
||||
|
||||
if !checking_for_update.is_empty() {
|
||||
return Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: format!(
|
||||
"Checking for updates to {} language server{}...",
|
||||
"Checking for updates to {}...",
|
||||
checking_for_update.join(", "),
|
||||
if checking_for_update.len() > 1 {
|
||||
"s"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
),
|
||||
on_click: None,
|
||||
};
|
||||
} else if !failed.is_empty() {
|
||||
}
|
||||
|
||||
if !failed.is_empty() {
|
||||
return Content {
|
||||
icon: Some(WARNING_ICON),
|
||||
message: format!(
|
||||
"Failed to download {} language server{}. Click to show error.",
|
||||
"Failed to download {}. Click to show error.",
|
||||
failed.join(", "),
|
||||
if failed.len() > 1 { "s" } else { "" }
|
||||
),
|
||||
on_click: Some(Arc::new(|this, cx| {
|
||||
this.show_error_message(&Default::default(), cx)
|
||||
|
||||
@@ -5,6 +5,9 @@ edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/ai.rs"
|
||||
doctest = false
|
||||
|
||||
@@ -19,11 +19,9 @@ pub struct Embedding(pub Vec<f32>);
|
||||
impl FromSql for Embedding {
|
||||
fn column_result(value: ValueRef) -> FromSqlResult<Self> {
|
||||
let bytes = value.as_blob()?;
|
||||
let embedding: Result<Vec<f32>, Box<bincode::ErrorKind>> = bincode::deserialize(bytes);
|
||||
if embedding.is_err() {
|
||||
return Err(rusqlite::types::FromSqlError::Other(embedding.unwrap_err()));
|
||||
}
|
||||
Ok(Embedding(embedding.unwrap()))
|
||||
let embedding =
|
||||
bincode::deserialize(bytes).map_err(|err| rusqlite::types::FromSqlError::Other(err))?;
|
||||
Ok(Embedding(embedding))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +110,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn round_to_decimals(n: OrderedFloat<f32>, decimal_places: i32) -> f32 {
|
||||
let factor = (10.0 as f32).powi(decimal_places);
|
||||
let factor = 10.0_f32.powi(decimal_places);
|
||||
(n * factor).round() / factor
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ impl PromptArguments {
|
||||
if self
|
||||
.language_name
|
||||
.as_ref()
|
||||
.and_then(|name| Some(!["Markdown", "Plain Text"].contains(&name.as_str())))
|
||||
.map(|name| !["Markdown", "Plain Text"].contains(&name.as_str()))
|
||||
.unwrap_or(true)
|
||||
{
|
||||
PromptFileType::Code
|
||||
@@ -51,8 +51,10 @@ pub trait PromptTemplate {
|
||||
#[repr(i8)]
|
||||
#[derive(PartialEq, Eq, Ord)]
|
||||
pub enum PromptPriority {
|
||||
Mandatory, // Ignores truncation
|
||||
Ordered { order: usize }, // Truncates based on priority
|
||||
/// Ignores truncation.
|
||||
Mandatory,
|
||||
/// Truncates based on priority.
|
||||
Ordered { order: usize },
|
||||
}
|
||||
|
||||
impl PartialOrd for PromptPriority {
|
||||
@@ -86,7 +88,6 @@ impl PromptChain {
|
||||
let mut sorted_indices = (0..self.templates.len()).collect::<Vec<_>>();
|
||||
sorted_indices.sort_by_key(|&i| Reverse(&self.templates[i].0));
|
||||
|
||||
// If Truncate
|
||||
let mut tokens_outstanding = if truncate {
|
||||
Some(self.args.model.capacity()? - self.args.reserved_tokens)
|
||||
} else {
|
||||
|
||||
@@ -24,11 +24,9 @@ impl PromptCodeSnippet {
|
||||
|
||||
let language_name = buffer
|
||||
.language()
|
||||
.and_then(|language| Some(language.name().to_string().to_lowercase()));
|
||||
.map(|language| language.name().to_string().to_lowercase());
|
||||
|
||||
let file_path = buffer
|
||||
.file()
|
||||
.and_then(|file| Some(file.path().to_path_buf()));
|
||||
let file_path = buffer.file().map(|file| file.path().to_path_buf());
|
||||
|
||||
(content, language_name, file_path)
|
||||
})?;
|
||||
@@ -46,7 +44,7 @@ impl ToString for PromptCodeSnippet {
|
||||
let path = self
|
||||
.path
|
||||
.as_ref()
|
||||
.and_then(|path| Some(path.to_string_lossy().to_string()))
|
||||
.map(|path| path.to_string_lossy().to_string())
|
||||
.unwrap_or("".to_string());
|
||||
let language_name = self.language_name.clone().unwrap_or("".to_string());
|
||||
let content = self.content.clone();
|
||||
@@ -67,7 +65,7 @@ impl PromptTemplate for RepositoryContext {
|
||||
let template = "You are working inside a large repository, here are a few code snippets that may be useful.";
|
||||
let mut prompt = String::new();
|
||||
|
||||
let mut remaining_tokens = max_token_length.clone();
|
||||
let mut remaining_tokens = max_token_length;
|
||||
let separator_token_length = args.model.count_tokens("\n")?;
|
||||
for snippet in &args.snippets {
|
||||
let mut snippet_prompt = template.to_string();
|
||||
|
||||
@@ -6,4 +6,4 @@ pub use completion::*;
|
||||
pub use embedding::*;
|
||||
pub use model::OpenAiLanguageModel;
|
||||
|
||||
pub const OPEN_AI_API_URL: &'static str = "https://api.openai.com/v1";
|
||||
pub const OPEN_AI_API_URL: &str = "https://api.openai.com/v1";
|
||||
|
||||
@@ -54,6 +54,7 @@ impl LanguageModel for FakeLanguageModel {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FakeEmbeddingProvider {
|
||||
pub embedding_count: AtomicUsize,
|
||||
}
|
||||
@@ -66,14 +67,6 @@ impl Clone for FakeEmbeddingProvider {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FakeEmbeddingProvider {
|
||||
fn default() -> Self {
|
||||
FakeEmbeddingProvider {
|
||||
embedding_count: AtomicUsize::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FakeEmbeddingProvider {
|
||||
pub fn embedding_count(&self) -> usize {
|
||||
self.embedding_count.load(atomic::Ordering::SeqCst)
|
||||
|
||||
@@ -5,6 +5,9 @@ edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
gpui.workspace = true
|
||||
|
||||
@@ -5,6 +5,9 @@ edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/assistant.rs"
|
||||
doctest = false
|
||||
|
||||
@@ -31,9 +31,9 @@ use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
canvas, div, point, relative, rems, uniform_list, Action, AnyElement, AppContext,
|
||||
AsyncAppContext, AsyncWindowContext, AvailableSpace, ClipboardItem, Context, EventEmitter,
|
||||
FocusHandle, FocusableView, FontStyle, FontWeight, HighlightStyle, InteractiveElement,
|
||||
IntoElement, Model, ModelContext, ParentElement, Pixels, PromptLevel, Render, SharedString,
|
||||
AsyncAppContext, AsyncWindowContext, ClipboardItem, Context, EventEmitter, FocusHandle,
|
||||
FocusableView, FontStyle, FontWeight, HighlightStyle, InteractiveElement, IntoElement, Model,
|
||||
ModelContext, ParentElement, Pixels, PromptLevel, Render, SharedString,
|
||||
StatefulInteractiveElement, Styled, Subscription, Task, TextStyle, UniformListScrollHandle,
|
||||
View, ViewContext, VisualContext, WeakModel, WeakView, WhiteSpace, WindowContext,
|
||||
};
|
||||
@@ -652,7 +652,7 @@ impl AssistantPanel {
|
||||
// If Markdown or No Language is Known, increase the randomness for more creative output
|
||||
// If Code, decrease temperature to get more deterministic outputs
|
||||
let temperature = if let Some(language) = language_name.clone() {
|
||||
if language.to_string() != "Markdown".to_string() {
|
||||
if *language != *"Markdown" {
|
||||
0.5
|
||||
} else {
|
||||
1.0
|
||||
@@ -979,7 +979,7 @@ impl AssistantPanel {
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: FontWeight::NORMAL,
|
||||
font_style: FontStyle::Normal,
|
||||
line_height: relative(1.3).into(),
|
||||
line_height: relative(1.3),
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
@@ -1284,25 +1284,25 @@ impl Render for AssistantPanel {
|
||||
let view = cx.view().clone();
|
||||
let scroll_handle = self.saved_conversations_scroll_handle.clone();
|
||||
let conversation_count = self.saved_conversations.len();
|
||||
canvas(move |bounds, cx| {
|
||||
uniform_list(
|
||||
view,
|
||||
"saved_conversations",
|
||||
conversation_count,
|
||||
|this, range, cx| {
|
||||
range
|
||||
.map(|ix| this.render_saved_conversation(ix, cx))
|
||||
.collect()
|
||||
},
|
||||
)
|
||||
.track_scroll(scroll_handle)
|
||||
.into_any_element()
|
||||
.draw(
|
||||
bounds.origin,
|
||||
bounds.size.map(AvailableSpace::Definite),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
canvas(
|
||||
move |bounds, cx| {
|
||||
let mut list = uniform_list(
|
||||
view,
|
||||
"saved_conversations",
|
||||
conversation_count,
|
||||
|this, range, cx| {
|
||||
range
|
||||
.map(|ix| this.render_saved_conversation(ix, cx))
|
||||
.collect()
|
||||
},
|
||||
)
|
||||
.track_scroll(scroll_handle)
|
||||
.into_any_element();
|
||||
list.layout(bounds.origin, bounds.size.into(), cx);
|
||||
list
|
||||
},
|
||||
|_bounds, mut list, cx| list.paint(cx),
|
||||
)
|
||||
.size_full()
|
||||
.into_any_element()
|
||||
}),
|
||||
@@ -1483,7 +1483,7 @@ impl Conversation {
|
||||
max_token_count: tiktoken_rs::model::get_context_size(&model.full_name()),
|
||||
pending_token_count: Task::ready(None),
|
||||
api_url: Some(api_url),
|
||||
model: model.clone(),
|
||||
model,
|
||||
_subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
|
||||
pending_save: Task::ready(Ok(())),
|
||||
path: None,
|
||||
@@ -1527,7 +1527,7 @@ impl Conversation {
|
||||
.as_ref()
|
||||
.map(|summary| summary.text.clone())
|
||||
.unwrap_or_default(),
|
||||
model: self.model.clone(),
|
||||
model: self.model,
|
||||
api_url: self.api_url.clone(),
|
||||
}
|
||||
}
|
||||
@@ -1633,26 +1633,23 @@ impl Conversation {
|
||||
fn count_remaining_tokens(&mut self, cx: &mut ModelContext<Self>) {
|
||||
let messages = self
|
||||
.messages(cx)
|
||||
.into_iter()
|
||||
.filter_map(|message| {
|
||||
Some(tiktoken_rs::ChatCompletionRequestMessage {
|
||||
role: match message.role {
|
||||
Role::User => "user".into(),
|
||||
Role::Assistant => "assistant".into(),
|
||||
Role::System => "system".into(),
|
||||
},
|
||||
content: Some(
|
||||
self.buffer
|
||||
.read(cx)
|
||||
.text_for_range(message.offset_range)
|
||||
.collect(),
|
||||
),
|
||||
name: None,
|
||||
function_call: None,
|
||||
})
|
||||
.map(|message| tiktoken_rs::ChatCompletionRequestMessage {
|
||||
role: match message.role {
|
||||
Role::User => "user".into(),
|
||||
Role::Assistant => "assistant".into(),
|
||||
Role::System => "system".into(),
|
||||
},
|
||||
content: Some(
|
||||
self.buffer
|
||||
.read(cx)
|
||||
.text_for_range(message.offset_range)
|
||||
.collect(),
|
||||
),
|
||||
name: None,
|
||||
function_call: None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let model = self.model.clone();
|
||||
let model = self.model;
|
||||
self.pending_token_count = cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
cx.background_executor()
|
||||
@@ -2416,7 +2413,9 @@ impl ConversationEditor {
|
||||
.read(cx)
|
||||
.messages(cx)
|
||||
.map(|message| BlockProperties {
|
||||
position: buffer.anchor_in_excerpt(excerpt_id, message.anchor),
|
||||
position: buffer
|
||||
.anchor_in_excerpt(excerpt_id, message.anchor)
|
||||
.unwrap(),
|
||||
height: 2,
|
||||
style: BlockStyle::Sticky,
|
||||
render: Arc::new({
|
||||
@@ -2835,6 +2834,7 @@ impl FocusableView for InlineAssistant {
|
||||
}
|
||||
|
||||
impl InlineAssistant {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn new(
|
||||
id: usize,
|
||||
measurements: Rc<Cell<BlockMeasurements>>,
|
||||
@@ -3200,7 +3200,7 @@ impl InlineAssistant {
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: FontWeight::NORMAL,
|
||||
font_style: FontStyle::Normal,
|
||||
line_height: relative(1.3).into(),
|
||||
line_height: relative(1.3),
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
|
||||
@@ -111,9 +111,23 @@ impl AssistantSettings {
|
||||
AiProviderSettings::OpenAi(settings) => {
|
||||
Ok(settings.default_model.unwrap_or(OpenAiModel::FourTurbo))
|
||||
}
|
||||
AiProviderSettings::AzureOpenAi(_settings) => {
|
||||
// TODO: We need to use an Azure OpenAI model here.
|
||||
Ok(OpenAiModel::FourTurbo)
|
||||
AiProviderSettings::AzureOpenAi(settings) => {
|
||||
let deployment_id = settings
|
||||
.deployment_id
|
||||
.as_deref()
|
||||
.ok_or_else(|| anyhow!("no Azure OpenAI deployment ID"))?;
|
||||
|
||||
match deployment_id {
|
||||
// https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models#gpt-4-and-gpt-4-turbo-preview
|
||||
"gpt-4" | "gpt-4-32k" => Ok(OpenAiModel::Four),
|
||||
// https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models#gpt-35
|
||||
"gpt-35-turbo" | "gpt-35-turbo-16k" | "gpt-35-turbo-instruct" => {
|
||||
Ok(OpenAiModel::ThreePointFiveTurbo)
|
||||
}
|
||||
_ => Err(anyhow!(
|
||||
"no matching OpenAI model found for deployment ID: '{deployment_id}'"
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,7 +297,7 @@ fn strip_invalid_spans_from_codeblock(
|
||||
} else if buffer.starts_with("<|")
|
||||
|| buffer.starts_with("<|S")
|
||||
|| buffer.starts_with("<|S|")
|
||||
|| buffer.ends_with("|")
|
||||
|| buffer.ends_with('|')
|
||||
|| buffer.ends_with("|E")
|
||||
|| buffer.ends_with("|E|")
|
||||
{
|
||||
@@ -335,7 +335,7 @@ fn strip_invalid_spans_from_codeblock(
|
||||
.strip_suffix("|E|>")
|
||||
.or_else(|| text.strip_suffix("E|>"))
|
||||
.or_else(|| text.strip_prefix("|>"))
|
||||
.or_else(|| text.strip_prefix(">"))
|
||||
.or_else(|| text.strip_prefix('>'))
|
||||
.unwrap_or(&text)
|
||||
.to_string();
|
||||
};
|
||||
|
||||
@@ -5,6 +5,9 @@ edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/audio.rs"
|
||||
doctest = false
|
||||
|
||||
@@ -5,6 +5,9 @@ edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/auto_update.rs"
|
||||
doctest = false
|
||||
|
||||
@@ -343,8 +343,7 @@ impl AutoUpdater {
|
||||
));
|
||||
cx.update(|cx| {
|
||||
if let Some(param) = ReleaseChannel::try_global(cx)
|
||||
.map(|release_channel| release_channel.release_query_param())
|
||||
.flatten()
|
||||
.and_then(|release_channel| release_channel.release_query_param())
|
||||
{
|
||||
url_string += "&";
|
||||
url_string += param;
|
||||
|
||||
@@ -5,6 +5,9 @@ edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/breadcrumbs.rs"
|
||||
doctest = false
|
||||
|
||||
@@ -5,6 +5,9 @@ edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/call.rs"
|
||||
doctest = false
|
||||
|
||||
@@ -302,7 +302,7 @@ impl ActiveCall {
|
||||
return Task::ready(Ok(()));
|
||||
}
|
||||
|
||||
let room_id = call.room_id.clone();
|
||||
let room_id = call.room_id;
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
let join = self
|
||||
|
||||
@@ -1182,19 +1182,10 @@ impl Room {
|
||||
) -> Task<Result<Model<Project>>> {
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
let role = self.local_participant.role;
|
||||
cx.emit(Event::RemoteProjectJoined { project_id: id });
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
let project = Project::remote(
|
||||
id,
|
||||
client,
|
||||
user_store,
|
||||
language_registry,
|
||||
fs,
|
||||
role,
|
||||
cx.clone(),
|
||||
)
|
||||
.await?;
|
||||
let project =
|
||||
Project::remote(id, client, user_store, language_registry, fs, cx.clone()).await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.joined_projects.retain(|project| {
|
||||
|
||||
@@ -5,6 +5,9 @@ edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/channel.rs"
|
||||
doctest = false
|
||||
|
||||
@@ -11,7 +11,7 @@ pub use channel_chat::{
|
||||
mentions_to_proto, ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId,
|
||||
MessageParams,
|
||||
};
|
||||
pub use channel_store::{Channel, ChannelEvent, ChannelMembership, ChannelStore, HostedProjectId};
|
||||
pub use channel_store::{Channel, ChannelEvent, ChannelMembership, ChannelStore};
|
||||
|
||||
#[cfg(test)]
|
||||
mod channel_store_tests;
|
||||
|
||||
@@ -126,7 +126,7 @@ impl ChannelBuffer {
|
||||
for (_, old_collaborator) in &self.collaborators {
|
||||
if !new_collaborators.contains_key(&old_collaborator.peer_id) {
|
||||
self.buffer.update(cx, |buffer, cx| {
|
||||
buffer.remove_peer(old_collaborator.replica_id as u16, cx)
|
||||
buffer.remove_peer(old_collaborator.replica_id, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -681,7 +681,7 @@ pub fn mentions_to_proto(mentions: &[(Range<usize>, UserId)]) -> Vec<proto::Chat
|
||||
start: range.start as u64,
|
||||
end: range.end as u64,
|
||||
}),
|
||||
user_id: *user_id as u64,
|
||||
user_id: *user_id,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ mod channel_index;
|
||||
use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat, ChannelMessage};
|
||||
use anyhow::{anyhow, Result};
|
||||
use channel_index::ChannelIndex;
|
||||
use client::{ChannelId, Client, Subscription, User, UserId, UserStore};
|
||||
use client::{ChannelId, Client, ClientSettings, ProjectId, Subscription, User, UserId, UserStore};
|
||||
use collections::{hash_map, HashMap, HashSet};
|
||||
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
|
||||
use gpui::{
|
||||
@@ -11,11 +11,11 @@ use gpui::{
|
||||
Task, WeakModel,
|
||||
};
|
||||
use language::Capability;
|
||||
use release_channel::RELEASE_CHANNEL;
|
||||
use rpc::{
|
||||
proto::{self, ChannelRole, ChannelVisibility},
|
||||
TypedEnvelope,
|
||||
};
|
||||
use settings::Settings;
|
||||
use std::{mem, sync::Arc, time::Duration};
|
||||
use util::{async_maybe, maybe, ResultExt};
|
||||
|
||||
@@ -27,10 +27,7 @@ pub fn init(client: &Arc<Client>, user_store: Model<UserStore>, cx: &mut AppCont
|
||||
cx.set_global(GlobalChannelStore(channel_store));
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
pub struct HostedProjectId(pub u64);
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
struct NotesVersion {
|
||||
epoch: u64,
|
||||
version: clock::Global,
|
||||
@@ -38,7 +35,7 @@ struct NotesVersion {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HostedProject {
|
||||
id: HostedProjectId,
|
||||
project_id: ProjectId,
|
||||
channel_id: ChannelId,
|
||||
name: SharedString,
|
||||
_visibility: proto::ChannelVisibility,
|
||||
@@ -47,7 +44,7 @@ pub struct HostedProject {
|
||||
impl From<proto::HostedProject> for HostedProject {
|
||||
fn from(project: proto::HostedProject) -> Self {
|
||||
Self {
|
||||
id: HostedProjectId(project.id),
|
||||
project_id: ProjectId(project.project_id),
|
||||
channel_id: ChannelId(project.channel_id),
|
||||
_visibility: project.visibility(),
|
||||
name: project.name.into(),
|
||||
@@ -60,7 +57,7 @@ pub struct ChannelStore {
|
||||
channel_invitations: Vec<Arc<Channel>>,
|
||||
channel_participants: HashMap<ChannelId, Vec<Arc<User>>>,
|
||||
channel_states: HashMap<ChannelId, ChannelState>,
|
||||
hosted_projects: HashMap<HostedProjectId, HostedProject>,
|
||||
hosted_projects: HashMap<ProjectId, HostedProject>,
|
||||
|
||||
outgoing_invites: HashSet<(ChannelId, UserId)>,
|
||||
update_channels_tx: mpsc::UnboundedSender<proto::UpdateChannels>,
|
||||
@@ -82,27 +79,28 @@ pub struct Channel {
|
||||
pub parent_path: Vec<ChannelId>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Debug)]
|
||||
pub struct ChannelState {
|
||||
latest_chat_message: Option<u64>,
|
||||
latest_notes_versions: Option<NotesVersion>,
|
||||
latest_notes_version: NotesVersion,
|
||||
observed_notes_version: NotesVersion,
|
||||
observed_chat_message: Option<u64>,
|
||||
observed_notes_versions: Option<NotesVersion>,
|
||||
role: Option<ChannelRole>,
|
||||
projects: HashSet<HostedProjectId>,
|
||||
projects: HashSet<ProjectId>,
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
pub fn link(&self) -> String {
|
||||
RELEASE_CHANNEL.link_prefix().to_owned()
|
||||
+ "channel/"
|
||||
+ &Self::slug(&self.name)
|
||||
+ "-"
|
||||
+ &self.id.to_string()
|
||||
pub fn link(&self, cx: &AppContext) -> String {
|
||||
format!(
|
||||
"{}/channel/{}-{}",
|
||||
ClientSettings::get_global(cx).server_url,
|
||||
Self::slug(&self.name),
|
||||
self.id
|
||||
)
|
||||
}
|
||||
|
||||
pub fn notes_link(&self, heading: Option<String>) -> String {
|
||||
self.link()
|
||||
pub fn notes_link(&self, heading: Option<String>, cx: &AppContext) -> String {
|
||||
self.link(cx)
|
||||
+ "/notes"
|
||||
+ &heading
|
||||
.map(|h| format!("#{}", Self::slug(&h)))
|
||||
@@ -305,8 +303,8 @@ impl ChannelStore {
|
||||
self.channel_index.by_id().get(&channel_id)
|
||||
}
|
||||
|
||||
pub fn projects_for_id(&self, channel_id: ChannelId) -> Vec<(SharedString, HostedProjectId)> {
|
||||
let mut projects: Vec<(SharedString, HostedProjectId)> = self
|
||||
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())
|
||||
@@ -592,7 +590,7 @@ impl ChannelStore {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<ChannelId>> {
|
||||
let client = self.client.clone();
|
||||
let name = name.trim_start_matches("#").to_owned();
|
||||
let name = name.trim_start_matches('#').to_owned();
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
let response = client
|
||||
.request(proto::CreateChannel {
|
||||
@@ -839,12 +837,10 @@ impl ChannelStore {
|
||||
Ok(users
|
||||
.into_iter()
|
||||
.zip(response.members)
|
||||
.filter_map(|(user, member)| {
|
||||
Some(ChannelMembership {
|
||||
user,
|
||||
role: member.role(),
|
||||
kind: member.kind(),
|
||||
})
|
||||
.map(|(user, member)| ChannelMembership {
|
||||
user,
|
||||
role: member.role(),
|
||||
kind: member.kind(),
|
||||
})
|
||||
.collect())
|
||||
})
|
||||
@@ -1161,27 +1157,27 @@ impl ChannelStore {
|
||||
let hosted_project: HostedProject = hosted_project.into();
|
||||
if let Some(old_project) = self
|
||||
.hosted_projects
|
||||
.insert(hosted_project.id, hosted_project.clone())
|
||||
.insert(hosted_project.project_id, hosted_project.clone())
|
||||
{
|
||||
self.channel_states
|
||||
.entry(old_project.channel_id)
|
||||
.or_default()
|
||||
.remove_hosted_project(old_project.id);
|
||||
.remove_hosted_project(old_project.project_id);
|
||||
}
|
||||
self.channel_states
|
||||
.entry(hosted_project.channel_id)
|
||||
.or_default()
|
||||
.add_hosted_project(hosted_project.id);
|
||||
.add_hosted_project(hosted_project.project_id);
|
||||
}
|
||||
|
||||
for hosted_project_id in payload.deleted_hosted_projects {
|
||||
let hosted_project_id = HostedProjectId(hosted_project_id);
|
||||
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.id);
|
||||
.remove_hosted_project(old_project.project_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1238,19 +1234,12 @@ impl ChannelState {
|
||||
}
|
||||
|
||||
fn has_channel_buffer_changed(&self) -> bool {
|
||||
if let Some(latest_version) = &self.latest_notes_versions {
|
||||
if let Some(observed_version) = &self.observed_notes_versions {
|
||||
latest_version.epoch > observed_version.epoch
|
||||
|| (latest_version.epoch == observed_version.epoch
|
||||
&& latest_version
|
||||
.version
|
||||
.changed_since(&observed_version.version))
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
self.latest_notes_version.epoch > self.observed_notes_version.epoch
|
||||
|| (self.latest_notes_version.epoch == self.observed_notes_version.epoch
|
||||
&& self
|
||||
.latest_notes_version
|
||||
.version
|
||||
.changed_since(&self.observed_notes_version.version))
|
||||
}
|
||||
|
||||
fn has_new_messages(&self) -> bool {
|
||||
@@ -1277,36 +1266,32 @@ impl ChannelState {
|
||||
}
|
||||
|
||||
fn acknowledge_notes_version(&mut self, epoch: u64, version: &clock::Global) {
|
||||
if let Some(existing) = &mut self.observed_notes_versions {
|
||||
if existing.epoch == epoch {
|
||||
existing.version.join(version);
|
||||
return;
|
||||
}
|
||||
if self.observed_notes_version.epoch == epoch {
|
||||
self.observed_notes_version.version.join(version);
|
||||
} else {
|
||||
self.observed_notes_version = NotesVersion {
|
||||
epoch,
|
||||
version: version.clone(),
|
||||
};
|
||||
}
|
||||
self.observed_notes_versions = Some(NotesVersion {
|
||||
epoch,
|
||||
version: version.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
fn update_latest_notes_version(&mut self, epoch: u64, version: &clock::Global) {
|
||||
if let Some(existing) = &mut self.latest_notes_versions {
|
||||
if existing.epoch == epoch {
|
||||
existing.version.join(version);
|
||||
return;
|
||||
}
|
||||
if self.latest_notes_version.epoch == epoch {
|
||||
self.latest_notes_version.version.join(version);
|
||||
} else {
|
||||
self.latest_notes_version = NotesVersion {
|
||||
epoch,
|
||||
version: version.clone(),
|
||||
};
|
||||
}
|
||||
self.latest_notes_versions = Some(NotesVersion {
|
||||
epoch,
|
||||
version: version.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
fn add_hosted_project(&mut self, project_id: HostedProjectId) {
|
||||
fn add_hosted_project(&mut self, project_id: ProjectId) {
|
||||
self.projects.insert(project_id);
|
||||
}
|
||||
|
||||
fn remove_hosted_project(&mut self, project_id: HostedProjectId) {
|
||||
fn remove_hosted_project(&mut self, project_id: ProjectId) {
|
||||
self.projects.remove(&project_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,9 +97,9 @@ impl<'a> Drop for ChannelPathsInsertGuard<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn channel_path_sorting_key<'a>(
|
||||
fn channel_path_sorting_key(
|
||||
id: ChannelId,
|
||||
channels_by_id: &'a BTreeMap<ChannelId, Arc<Channel>>,
|
||||
channels_by_id: &BTreeMap<ChannelId, Arc<Channel>>,
|
||||
) -> impl Iterator<Item = (&str, ChannelId)> {
|
||||
let (parent_path, name) = channels_by_id
|
||||
.get(&id)
|
||||
|
||||
@@ -5,6 +5,9 @@ edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/cli.rs"
|
||||
doctest = false
|
||||
@@ -15,6 +18,7 @@ path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
# TODO: Use workspace version of `clap`.
|
||||
clap = { version = "3.1", features = ["derive"] }
|
||||
ipc-channel = "0.16"
|
||||
serde.workspace = true
|
||||
|
||||
@@ -5,6 +5,9 @@ edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/client.rs"
|
||||
doctest = false
|
||||
|
||||
@@ -27,7 +27,7 @@ use release_channel::{AppVersion, ReleaseChannel};
|
||||
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json;
|
||||
|
||||
use settings::{Settings, SettingsStore};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
@@ -61,7 +61,7 @@ lazy_static! {
|
||||
pub static ref ZED_APP_PATH: Option<PathBuf> =
|
||||
std::env::var("ZED_APP_PATH").ok().map(PathBuf::from);
|
||||
pub static ref ZED_ALWAYS_ACTIVE: bool =
|
||||
std::env::var("ZED_ALWAYS_ACTIVE").map_or(false, |e| e.len() > 0);
|
||||
std::env::var("ZED_ALWAYS_ACTIVE").map_or(false, |e| !e.is_empty());
|
||||
}
|
||||
|
||||
pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100);
|
||||
@@ -427,7 +427,7 @@ impl Client {
|
||||
http: Arc<HttpClientWithUrl>,
|
||||
cx: &mut AppContext,
|
||||
) -> Arc<Self> {
|
||||
let client = Arc::new(Self {
|
||||
Arc::new(Self {
|
||||
id: AtomicU64::new(0),
|
||||
peer: Peer::new(0),
|
||||
telemetry: Telemetry::new(clock, http.clone(), cx),
|
||||
@@ -438,9 +438,7 @@ impl Client {
|
||||
authenticate: Default::default(),
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
establish_connection: Default::default(),
|
||||
});
|
||||
|
||||
client
|
||||
})
|
||||
}
|
||||
|
||||
pub fn id(&self) -> u64 {
|
||||
@@ -573,17 +571,18 @@ impl Client {
|
||||
let mut state = self.state.write();
|
||||
if state.entities_by_type_and_remote_id.contains_key(&id) {
|
||||
return Err(anyhow!("already subscribed to entity"));
|
||||
} else {
|
||||
state
|
||||
.entities_by_type_and_remote_id
|
||||
.insert(id, WeakSubscriber::Pending(Default::default()));
|
||||
Ok(PendingEntitySubscription {
|
||||
client: self.clone(),
|
||||
remote_id,
|
||||
consumed: false,
|
||||
_entity_type: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
state
|
||||
.entities_by_type_and_remote_id
|
||||
.insert(id, WeakSubscriber::Pending(Default::default()));
|
||||
|
||||
Ok(PendingEntitySubscription {
|
||||
client: self.clone(),
|
||||
remote_id,
|
||||
consumed: false,
|
||||
_entity_type: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
@@ -926,7 +925,7 @@ impl Client {
|
||||
move |cx| async move {
|
||||
match handle_io.await {
|
||||
Ok(()) => {
|
||||
if this.status().borrow().clone()
|
||||
if *this.status().borrow()
|
||||
== (Status::Connected {
|
||||
connection_id,
|
||||
peer_id,
|
||||
@@ -1335,7 +1334,7 @@ impl Client {
|
||||
pending.push(message);
|
||||
return;
|
||||
}
|
||||
Some(weak_subscriber @ _) => match weak_subscriber {
|
||||
Some(weak_subscriber) => match weak_subscriber {
|
||||
WeakSubscriber::Entity { handle } => {
|
||||
subscriber = handle.upgrade();
|
||||
}
|
||||
@@ -1438,21 +1437,29 @@ async fn delete_credentials_from_keychain(cx: &AsyncAppContext) -> Result<()> {
|
||||
.await
|
||||
}
|
||||
|
||||
const WORKTREE_URL_PREFIX: &str = "zed://worktrees/";
|
||||
/// prefix for the zed:// url scheme
|
||||
pub static ZED_URL_SCHEME: &str = "zed";
|
||||
|
||||
pub fn encode_worktree_url(id: u64, access_token: &str) -> String {
|
||||
format!("{}{}/{}", WORKTREE_URL_PREFIX, id, access_token)
|
||||
}
|
||||
|
||||
pub fn decode_worktree_url(url: &str) -> Option<(u64, String)> {
|
||||
let path = url.trim().strip_prefix(WORKTREE_URL_PREFIX)?;
|
||||
let mut parts = path.split('/');
|
||||
let id = parts.next()?.parse::<u64>().ok()?;
|
||||
let access_token = parts.next()?;
|
||||
if access_token.is_empty() {
|
||||
return None;
|
||||
/// Parses the given link into a Zed link.
|
||||
///
|
||||
/// Returns a [`Some`] containing the unprefixed link if the link is a Zed link.
|
||||
/// Returns [`None`] otherwise.
|
||||
pub fn parse_zed_link<'a>(link: &'a str, cx: &AppContext) -> Option<&'a str> {
|
||||
let server_url = &ClientSettings::get_global(cx).server_url;
|
||||
if let Some(stripped) = link
|
||||
.strip_prefix(server_url)
|
||||
.and_then(|result| result.strip_prefix('/'))
|
||||
{
|
||||
return Some(stripped);
|
||||
}
|
||||
Some((id, access_token.to_string()))
|
||||
if let Some(stripped) = link
|
||||
.strip_prefix(ZED_URL_SCHEME)
|
||||
.and_then(|result| result.strip_prefix("://"))
|
||||
{
|
||||
return Some(stripped);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1630,17 +1637,6 @@ mod tests {
|
||||
assert_eq!(*dropped_auth_count.lock(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_and_decode_worktree_url() {
|
||||
let url = encode_worktree_url(5, "deadbeef");
|
||||
assert_eq!(decode_worktree_url(&url), Some((5, "deadbeef".to_string())));
|
||||
assert_eq!(
|
||||
decode_worktree_url(&format!("\n {}\t", url)),
|
||||
Some((5, "deadbeef".to_string()))
|
||||
);
|
||||
assert_eq!(decode_worktree_url("not://the-right-format"), None);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_subscribing_to_entity(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
@@ -84,7 +84,7 @@ impl Telemetry {
|
||||
TelemetrySettings::register(cx);
|
||||
|
||||
let state = Arc::new(Mutex::new(TelemetryState {
|
||||
settings: TelemetrySettings::get_global(cx).clone(),
|
||||
settings: *TelemetrySettings::get_global(cx),
|
||||
app_metadata: cx.app_metadata(),
|
||||
architecture: env::consts::ARCH,
|
||||
release_channel,
|
||||
@@ -119,7 +119,7 @@ impl Telemetry {
|
||||
|
||||
move |cx| {
|
||||
let mut state = state.lock();
|
||||
state.settings = TelemetrySettings::get_global(cx).clone();
|
||||
state.settings = *TelemetrySettings::get_global(cx);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
@@ -168,7 +168,7 @@ impl Telemetry {
|
||||
) {
|
||||
let mut state = self.state.lock();
|
||||
state.installation_id = installation_id.map(|id| id.into());
|
||||
state.session_id = Some(session_id.into());
|
||||
state.session_id = Some(session_id);
|
||||
drop(state);
|
||||
|
||||
let this = self.clone();
|
||||
@@ -387,11 +387,9 @@ impl Telemetry {
|
||||
event,
|
||||
});
|
||||
|
||||
if state.installation_id.is_some() {
|
||||
if state.events_queue.len() >= state.max_queue_size {
|
||||
drop(state);
|
||||
self.flush_events();
|
||||
}
|
||||
if state.installation_id.is_some() && state.events_queue.len() >= state.max_queue_size {
|
||||
drop(state);
|
||||
self.flush_events();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,7 +431,7 @@ impl Telemetry {
|
||||
json_bytes.clear();
|
||||
serde_json::to_writer(&mut json_bytes, event)?;
|
||||
file.write_all(&json_bytes)?;
|
||||
file.write(b"\n")?;
|
||||
file.write_all(b"\n")?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -442,7 +440,7 @@ impl Telemetry {
|
||||
let request_body = EventRequestBody {
|
||||
installation_id: state.installation_id.as_deref().map(Into::into),
|
||||
session_id: state.session_id.clone(),
|
||||
is_staff: state.is_staff.clone(),
|
||||
is_staff: state.is_staff,
|
||||
app_version: state
|
||||
.app_metadata
|
||||
.app_version
|
||||
|
||||
@@ -24,6 +24,9 @@ impl std::fmt::Display for ChannelId {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
pub struct ProjectId(pub u64);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ParticipantIndex(pub u32);
|
||||
|
||||
@@ -653,7 +656,7 @@ impl UserStore {
|
||||
let users = response
|
||||
.users
|
||||
.into_iter()
|
||||
.map(|user| User::new(user))
|
||||
.map(User::new)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
this.update(&mut cx, |this, _| {
|
||||
|
||||
@@ -5,6 +5,9 @@ edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/clock.rs"
|
||||
doctest = false
|
||||
|
||||
@@ -19,5 +19,7 @@ ZED_CLIENT_CHECKSUM_SEED = "development-checksum-seed"
|
||||
# CLICKHOUSE_PASSWORD = ""
|
||||
# CLICKHOUSE_DATABASE = "default"
|
||||
|
||||
# SLACK_PANICS_WEBHOOK = ""
|
||||
|
||||
# RUST_LOG=info
|
||||
# LOG_JSON=true
|
||||
|
||||
@@ -7,23 +7,23 @@ version = "0.44.0"
|
||||
publish = false
|
||||
license = "AGPL-3.0-or-later"
|
||||
|
||||
[features]
|
||||
seed-support = ["reqwest"]
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "collab"
|
||||
|
||||
[[bin]]
|
||||
name = "seed"
|
||||
required-features = ["seed-support"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-tungstenite = "0.16"
|
||||
aws-config = { version = "1.1.5" }
|
||||
aws-sdk-s3 = { version = "1.15.0" }
|
||||
axum = { version = "0.5", features = ["json", "headers", "ws"] }
|
||||
axum-extra = { version = "0.3", features = ["erased-json"] }
|
||||
axum = { version = "0.6", features = ["json", "headers", "ws"] }
|
||||
axum-extra = { version = "0.4", features = ["erased-json"] }
|
||||
base64.workspace = true
|
||||
chrono.workspace = true
|
||||
clock.workspace = true
|
||||
clickhouse.workspace = true
|
||||
@@ -32,8 +32,6 @@ dashmap = "5.4"
|
||||
envy = "0.4.2"
|
||||
futures.workspace = true
|
||||
hex.workspace = true
|
||||
hyper = "0.14"
|
||||
lazy_static.workspace = true
|
||||
live_kit_server.workspace = true
|
||||
log.workspace = true
|
||||
nanoid = "0.4"
|
||||
@@ -41,7 +39,7 @@ parking_lot.workspace = true
|
||||
prometheus = "0.13"
|
||||
prost.workspace = true
|
||||
rand.workspace = true
|
||||
reqwest = { version = "0.11", features = ["json"], optional = true }
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
rpc.workspace = true
|
||||
scrypt = "0.7"
|
||||
sea-orm = { version = "0.12.x", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls", "with-uuid"] }
|
||||
@@ -51,6 +49,8 @@ serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
sha2.workspace = true
|
||||
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "json", "time", "uuid", "any"] }
|
||||
subtle.workspace = true
|
||||
rustc-demangle.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
text.workspace = true
|
||||
time.workspace = true
|
||||
@@ -59,8 +59,7 @@ toml.workspace = true
|
||||
tower = "0.4"
|
||||
tower-http = { workspace = true, features = ["trace"] }
|
||||
tracing = "0.1.34"
|
||||
tracing-log = "0.1.3"
|
||||
tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] }
|
||||
tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json", "registry", "tracing-log"] }
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ metadata:
|
||||
namespace: ${ZED_KUBE_NAMESPACE}
|
||||
name: ${ZED_SERVICE_NAME}
|
||||
annotations:
|
||||
service.beta.kubernetes.io/do-loadbalancer-name: "${ZED_SERVICE_NAME}-${ZED_KUBE_NAMESPACE}"
|
||||
service.beta.kubernetes.io/do-loadbalancer-size-unit: "${ZED_LOAD_BALANCER_SIZE_UNIT}"
|
||||
service.beta.kubernetes.io/do-loadbalancer-tls-ports: "443"
|
||||
service.beta.kubernetes.io/do-loadbalancer-certificate-id: ${ZED_DO_CERTIFICATE_ID}
|
||||
service.beta.kubernetes.io/do-loadbalancer-disable-lets-encrypt-dns-records: "true"
|
||||
@@ -33,6 +35,11 @@ metadata:
|
||||
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxSurge: 1
|
||||
maxUnavailable: 0
|
||||
selector:
|
||||
matchLabels:
|
||||
app: ${ZED_SERVICE_NAME}
|
||||
@@ -76,6 +83,13 @@ spec:
|
||||
port: 8080
|
||||
initialDelaySeconds: 1
|
||||
periodSeconds: 1
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 8080
|
||||
initialDelaySeconds: 1
|
||||
periodSeconds: 1
|
||||
failureThreshold: 15
|
||||
env:
|
||||
- name: HTTP_PORT
|
||||
value: "8080"
|
||||
@@ -156,6 +170,11 @@ spec:
|
||||
secretKeyRef:
|
||||
name: clickhouse
|
||||
key: database
|
||||
- name: SLACK_PANICS_WEBHOOK
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: slack
|
||||
key: panics_webhook
|
||||
- name: INVITE_LINK_PREFIX
|
||||
value: ${INVITE_LINK_PREFIX}
|
||||
- name: RUST_BACKTRACE
|
||||
@@ -171,3 +190,4 @@ spec:
|
||||
# FIXME - Switch to the more restrictive `PERFMON` capability.
|
||||
# This capability isn't yet available in a stable version of Debian.
|
||||
add: ["SYS_ADMIN"]
|
||||
terminationGracePeriodSeconds: 10
|
||||
|
||||
@@ -5,6 +5,7 @@ metadata:
|
||||
namespace: ${ZED_KUBE_NAMESPACE}
|
||||
name: postgrest
|
||||
annotations:
|
||||
service.beta.kubernetes.io/do-loadbalancer-name: "postgrest-${ZED_KUBE_NAMESPACE}"
|
||||
service.beta.kubernetes.io/do-loadbalancer-tls-ports: "443"
|
||||
service.beta.kubernetes.io/do-loadbalancer-certificate-id: ${ZED_DO_CERTIFICATE_ID}
|
||||
service.beta.kubernetes.io/do-loadbalancer-disable-lets-encrypt-dns-records: "true"
|
||||
|
||||
@@ -46,10 +46,11 @@ CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id");
|
||||
CREATE TABLE "projects" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"room_id" INTEGER REFERENCES rooms (id) ON DELETE CASCADE NOT NULL,
|
||||
"host_user_id" INTEGER REFERENCES users (id) NOT NULL,
|
||||
"host_user_id" INTEGER REFERENCES users (id),
|
||||
"host_connection_id" INTEGER,
|
||||
"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)
|
||||
);
|
||||
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");
|
||||
@@ -247,7 +248,10 @@ CREATE UNIQUE INDEX "index_channel_members_on_channel_id_and_user_id" ON "channe
|
||||
CREATE TABLE "buffers" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
|
||||
"epoch" INTEGER NOT NULL DEFAULT 0
|
||||
"epoch" INTEGER NOT NULL DEFAULT 0,
|
||||
"latest_operation_epoch" INTEGER,
|
||||
"latest_operation_replica_id" INTEGER,
|
||||
"latest_operation_lamport_timestamp" INTEGER
|
||||
);
|
||||
|
||||
CREATE INDEX "index_buffers_on_channel_id" ON "buffers" ("channel_id");
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Add migration script here
|
||||
ALTER TABLE projects ALTER COLUMN host_user_id DROP NOT NULL;
|
||||
ALTER TABLE projects ADD COLUMN hosted_project_id INTEGER REFERENCES hosted_projects(id) UNIQUE NULL;
|
||||
@@ -0,0 +1,17 @@
|
||||
-- Add migration script here
|
||||
|
||||
ALTER TABLE buffers ADD COLUMN latest_operation_epoch INTEGER;
|
||||
ALTER TABLE buffers ADD COLUMN latest_operation_lamport_timestamp INTEGER;
|
||||
ALTER TABLE buffers ADD COLUMN latest_operation_replica_id INTEGER;
|
||||
|
||||
WITH ops AS (
|
||||
SELECT DISTINCT ON (buffer_id) buffer_id, epoch, lamport_timestamp, replica_id
|
||||
FROM buffer_operations
|
||||
ORDER BY buffer_id, epoch DESC, lamport_timestamp DESC, replica_id DESC
|
||||
)
|
||||
UPDATE buffers
|
||||
SET latest_operation_epoch = ops.epoch,
|
||||
latest_operation_lamport_timestamp = ops.lamport_timestamp,
|
||||
latest_operation_replica_id = ops.replica_id
|
||||
FROM ops
|
||||
WHERE buffers.id = ops.buffer_id;
|
||||
@@ -1,5 +1,7 @@
|
||||
pub mod events;
|
||||
pub mod extensions;
|
||||
pub mod ips_file;
|
||||
pub mod slack;
|
||||
|
||||
use crate::{
|
||||
auth,
|
||||
@@ -9,7 +11,7 @@ use crate::{
|
||||
use anyhow::anyhow;
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::{Path, Query},
|
||||
extract::{self, Path, Query},
|
||||
http::{self, Request, StatusCode},
|
||||
middleware::{self, Next},
|
||||
response::IntoResponse,
|
||||
@@ -21,15 +23,13 @@ use chrono::SecondsFormat;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tower::ServiceBuilder;
|
||||
use tracing::instrument;
|
||||
|
||||
pub use extensions::fetch_extensions_from_blob_store_periodically;
|
||||
|
||||
pub fn routes(rpc_server: Option<Arc<rpc::Server>>, state: Arc<AppState>) -> Router<Body> {
|
||||
pub fn routes(rpc_server: Option<Arc<rpc::Server>>, state: Arc<AppState>) -> Router<(), Body> {
|
||||
Router::new()
|
||||
.route("/user", get(get_authenticated_user))
|
||||
.route("/users/:id/access_tokens", post(create_access_token))
|
||||
.route("/panic", post(trace_panic))
|
||||
.route("/rpc_server_snapshot", get(get_rpc_server_snapshot))
|
||||
.route("/contributors", get(get_contributors).post(add_contributor))
|
||||
.route("/contributor", get(check_is_contributor))
|
||||
@@ -120,20 +120,6 @@ struct CreateUserResponse {
|
||||
metrics_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Panic {
|
||||
version: String,
|
||||
release_channel: String,
|
||||
backtrace_hash: String,
|
||||
text: String,
|
||||
}
|
||||
|
||||
#[instrument(skip(panic))]
|
||||
async fn trace_panic(panic: Json<Panic>) -> Result<()> {
|
||||
tracing::error!(version = %panic.version, release_channel = %panic.release_channel, backtrace_hash = %panic.backtrace_hash, text = %panic.text, "panic report");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_rpc_server_snapshot(
|
||||
Extension(rpc_server): Extension<Option<Arc<rpc::Server>>>,
|
||||
) -> Result<ErasedJson> {
|
||||
@@ -190,17 +176,16 @@ async fn check_is_contributor(
|
||||
}
|
||||
|
||||
async fn add_contributor(
|
||||
Json(params): Json<AuthenticatedUserParams>,
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
extract::Json(params): extract::Json<AuthenticatedUserParams>,
|
||||
) -> Result<()> {
|
||||
Ok(app
|
||||
.db
|
||||
app.db
|
||||
.add_contributor(
|
||||
¶ms.github_login,
|
||||
params.github_user_id,
|
||||
params.github_email.as_deref(),
|
||||
)
|
||||
.await?)
|
||||
.await
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
||||
@@ -1,34 +1,38 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use aws_sdk_s3::primitives::ByteStream;
|
||||
use axum::{
|
||||
body::Bytes, headers::Header, http::HeaderName, routing::post, Extension, Router, TypedHeader,
|
||||
body::Bytes,
|
||||
headers::Header,
|
||||
http::{HeaderMap, HeaderName, StatusCode},
|
||||
routing::post,
|
||||
Extension, Router, TypedHeader,
|
||||
};
|
||||
use hyper::StatusCode;
|
||||
use lazy_static::lazy_static;
|
||||
use serde::{Serialize, Serializer};
|
||||
use sha2::{Digest, Sha256};
|
||||
use telemetry_events::{
|
||||
ActionEvent, AppEvent, AssistantEvent, CallEvent, CopilotEvent, CpuEvent, EditEvent,
|
||||
EditorEvent, Event, EventRequestBody, EventWrapper, MemoryEvent, SettingEvent,
|
||||
};
|
||||
use util::SemanticVersion;
|
||||
|
||||
use crate::{AppState, Error, Result};
|
||||
use crate::{api::slack, AppState, Error, Result};
|
||||
|
||||
use super::ips_file::IpsFile;
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new().route("/telemetry/events", post(post_events))
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref ZED_CHECKSUM_HEADER: HeaderName = HeaderName::from_static("x-zed-checksum");
|
||||
static ref CLOUDFLARE_IP_COUNTRY_HEADER: HeaderName = HeaderName::from_static("cf-ipcountry");
|
||||
Router::new()
|
||||
.route("/telemetry/events", post(post_events))
|
||||
.route("/telemetry/crashes", post(post_crash))
|
||||
}
|
||||
|
||||
pub struct ZedChecksumHeader(Vec<u8>);
|
||||
|
||||
impl Header for ZedChecksumHeader {
|
||||
fn name() -> &'static HeaderName {
|
||||
&ZED_CHECKSUM_HEADER
|
||||
static ZED_CHECKSUM_HEADER: OnceLock<HeaderName> = OnceLock::new();
|
||||
ZED_CHECKSUM_HEADER.get_or_init(|| HeaderName::from_static("x-zed-checksum"))
|
||||
}
|
||||
|
||||
fn decode<'i, I>(values: &mut I) -> Result<Self, axum::headers::Error>
|
||||
@@ -55,7 +59,8 @@ pub struct CloudflareIpCountryHeader(String);
|
||||
|
||||
impl Header for CloudflareIpCountryHeader {
|
||||
fn name() -> &'static HeaderName {
|
||||
&CLOUDFLARE_IP_COUNTRY_HEADER
|
||||
static CLOUDFLARE_IP_COUNTRY_HEADER: OnceLock<HeaderName> = OnceLock::new();
|
||||
CLOUDFLARE_IP_COUNTRY_HEADER.get_or_init(|| HeaderName::from_static("cf-ipcountry"))
|
||||
}
|
||||
|
||||
fn decode<'i, I>(values: &mut I) -> Result<Self, axum::headers::Error>
|
||||
@@ -77,6 +82,140 @@ impl Header for CloudflareIpCountryHeader {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn post_crash(
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
body: Bytes,
|
||||
) -> Result<()> {
|
||||
static CRASH_REPORTS_BUCKET: &str = "zed-crash-reports";
|
||||
|
||||
let report = IpsFile::parse(&body)?;
|
||||
let version_threshold = SemanticVersion::new(0, 123, 0);
|
||||
|
||||
let bundle_id = &report.header.bundle_id;
|
||||
let app_version = &report.app_version();
|
||||
|
||||
if bundle_id == "dev.zed.Zed-Dev" {
|
||||
log::error!("Crash uploads from {} are ignored.", bundle_id);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if app_version.is_none() || app_version.unwrap() < version_threshold {
|
||||
log::error!(
|
||||
"Crash uploads from {} are ignored.",
|
||||
report.header.app_version
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
let app_version = app_version.unwrap();
|
||||
|
||||
if let Some(blob_store_client) = app.blob_store_client.as_ref() {
|
||||
let response = blob_store_client
|
||||
.head_object()
|
||||
.bucket(CRASH_REPORTS_BUCKET)
|
||||
.key(report.header.incident_id.clone() + ".ips")
|
||||
.send()
|
||||
.await;
|
||||
|
||||
if response.is_ok() {
|
||||
log::info!("We've already uploaded this crash");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
blob_store_client
|
||||
.put_object()
|
||||
.bucket(CRASH_REPORTS_BUCKET)
|
||||
.key(report.header.incident_id.clone() + ".ips")
|
||||
.acl(aws_sdk_s3::types::ObjectCannedAcl::PublicRead)
|
||||
.body(ByteStream::from(body.to_vec()))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| log::error!("Failed to upload crash: {}", e))
|
||||
.ok();
|
||||
}
|
||||
|
||||
let recent_panic_on: Option<i64> = headers
|
||||
.get("x-zed-panicked-on")
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.and_then(|s| s.parse().ok());
|
||||
let mut recent_panic = None;
|
||||
|
||||
if let Some(recent_panic_on) = recent_panic_on {
|
||||
let crashed_at = match report.timestamp() {
|
||||
Ok(t) => Some(t),
|
||||
Err(e) => {
|
||||
log::error!("Can't parse {}: {}", report.header.timestamp, e);
|
||||
None
|
||||
}
|
||||
};
|
||||
if crashed_at.is_some_and(|t| (t.timestamp_millis() - recent_panic_on).abs() <= 30000) {
|
||||
recent_panic = headers.get("x-zed-panic").and_then(|h| h.to_str().ok());
|
||||
}
|
||||
}
|
||||
|
||||
let description = report.description(recent_panic);
|
||||
let summary = report.backtrace_summary();
|
||||
|
||||
tracing::error!(
|
||||
service = "client",
|
||||
version = %report.header.app_version,
|
||||
os_version = %report.header.os_version,
|
||||
bundle_id = %report.header.bundle_id,
|
||||
incident_id = %report.header.incident_id,
|
||||
description = %description,
|
||||
backtrace = %summary,
|
||||
"crash report");
|
||||
|
||||
if let Some(slack_panics_webhook) = app.config.slack_panics_webhook.clone() {
|
||||
let payload = slack::WebhookBody::new(|w| {
|
||||
w.add_section(|s| s.text(slack::Text::markdown(description)))
|
||||
.add_section(|s| {
|
||||
s.add_field(slack::Text::markdown(format!(
|
||||
"*Version:*\n{} ({})",
|
||||
bundle_id, app_version
|
||||
)))
|
||||
.add_field({
|
||||
let hostname = app.config.blob_store_url.clone().unwrap_or_default();
|
||||
let hostname = hostname.strip_prefix("https://").unwrap_or_else(|| {
|
||||
hostname.strip_prefix("http://").unwrap_or_default()
|
||||
});
|
||||
|
||||
slack::Text::markdown(format!(
|
||||
"*Incident:*\n<https://{}.{}/{}.ips|{}…>",
|
||||
CRASH_REPORTS_BUCKET,
|
||||
hostname,
|
||||
report.header.incident_id,
|
||||
report
|
||||
.header
|
||||
.incident_id
|
||||
.chars()
|
||||
.take(8)
|
||||
.collect::<String>(),
|
||||
))
|
||||
})
|
||||
})
|
||||
.add_rich_text(|r| r.add_preformatted(|p| p.add_text(summary)))
|
||||
});
|
||||
let payload_json = serde_json::to_string(&payload).map_err(|err| {
|
||||
log::error!("Failed to serialize payload to JSON: {err}");
|
||||
Error::Internal(anyhow!(err))
|
||||
})?;
|
||||
|
||||
reqwest::Client::new()
|
||||
.post(slack_panics_webhook)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(payload_json)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
log::error!("Failed to send payload to Slack: {err}");
|
||||
Error::Internal(anyhow!(err))
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn post_events(
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
TypedHeader(ZedChecksumHeader(checksum)): TypedHeader<ZedChecksumHeader>,
|
||||
@@ -102,7 +241,7 @@ pub async fn post_events(
|
||||
summer.update(&body);
|
||||
summer.update(checksum_seed);
|
||||
|
||||
if &checksum[..] != &summer.finalize()[..] {
|
||||
if &checksum != &summer.finalize()[..] {
|
||||
return Err(Error::Http(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"invalid checksum".into(),
|
||||
@@ -219,40 +358,68 @@ struct ToUpload {
|
||||
|
||||
impl ToUpload {
|
||||
pub async fn upload(&self, clickhouse_client: &clickhouse::Client) -> anyhow::Result<()> {
|
||||
Self::upload_to_table("editor_events", &self.editor_events, clickhouse_client)
|
||||
const EDITOR_EVENTS_TABLE: &str = "editor_events";
|
||||
Self::upload_to_table(EDITOR_EVENTS_TABLE, &self.editor_events, clickhouse_client)
|
||||
.await
|
||||
.with_context(|| format!("failed to upload to table 'editor_events'"))?;
|
||||
Self::upload_to_table("copilot_events", &self.copilot_events, clickhouse_client)
|
||||
.await
|
||||
.with_context(|| format!("failed to upload to table 'copilot_events'"))?;
|
||||
.with_context(|| format!("failed to upload to table '{EDITOR_EVENTS_TABLE}'"))?;
|
||||
|
||||
const COPILOT_EVENTS_TABLE: &str = "copilot_events";
|
||||
Self::upload_to_table(
|
||||
"assistant_events",
|
||||
COPILOT_EVENTS_TABLE,
|
||||
&self.copilot_events,
|
||||
clickhouse_client,
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("failed to upload to table '{COPILOT_EVENTS_TABLE}'"))?;
|
||||
|
||||
const ASSISTANT_EVENTS_TABLE: &str = "assistant_events";
|
||||
Self::upload_to_table(
|
||||
ASSISTANT_EVENTS_TABLE,
|
||||
&self.assistant_events,
|
||||
clickhouse_client,
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("failed to upload to table 'assistant_events'"))?;
|
||||
Self::upload_to_table("call_events", &self.call_events, clickhouse_client)
|
||||
.with_context(|| format!("failed to upload to table '{ASSISTANT_EVENTS_TABLE}'"))?;
|
||||
|
||||
const CALL_EVENTS_TABLE: &str = "call_events";
|
||||
Self::upload_to_table(CALL_EVENTS_TABLE, &self.call_events, clickhouse_client)
|
||||
.await
|
||||
.with_context(|| format!("failed to upload to table 'call_events'"))?;
|
||||
Self::upload_to_table("cpu_events", &self.cpu_events, clickhouse_client)
|
||||
.with_context(|| format!("failed to upload to table '{CALL_EVENTS_TABLE}'"))?;
|
||||
|
||||
const CPU_EVENTS_TABLE: &str = "cpu_events";
|
||||
Self::upload_to_table(CPU_EVENTS_TABLE, &self.cpu_events, clickhouse_client)
|
||||
.await
|
||||
.with_context(|| format!("failed to upload to table 'cpu_events'"))?;
|
||||
Self::upload_to_table("memory_events", &self.memory_events, clickhouse_client)
|
||||
.with_context(|| format!("failed to upload to table '{CPU_EVENTS_TABLE}'"))?;
|
||||
|
||||
const MEMORY_EVENTS_TABLE: &str = "memory_events";
|
||||
Self::upload_to_table(MEMORY_EVENTS_TABLE, &self.memory_events, clickhouse_client)
|
||||
.await
|
||||
.with_context(|| format!("failed to upload to table 'memory_events'"))?;
|
||||
Self::upload_to_table("app_events", &self.app_events, clickhouse_client)
|
||||
.with_context(|| format!("failed to upload to table '{MEMORY_EVENTS_TABLE}'"))?;
|
||||
|
||||
const APP_EVENTS_TABLE: &str = "app_events";
|
||||
Self::upload_to_table(APP_EVENTS_TABLE, &self.app_events, clickhouse_client)
|
||||
.await
|
||||
.with_context(|| format!("failed to upload to table 'app_events'"))?;
|
||||
Self::upload_to_table("setting_events", &self.setting_events, clickhouse_client)
|
||||
.with_context(|| format!("failed to upload to table '{APP_EVENTS_TABLE}'"))?;
|
||||
|
||||
const SETTING_EVENTS_TABLE: &str = "setting_events";
|
||||
Self::upload_to_table(
|
||||
SETTING_EVENTS_TABLE,
|
||||
&self.setting_events,
|
||||
clickhouse_client,
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("failed to upload to table '{SETTING_EVENTS_TABLE}'"))?;
|
||||
|
||||
const EDIT_EVENTS_TABLE: &str = "edit_events";
|
||||
Self::upload_to_table(EDIT_EVENTS_TABLE, &self.edit_events, clickhouse_client)
|
||||
.await
|
||||
.with_context(|| format!("failed to upload to table 'setting_events'"))?;
|
||||
Self::upload_to_table("edit_events", &self.edit_events, clickhouse_client)
|
||||
.with_context(|| format!("failed to upload to table '{EDIT_EVENTS_TABLE}'"))?;
|
||||
|
||||
const ACTION_EVENTS_TABLE: &str = "action_events";
|
||||
Self::upload_to_table(ACTION_EVENTS_TABLE, &self.action_events, clickhouse_client)
|
||||
.await
|
||||
.with_context(|| format!("failed to upload to table 'edit_events'"))?;
|
||||
Self::upload_to_table("action_events", &self.action_events, clickhouse_client)
|
||||
.await
|
||||
.with_context(|| format!("failed to upload to table 'action_events'"))?;
|
||||
.with_context(|| format!("failed to upload to table '{ACTION_EVENTS_TABLE}'"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,12 @@ use anyhow::{anyhow, Context as _};
|
||||
use aws_sdk_s3::presigning::PresigningConfig;
|
||||
use axum::{
|
||||
extract::{Path, Query},
|
||||
http::StatusCode,
|
||||
response::Redirect,
|
||||
routing::get,
|
||||
Extension, Json, Router,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use hyper::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use time::PrimitiveDateTime;
|
||||
@@ -147,9 +147,7 @@ async fn fetch_extensions_from_blob_store(
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let objects = list
|
||||
.contents
|
||||
.ok_or_else(|| anyhow!("missing bucket contents"))?;
|
||||
let objects = list.contents.unwrap_or_default();
|
||||
|
||||
let mut published_versions = HashMap::<&str, Vec<&str>>::default();
|
||||
for object in &objects {
|
||||
|
||||
352
crates/collab/src/api/ips_file.rs
Normal file
352
crates/collab/src/api/ips_file.rs
Normal file
@@ -0,0 +1,352 @@
|
||||
use collections::HashMap;
|
||||
|
||||
use serde_derive::Deserialize;
|
||||
use serde_derive::Serialize;
|
||||
use serde_json::Value;
|
||||
use util::SemanticVersion;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct IpsFile {
|
||||
pub header: Header,
|
||||
pub body: Body,
|
||||
}
|
||||
|
||||
impl IpsFile {
|
||||
pub fn parse(bytes: &[u8]) -> anyhow::Result<IpsFile> {
|
||||
let mut split = bytes.splitn(2, |&b| b == b'\n');
|
||||
let header_bytes = split
|
||||
.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("No header found"))?;
|
||||
let header: Header = serde_json::from_slice(header_bytes)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to parse header: {}", e))?;
|
||||
|
||||
let body_bytes = split
|
||||
.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("No body found"))?;
|
||||
|
||||
let body: Body = serde_json::from_slice(body_bytes)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to parse body: {}", e))?;
|
||||
Ok(IpsFile { header, body })
|
||||
}
|
||||
|
||||
pub fn faulting_thread(&self) -> Option<&Thread> {
|
||||
self.body.threads.get(self.body.faulting_thread? as usize)
|
||||
}
|
||||
|
||||
pub fn app_version(&self) -> Option<SemanticVersion> {
|
||||
self.header.app_version.parse().ok()
|
||||
}
|
||||
|
||||
pub fn timestamp(&self) -> anyhow::Result<chrono::DateTime<chrono::FixedOffset>> {
|
||||
chrono::DateTime::parse_from_str(&self.header.timestamp, "%Y-%m-%d %H:%M:%S%.f %#z")
|
||||
.map_err(|e| anyhow::anyhow!(e))
|
||||
}
|
||||
|
||||
pub fn description(&self, panic: Option<&str>) -> String {
|
||||
let mut desc = if self.body.termination.indicator == "Abort trap: 6" {
|
||||
match panic {
|
||||
Some(panic_message) => format!("Panic `{}`", panic_message),
|
||||
None => "Crash `Abort trap: 6` (possible panic)".into(),
|
||||
}
|
||||
} else if let Some(msg) = &self.body.exception.message {
|
||||
format!("Exception `{}`", msg)
|
||||
} else {
|
||||
format!("Crash `{}`", self.body.termination.indicator)
|
||||
};
|
||||
if let Some(thread) = self.faulting_thread() {
|
||||
if let Some(queue) = thread.queue.as_ref() {
|
||||
desc += &format!(
|
||||
" on thread {} ({})",
|
||||
self.body.faulting_thread.unwrap_or_default(),
|
||||
queue
|
||||
);
|
||||
} else {
|
||||
desc += &format!(
|
||||
" on thread {} ({})",
|
||||
self.body.faulting_thread.unwrap_or_default(),
|
||||
thread.name.clone().unwrap_or_default()
|
||||
);
|
||||
}
|
||||
}
|
||||
desc
|
||||
}
|
||||
|
||||
pub fn backtrace_summary(&self) -> String {
|
||||
if let Some(thread) = self.faulting_thread() {
|
||||
let mut frames = thread
|
||||
.frames
|
||||
.iter()
|
||||
.filter_map(|frame| {
|
||||
if let Some(name) = &frame.symbol {
|
||||
if self.is_ignorable_frame(name) {
|
||||
return None;
|
||||
}
|
||||
Some(format!("{:#}", rustc_demangle::demangle(name)))
|
||||
} else if let Some(image) = self.body.used_images.get(frame.image_index) {
|
||||
Some(image.name.clone().unwrap_or("<unknown-image>".into()))
|
||||
} else {
|
||||
Some("<unknown>".into())
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let total = frames.len();
|
||||
if total > 21 {
|
||||
frames = frames.into_iter().take(20).collect();
|
||||
frames.push(format!(" and {} more...", total - 20))
|
||||
}
|
||||
frames.join("\n")
|
||||
} else {
|
||||
"<no backtrace available>".into()
|
||||
}
|
||||
}
|
||||
|
||||
fn is_ignorable_frame(&self, symbol: &String) -> bool {
|
||||
[
|
||||
"pthread_kill",
|
||||
"panic",
|
||||
"backtrace",
|
||||
"rust_begin_unwind",
|
||||
"abort",
|
||||
]
|
||||
.iter()
|
||||
.any(|s| symbol.contains(s))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct Header {
|
||||
pub app_name: String,
|
||||
pub timestamp: String,
|
||||
pub app_version: String,
|
||||
pub slice_uuid: String,
|
||||
pub build_version: String,
|
||||
pub platform: i64,
|
||||
#[serde(rename = "bundleID", default)]
|
||||
pub bundle_id: String,
|
||||
pub share_with_app_devs: i64,
|
||||
pub is_first_party: i64,
|
||||
pub bug_type: String,
|
||||
pub os_version: String,
|
||||
pub roots_installed: i64,
|
||||
pub name: String,
|
||||
pub incident_id: String,
|
||||
}
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
pub struct Body {
|
||||
pub uptime: i64,
|
||||
pub proc_role: String,
|
||||
pub version: i64,
|
||||
#[serde(rename = "userID")]
|
||||
pub user_id: i64,
|
||||
pub deploy_version: i64,
|
||||
pub model_code: String,
|
||||
#[serde(rename = "coalitionID")]
|
||||
pub coalition_id: i64,
|
||||
pub os_version: OsVersion,
|
||||
pub capture_time: String,
|
||||
pub code_signing_monitor: i64,
|
||||
pub incident: String,
|
||||
pub pid: i64,
|
||||
pub translated: bool,
|
||||
pub cpu_type: String,
|
||||
#[serde(rename = "roots_installed")]
|
||||
pub roots_installed: i64,
|
||||
#[serde(rename = "bug_type")]
|
||||
pub bug_type: String,
|
||||
pub proc_launch: String,
|
||||
pub proc_start_abs_time: i64,
|
||||
pub proc_exit_abs_time: i64,
|
||||
pub proc_name: String,
|
||||
pub proc_path: String,
|
||||
pub bundle_info: BundleInfo,
|
||||
pub store_info: StoreInfo,
|
||||
pub parent_proc: String,
|
||||
pub parent_pid: i64,
|
||||
pub coalition_name: String,
|
||||
pub crash_reporter_key: String,
|
||||
#[serde(rename = "codeSigningID")]
|
||||
pub code_signing_id: String,
|
||||
#[serde(rename = "codeSigningTeamID")]
|
||||
pub code_signing_team_id: String,
|
||||
pub code_signing_flags: i64,
|
||||
pub code_signing_validation_category: i64,
|
||||
pub code_signing_trust_level: i64,
|
||||
pub instruction_byte_stream: InstructionByteStream,
|
||||
pub sip: String,
|
||||
pub exception: Exception,
|
||||
pub termination: Termination,
|
||||
pub asi: Asi,
|
||||
pub ext_mods: ExtMods,
|
||||
pub faulting_thread: Option<i64>,
|
||||
pub threads: Vec<Thread>,
|
||||
pub used_images: Vec<UsedImage>,
|
||||
pub shared_cache: SharedCache,
|
||||
pub vm_summary: String,
|
||||
pub legacy_info: LegacyInfo,
|
||||
pub log_writing_signature: String,
|
||||
pub trial_info: TrialInfo,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
pub struct OsVersion {
|
||||
pub train: String,
|
||||
pub build: String,
|
||||
pub release_type: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
pub struct BundleInfo {
|
||||
#[serde(rename = "CFBundleShortVersionString")]
|
||||
pub cfbundle_short_version_string: String,
|
||||
#[serde(rename = "CFBundleVersion")]
|
||||
pub cfbundle_version: String,
|
||||
#[serde(rename = "CFBundleIdentifier")]
|
||||
pub cfbundle_identifier: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
pub struct StoreInfo {
|
||||
pub device_identifier_for_vendor: String,
|
||||
pub third_party: bool,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
pub struct InstructionByteStream {
|
||||
#[serde(rename = "beforePC")]
|
||||
pub before_pc: String,
|
||||
#[serde(rename = "atPC")]
|
||||
pub at_pc: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
pub struct Exception {
|
||||
pub codes: String,
|
||||
pub raw_codes: Vec<i64>,
|
||||
#[serde(rename = "type")]
|
||||
pub type_field: String,
|
||||
pub subtype: Option<String>,
|
||||
pub signal: String,
|
||||
pub port: Option<i64>,
|
||||
pub guard_id: Option<i64>,
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
pub struct Termination {
|
||||
pub flags: i64,
|
||||
pub code: i64,
|
||||
pub namespace: String,
|
||||
pub indicator: String,
|
||||
pub by_proc: String,
|
||||
pub by_pid: i64,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
pub struct Asi {
|
||||
#[serde(rename = "libsystem_c.dylib")]
|
||||
pub libsystem_c_dylib: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
pub struct ExtMods {
|
||||
pub caller: ExtMod,
|
||||
pub system: ExtMod,
|
||||
pub targeted: ExtMod,
|
||||
pub warnings: i64,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
pub struct ExtMod {
|
||||
#[serde(rename = "thread_create")]
|
||||
pub thread_create: i64,
|
||||
#[serde(rename = "thread_set_state")]
|
||||
pub thread_set_state: i64,
|
||||
#[serde(rename = "task_for_pid")]
|
||||
pub task_for_pid: i64,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
pub struct Thread {
|
||||
pub thread_state: HashMap<String, Value>,
|
||||
pub id: i64,
|
||||
pub triggered: Option<bool>,
|
||||
pub name: Option<String>,
|
||||
pub queue: Option<String>,
|
||||
pub frames: Vec<Frame>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
pub struct Frame {
|
||||
pub image_offset: i64,
|
||||
pub symbol: Option<String>,
|
||||
pub symbol_location: Option<i64>,
|
||||
pub image_index: usize,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
pub struct UsedImage {
|
||||
pub source: String,
|
||||
pub arch: Option<String>,
|
||||
pub base: i64,
|
||||
#[serde(rename = "CFBundleShortVersionString")]
|
||||
pub cfbundle_short_version_string: Option<String>,
|
||||
#[serde(rename = "CFBundleIdentifier")]
|
||||
pub cfbundle_identifier: Option<String>,
|
||||
pub size: i64,
|
||||
pub uuid: String,
|
||||
pub path: Option<String>,
|
||||
pub name: Option<String>,
|
||||
#[serde(rename = "CFBundleVersion")]
|
||||
pub cfbundle_version: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
pub struct SharedCache {
|
||||
pub base: i64,
|
||||
pub size: i64,
|
||||
pub uuid: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
pub struct LegacyInfo {
|
||||
pub thread_triggered: ThreadTriggered,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
pub struct ThreadTriggered {
|
||||
pub name: String,
|
||||
pub queue: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
pub struct TrialInfo {
|
||||
pub rollouts: Vec<Rollout>,
|
||||
pub experiments: Vec<Value>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
pub struct Rollout {
|
||||
pub rollout_id: String,
|
||||
pub factor_pack_ids: HashMap<String, Value>,
|
||||
pub deployment_id: i64,
|
||||
}
|
||||
144
crates/collab/src/api/slack.rs
Normal file
144
crates/collab/src/api/slack.rs
Normal file
@@ -0,0 +1,144 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// https://api.slack.com/reference/messaging/payload
|
||||
#[derive(Default, Clone, Serialize, Deserialize)]
|
||||
pub struct WebhookBody {
|
||||
text: String,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
blocks: Vec<Block>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
thread_ts: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
mrkdwn: Option<bool>,
|
||||
}
|
||||
|
||||
impl WebhookBody {
|
||||
pub fn new(f: impl FnOnce(Self) -> Self) -> Self {
|
||||
f(Self::default())
|
||||
}
|
||||
|
||||
pub fn add_section(mut self, build: impl FnOnce(Section) -> Section) -> Self {
|
||||
self.blocks.push(Block::Section(build(Section::default())));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_rich_text(mut self, build: impl FnOnce(RichText) -> RichText) -> Self {
|
||||
self.blocks
|
||||
.push(Block::RichText(build(RichText::default())));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
/// https://api.slack.com/reference/block-kit/blocks
|
||||
pub enum Block {
|
||||
#[serde(rename = "section")]
|
||||
Section(Section),
|
||||
#[serde(rename = "rich_text")]
|
||||
RichText(RichText),
|
||||
// .... etc.
|
||||
}
|
||||
|
||||
/// https://api.slack.com/reference/block-kit/blocks#section
|
||||
#[derive(Default, Clone, Serialize, Deserialize)]
|
||||
pub struct Section {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
text: Option<Text>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
fields: Vec<Text>,
|
||||
// fields, accessories...
|
||||
}
|
||||
|
||||
impl Section {
|
||||
pub fn text(mut self, text: Text) -> Self {
|
||||
self.text = Some(text);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_field(mut self, field: Text) -> Self {
|
||||
self.fields.push(field);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// https://api.slack.com/reference/block-kit/composition-objects#text
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Text {
|
||||
#[serde(rename = "plain_text")]
|
||||
PlainText { text: String, emoji: bool },
|
||||
#[serde(rename = "mrkdwn")]
|
||||
Markdown { text: String, verbatim: bool },
|
||||
}
|
||||
|
||||
impl Text {
|
||||
pub fn plain(s: String) -> Self {
|
||||
Self::PlainText {
|
||||
text: s,
|
||||
emoji: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn markdown(s: String) -> Self {
|
||||
Self::Markdown {
|
||||
text: s,
|
||||
verbatim: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Serialize, Deserialize)]
|
||||
pub struct RichText {
|
||||
elements: Vec<RichTextObject>,
|
||||
}
|
||||
|
||||
impl RichText {
|
||||
pub fn new(f: impl FnOnce(Self) -> Self) -> Self {
|
||||
f(Self::default())
|
||||
}
|
||||
|
||||
pub fn add_preformatted(
|
||||
mut self,
|
||||
build: impl FnOnce(RichTextPreformatted) -> RichTextPreformatted,
|
||||
) -> Self {
|
||||
self.elements.push(RichTextObject::Preformatted(build(
|
||||
RichTextPreformatted::default(),
|
||||
)));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// https://api.slack.com/reference/block-kit/blocks#rich_text
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum RichTextObject {
|
||||
#[serde(rename = "rich_text_preformatted")]
|
||||
Preformatted(RichTextPreformatted),
|
||||
// etc.
|
||||
}
|
||||
|
||||
/// https://api.slack.com/reference/block-kit/blocks#rich_text_preformatted
|
||||
#[derive(Clone, Default, Serialize, Deserialize)]
|
||||
pub struct RichTextPreformatted {
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
elements: Vec<RichTextElement>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
border: Option<u8>,
|
||||
}
|
||||
|
||||
impl RichTextPreformatted {
|
||||
pub fn add_text(mut self, text: String) -> Self {
|
||||
self.elements.push(RichTextElement::Text { text });
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// https://api.slack.com/reference/block-kit/blocks#element-types
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum RichTextElement {
|
||||
#[serde(rename = "text")]
|
||||
Text { text: String },
|
||||
// etc.
|
||||
}
|
||||
@@ -8,24 +8,16 @@ use axum::{
|
||||
middleware::Next,
|
||||
response::IntoResponse,
|
||||
};
|
||||
use lazy_static::lazy_static;
|
||||
use prometheus::{exponential_buckets, register_histogram, Histogram};
|
||||
use rand::thread_rng;
|
||||
use scrypt::{
|
||||
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
|
||||
password_hash::{PasswordHash, PasswordVerifier},
|
||||
Scrypt,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Digest;
|
||||
use std::sync::OnceLock;
|
||||
use std::{sync::Arc, time::Instant};
|
||||
|
||||
lazy_static! {
|
||||
static ref METRIC_ACCESS_TOKEN_HASHING_TIME: Histogram = register_histogram!(
|
||||
"access_token_hashing_time",
|
||||
"time spent hashing access tokens",
|
||||
exponential_buckets(10.0, 2.0, 10).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
use subtle::ConstantTimeEq;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct Impersonator(pub Option<db::User>);
|
||||
@@ -124,8 +116,7 @@ pub async fn create_access_token(
|
||||
) -> Result<String> {
|
||||
const VERSION: usize = 1;
|
||||
let access_token = rpc::auth::random_token();
|
||||
let access_token_hash =
|
||||
hash_access_token(&access_token).context("failed to hash access token")?;
|
||||
let access_token_hash = hash_access_token(&access_token);
|
||||
let id = db
|
||||
.create_access_token(
|
||||
user_id,
|
||||
@@ -141,23 +132,15 @@ pub async fn create_access_token(
|
||||
})?)
|
||||
}
|
||||
|
||||
fn hash_access_token(token: &str) -> Result<String> {
|
||||
// Avoid slow hashing in debug mode.
|
||||
let params = if cfg!(debug_assertions) {
|
||||
scrypt::Params::new(1, 1, 1).unwrap()
|
||||
} else {
|
||||
scrypt::Params::new(14, 8, 1).unwrap()
|
||||
};
|
||||
|
||||
Ok(Scrypt
|
||||
.hash_password(
|
||||
token.as_bytes(),
|
||||
None,
|
||||
params,
|
||||
&SaltString::generate(thread_rng()),
|
||||
)
|
||||
.map_err(anyhow::Error::new)?
|
||||
.to_string())
|
||||
/// Hashing prevents anyone with access to the database being able to login.
|
||||
/// As the token is randomly generated, we don't need to worry about scrypt-style
|
||||
/// protection.
|
||||
fn hash_access_token(token: &str) -> String {
|
||||
let digest = sha2::Sha256::digest(token);
|
||||
format!(
|
||||
"$sha256${}",
|
||||
base64::encode_config(digest, base64::URL_SAFE)
|
||||
)
|
||||
}
|
||||
|
||||
/// Encrypts the given access token with the given public key to avoid leaking it on the way
|
||||
@@ -182,6 +165,16 @@ pub async fn verify_access_token(
|
||||
user_id: UserId,
|
||||
db: &Arc<Database>,
|
||||
) -> Result<VerifyAccessTokenResult> {
|
||||
static METRIC_ACCESS_TOKEN_HASHING_TIME: OnceLock<Histogram> = OnceLock::new();
|
||||
let metric_access_token_hashing_time = METRIC_ACCESS_TOKEN_HASHING_TIME.get_or_init(|| {
|
||||
register_histogram!(
|
||||
"access_token_hashing_time",
|
||||
"time spent hashing access tokens",
|
||||
exponential_buckets(10.0, 2.0, 10).unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
let token: AccessTokenJson = serde_json::from_str(&token)?;
|
||||
|
||||
let db_token = db.get_access_token(token.id).await?;
|
||||
@@ -189,15 +182,27 @@ pub async fn verify_access_token(
|
||||
if token_user_id != user_id {
|
||||
return Err(anyhow!("no such access token"))?;
|
||||
}
|
||||
|
||||
let db_hash = PasswordHash::new(&db_token.hash).map_err(anyhow::Error::new)?;
|
||||
let t0 = Instant::now();
|
||||
let is_valid = Scrypt
|
||||
.verify_password(token.token.as_bytes(), &db_hash)
|
||||
.is_ok();
|
||||
|
||||
let is_valid = if db_token.hash.starts_with("$scrypt$") {
|
||||
let db_hash = PasswordHash::new(&db_token.hash).map_err(anyhow::Error::new)?;
|
||||
Scrypt
|
||||
.verify_password(token.token.as_bytes(), &db_hash)
|
||||
.is_ok()
|
||||
} else {
|
||||
let token_hash = hash_access_token(&token.token);
|
||||
db_token.hash.as_bytes().ct_eq(token_hash.as_ref()).into()
|
||||
};
|
||||
|
||||
let duration = t0.elapsed();
|
||||
log::info!("hashed access token in {:?}", duration);
|
||||
METRIC_ACCESS_TOKEN_HASHING_TIME.observe(duration.as_millis() as f64);
|
||||
metric_access_token_hashing_time.observe(duration.as_millis() as f64);
|
||||
|
||||
if is_valid && db_token.hash.starts_with("$scrypt$") {
|
||||
let new_hash = hash_access_token(&token.token);
|
||||
db.update_access_token_hash(db_token.id, &new_hash).await?;
|
||||
}
|
||||
|
||||
Ok(VerifyAccessTokenResult {
|
||||
is_valid,
|
||||
impersonator_id: if db_token.impersonated_user_id.is_some() {
|
||||
@@ -207,3 +212,145 @@ pub async fn verify_access_token(
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use rand::thread_rng;
|
||||
use scrypt::password_hash::{PasswordHasher, SaltString};
|
||||
use sea_orm::EntityTrait;
|
||||
|
||||
use super::*;
|
||||
use crate::db::{access_token, NewUserParams};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_verify_access_token(cx: &mut gpui::TestAppContext) {
|
||||
let test_db = crate::db::TestDb::postgres(cx.executor().clone());
|
||||
let db = test_db.db();
|
||||
|
||||
let user = db
|
||||
.create_user(
|
||||
"example@example.com",
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "example".into(),
|
||||
github_user_id: 1,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let token = create_access_token(&db, user.user_id, None).await.unwrap();
|
||||
assert!(matches!(
|
||||
verify_access_token(&token, user.user_id, &db)
|
||||
.await
|
||||
.unwrap(),
|
||||
VerifyAccessTokenResult {
|
||||
is_valid: true,
|
||||
impersonator_id: None,
|
||||
}
|
||||
));
|
||||
|
||||
let old_token = create_previous_access_token(user.user_id, None, &db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let old_token_id = serde_json::from_str::<AccessTokenJson>(&old_token)
|
||||
.unwrap()
|
||||
.id;
|
||||
|
||||
let hash = db
|
||||
.transaction(|tx| async move {
|
||||
Ok(access_token::Entity::find_by_id(old_token_id)
|
||||
.one(&*tx)
|
||||
.await?)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.hash;
|
||||
assert!(hash.starts_with("$scrypt$"));
|
||||
|
||||
assert!(matches!(
|
||||
verify_access_token(&old_token, user.user_id, &db)
|
||||
.await
|
||||
.unwrap(),
|
||||
VerifyAccessTokenResult {
|
||||
is_valid: true,
|
||||
impersonator_id: None,
|
||||
}
|
||||
));
|
||||
|
||||
let hash = db
|
||||
.transaction(|tx| async move {
|
||||
Ok(access_token::Entity::find_by_id(old_token_id)
|
||||
.one(&*tx)
|
||||
.await?)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.hash;
|
||||
assert!(hash.starts_with("$sha256$"));
|
||||
|
||||
assert!(matches!(
|
||||
verify_access_token(&old_token, user.user_id, &db)
|
||||
.await
|
||||
.unwrap(),
|
||||
VerifyAccessTokenResult {
|
||||
is_valid: true,
|
||||
impersonator_id: None,
|
||||
}
|
||||
));
|
||||
|
||||
assert!(matches!(
|
||||
verify_access_token(&token, user.user_id, &db)
|
||||
.await
|
||||
.unwrap(),
|
||||
VerifyAccessTokenResult {
|
||||
is_valid: true,
|
||||
impersonator_id: None,
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
async fn create_previous_access_token(
|
||||
user_id: UserId,
|
||||
impersonated_user_id: Option<UserId>,
|
||||
db: &Database,
|
||||
) -> Result<String> {
|
||||
let access_token = rpc::auth::random_token();
|
||||
let access_token_hash = previous_hash_access_token(&access_token)?;
|
||||
let id = db
|
||||
.create_access_token(
|
||||
user_id,
|
||||
impersonated_user_id,
|
||||
&access_token_hash,
|
||||
MAX_ACCESS_TOKENS_TO_STORE,
|
||||
)
|
||||
.await?;
|
||||
Ok(serde_json::to_string(&AccessTokenJson {
|
||||
version: 1,
|
||||
id,
|
||||
token: access_token,
|
||||
})?)
|
||||
}
|
||||
|
||||
fn previous_hash_access_token(token: &str) -> Result<String> {
|
||||
// Avoid slow hashing in debug mode.
|
||||
let params = if cfg!(debug_assertions) {
|
||||
scrypt::Params::new(1, 1, 1).unwrap()
|
||||
} else {
|
||||
scrypt::Params::new(14, 8, 1).unwrap()
|
||||
};
|
||||
|
||||
Ok(Scrypt
|
||||
.hash_password(
|
||||
token.as_bytes(),
|
||||
None,
|
||||
params,
|
||||
&SaltString::generate(thread_rng()),
|
||||
)
|
||||
.map_err(anyhow::Error::new)?
|
||||
.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,9 +88,9 @@ async fn fetch_github<T: DeserializeOwned>(client: &reqwest::Client, url: &str)
|
||||
.header("user-agent", "zed")
|
||||
.send()
|
||||
.await
|
||||
.expect(&format!("failed to fetch '{}'", url));
|
||||
.unwrap_or_else(|_| panic!("failed to fetch '{}'", url));
|
||||
response
|
||||
.json()
|
||||
.await
|
||||
.expect(&format!("failed to deserialize github user from '{}'", url))
|
||||
.unwrap_or_else(|_| panic!("failed to deserialize github user from '{}'", url))
|
||||
}
|
||||
|
||||
@@ -359,7 +359,7 @@ impl Database {
|
||||
const SLEEPS: [f32; 10] = [10., 20., 40., 80., 160., 320., 640., 1280., 2560., 5120.];
|
||||
if is_serialization_error(error) && prev_attempt_count < SLEEPS.len() {
|
||||
let base_delay = SLEEPS[prev_attempt_count];
|
||||
let randomized_delay = base_delay as f32 * self.rng.lock().await.gen_range(0.5..=2.0);
|
||||
let randomized_delay = base_delay * self.rng.lock().await.gen_range(0.5..=2.0);
|
||||
log::info!(
|
||||
"retrying transaction after serialization error. delay: {} ms.",
|
||||
randomized_delay
|
||||
@@ -375,7 +375,7 @@ impl Database {
|
||||
}
|
||||
|
||||
fn is_serialization_error(error: &Error) -> bool {
|
||||
const SERIALIZATION_FAILURE_CODE: &'static str = "40001";
|
||||
const SERIALIZATION_FAILURE_CODE: &str = "40001";
|
||||
match error {
|
||||
Error::Database(
|
||||
DbErr::Exec(sea_orm::RuntimeErr::SqlxError(error))
|
||||
@@ -670,6 +670,8 @@ pub struct RefreshedChannelBuffer {
|
||||
}
|
||||
|
||||
pub struct Project {
|
||||
pub id: ProjectId,
|
||||
pub role: ChannelRole,
|
||||
pub collaborators: Vec<ProjectCollaborator>,
|
||||
pub worktrees: BTreeMap<u64, Worktree>,
|
||||
pub language_servers: Vec<proto::LanguageServer>,
|
||||
@@ -695,7 +697,7 @@ impl ProjectCollaborator {
|
||||
#[derive(Debug)]
|
||||
pub struct LeftProject {
|
||||
pub id: ProjectId,
|
||||
pub host_user_id: UserId,
|
||||
pub host_user_id: Option<UserId>,
|
||||
pub host_connection_id: Option<ConnectionId>,
|
||||
pub connection_ids: Vec<ConnectionId>,
|
||||
}
|
||||
|
||||
@@ -55,4 +55,22 @@ impl Database {
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Retrieves the access token with the given ID.
|
||||
pub async fn update_access_token_hash(
|
||||
&self,
|
||||
id: AccessTokenId,
|
||||
new_hash: &str,
|
||||
) -> Result<access_token::Model> {
|
||||
self.transaction(|tx| async move {
|
||||
Ok(access_token::Entity::update(access_token::ActiveModel {
|
||||
id: ActiveValue::unchanged(id),
|
||||
hash: ActiveValue::set(new_hash.into()),
|
||||
..Default::default()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?)
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ impl Database {
|
||||
connection: ConnectionId,
|
||||
) -> Result<proto::JoinChannelBufferResponse> {
|
||||
self.transaction(|tx| async move {
|
||||
let channel = self.get_channel_internal(channel_id, &*tx).await?;
|
||||
let channel = self.get_channel_internal(channel_id, &tx).await?;
|
||||
self.check_user_is_channel_participant(&channel, user_id, &tx)
|
||||
.await?;
|
||||
|
||||
@@ -134,10 +134,10 @@ impl Database {
|
||||
let mut results = Vec::new();
|
||||
for client_buffer in buffers {
|
||||
let channel = self
|
||||
.get_channel_internal(ChannelId::from_proto(client_buffer.channel_id), &*tx)
|
||||
.get_channel_internal(ChannelId::from_proto(client_buffer.channel_id), &tx)
|
||||
.await?;
|
||||
if self
|
||||
.check_user_is_channel_participant(&channel, user_id, &*tx)
|
||||
.check_user_is_channel_participant(&channel, user_id, &tx)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
@@ -145,7 +145,7 @@ impl Database {
|
||||
continue;
|
||||
}
|
||||
|
||||
let buffer = self.get_channel_buffer(channel.id, &*tx).await?;
|
||||
let buffer = self.get_channel_buffer(channel.id, &tx).await?;
|
||||
let mut collaborators = channel_buffer_collaborator::Entity::find()
|
||||
.filter(channel_buffer_collaborator::Column::ChannelId.eq(channel.id))
|
||||
.all(&*tx)
|
||||
@@ -161,11 +161,9 @@ impl Database {
|
||||
|
||||
// Find the collaborator record for this user's previous lost
|
||||
// connection. Update it with the new connection id.
|
||||
let server_id = ServerId(connection_id.owner_id as i32);
|
||||
let Some(self_collaborator) = collaborators.iter_mut().find(|c| {
|
||||
c.user_id == user_id
|
||||
&& (c.connection_lost || c.connection_server_id != server_id)
|
||||
}) else {
|
||||
let Some(self_collaborator) =
|
||||
collaborators.iter_mut().find(|c| c.user_id == user_id)
|
||||
else {
|
||||
log::info!("can't rejoin buffer, no previous collaborator found");
|
||||
continue;
|
||||
};
|
||||
@@ -182,7 +180,7 @@ impl Database {
|
||||
|
||||
let client_version = version_from_wire(&client_buffer.version);
|
||||
let serialization_version = self
|
||||
.get_buffer_operation_serialization_version(buffer.id, buffer.epoch, &*tx)
|
||||
.get_buffer_operation_serialization_version(buffer.id, buffer.epoch, &tx)
|
||||
.await?;
|
||||
|
||||
let mut rows = buffer_operation::Entity::find()
|
||||
@@ -285,7 +283,7 @@ impl Database {
|
||||
connection: ConnectionId,
|
||||
) -> Result<LeftChannelBuffer> {
|
||||
self.transaction(|tx| async move {
|
||||
self.leave_channel_buffer_internal(channel_id, connection, &*tx)
|
||||
self.leave_channel_buffer_internal(channel_id, connection, &tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
@@ -310,7 +308,7 @@ impl Database {
|
||||
connection_lost: ActiveValue::set(true),
|
||||
..Default::default()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.exec(tx)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -339,7 +337,7 @@ impl Database {
|
||||
let mut result = Vec::new();
|
||||
for channel_id in channel_ids {
|
||||
let left_channel_buffer = self
|
||||
.leave_channel_buffer_internal(channel_id, connection, &*tx)
|
||||
.leave_channel_buffer_internal(channel_id, connection, &tx)
|
||||
.await?;
|
||||
result.push(left_channel_buffer);
|
||||
}
|
||||
@@ -365,7 +363,7 @@ impl Database {
|
||||
.eq(connection.owner_id as i32),
|
||||
),
|
||||
)
|
||||
.exec(&*tx)
|
||||
.exec(tx)
|
||||
.await?;
|
||||
if result.rows_affected == 0 {
|
||||
Err(anyhow!("not a collaborator on this project"))?;
|
||||
@@ -377,7 +375,7 @@ impl Database {
|
||||
.filter(
|
||||
Condition::all().add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)),
|
||||
)
|
||||
.stream(&*tx)
|
||||
.stream(tx)
|
||||
.await?;
|
||||
while let Some(row) = rows.next().await {
|
||||
let row = row?;
|
||||
@@ -408,7 +406,7 @@ impl Database {
|
||||
channel_id: ChannelId,
|
||||
) -> Result<Vec<UserId>> {
|
||||
self.transaction(|tx| async move {
|
||||
self.get_channel_buffer_collaborators_internal(channel_id, &*tx)
|
||||
self.get_channel_buffer_collaborators_internal(channel_id, &tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
@@ -431,7 +429,7 @@ impl Database {
|
||||
Condition::all().add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)),
|
||||
)
|
||||
.into_values::<_, QueryUserIds>()
|
||||
.all(&*tx)
|
||||
.all(tx)
|
||||
.await?;
|
||||
|
||||
Ok(users)
|
||||
@@ -449,7 +447,7 @@ impl Database {
|
||||
Vec<proto::VectorClockEntry>,
|
||||
)> {
|
||||
self.transaction(move |tx| async move {
|
||||
let channel = self.get_channel_internal(channel_id, &*tx).await?;
|
||||
let channel = self.get_channel_internal(channel_id, &tx).await?;
|
||||
|
||||
let mut requires_write_permission = false;
|
||||
for op in operations.iter() {
|
||||
@@ -459,10 +457,10 @@ impl Database {
|
||||
}
|
||||
}
|
||||
if requires_write_permission {
|
||||
self.check_user_is_channel_member(&channel, user, &*tx)
|
||||
self.check_user_is_channel_member(&channel, user, &tx)
|
||||
.await?;
|
||||
} else {
|
||||
self.check_user_is_channel_participant(&channel, user, &*tx)
|
||||
self.check_user_is_channel_participant(&channel, user, &tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -473,7 +471,7 @@ impl Database {
|
||||
.ok_or_else(|| anyhow!("no such buffer"))?;
|
||||
|
||||
let serialization_version = self
|
||||
.get_buffer_operation_serialization_version(buffer.id, buffer.epoch, &*tx)
|
||||
.get_buffer_operation_serialization_version(buffer.id, buffer.epoch, &tx)
|
||||
.await?;
|
||||
|
||||
let operations = operations
|
||||
@@ -502,13 +500,13 @@ impl Database {
|
||||
buffer.epoch,
|
||||
*max_operation.replica_id.as_ref(),
|
||||
*max_operation.lamport_timestamp.as_ref(),
|
||||
&*tx,
|
||||
&tx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
channel_members = self.get_channel_participants(&channel, &*tx).await?;
|
||||
channel_members = self.get_channel_participants(&channel, &tx).await?;
|
||||
let collaborators = self
|
||||
.get_channel_buffer_collaborators_internal(channel_id, &*tx)
|
||||
.get_channel_buffer_collaborators_internal(channel_id, &tx)
|
||||
.await?;
|
||||
channel_members.retain(|member| !collaborators.contains(member));
|
||||
|
||||
@@ -560,6 +558,17 @@ impl Database {
|
||||
lamport_timestamp: i32,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<()> {
|
||||
buffer::Entity::update(buffer::ActiveModel {
|
||||
id: ActiveValue::Unchanged(buffer_id),
|
||||
epoch: ActiveValue::Unchanged(epoch),
|
||||
latest_operation_epoch: ActiveValue::Set(Some(epoch)),
|
||||
latest_operation_replica_id: ActiveValue::Set(Some(replica_id)),
|
||||
latest_operation_lamport_timestamp: ActiveValue::Set(Some(lamport_timestamp)),
|
||||
channel_id: ActiveValue::NotSet,
|
||||
})
|
||||
.exec(tx)
|
||||
.await?;
|
||||
|
||||
use observed_buffer_edits::Column;
|
||||
observed_buffer_edits::Entity::insert(observed_buffer_edits::ActiveModel {
|
||||
user_id: ActiveValue::Set(user_id),
|
||||
@@ -604,7 +613,7 @@ impl Database {
|
||||
.select_only()
|
||||
.column(buffer_snapshot::Column::OperationSerializationVersion)
|
||||
.into_values::<_, QueryOperationSerializationVersion>()
|
||||
.one(&*tx)
|
||||
.one(tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("missing buffer snapshot"))?)
|
||||
}
|
||||
@@ -619,7 +628,7 @@ impl Database {
|
||||
..Default::default()
|
||||
}
|
||||
.find_related(buffer::Entity)
|
||||
.one(&*tx)
|
||||
.one(tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("no such buffer"))?)
|
||||
}
|
||||
@@ -641,7 +650,7 @@ impl Database {
|
||||
.eq(id)
|
||||
.and(buffer_snapshot::Column::Epoch.eq(buffer.epoch)),
|
||||
)
|
||||
.one(&*tx)
|
||||
.one(tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("no such snapshot"))?;
|
||||
|
||||
@@ -659,7 +668,7 @@ impl Database {
|
||||
)
|
||||
.order_by_asc(buffer_operation::Column::LamportTimestamp)
|
||||
.order_by_asc(buffer_operation::Column::ReplicaId)
|
||||
.stream(&*tx)
|
||||
.stream(tx)
|
||||
.await?;
|
||||
|
||||
let mut operations = Vec::new();
|
||||
@@ -713,7 +722,10 @@ impl Database {
|
||||
buffer::ActiveModel {
|
||||
id: ActiveValue::Unchanged(buffer.id),
|
||||
epoch: ActiveValue::Set(epoch),
|
||||
..Default::default()
|
||||
latest_operation_epoch: ActiveValue::NotSet,
|
||||
latest_operation_replica_id: ActiveValue::NotSet,
|
||||
latest_operation_lamport_timestamp: ActiveValue::NotSet,
|
||||
channel_id: ActiveValue::NotSet,
|
||||
}
|
||||
.save(tx)
|
||||
.await?;
|
||||
@@ -739,7 +751,7 @@ impl Database {
|
||||
epoch,
|
||||
component.replica_id as i32,
|
||||
component.timestamp as i32,
|
||||
&*tx,
|
||||
&tx,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -747,30 +759,6 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn latest_channel_buffer_changes(
|
||||
&self,
|
||||
channel_ids_by_buffer_id: &HashMap<BufferId, ChannelId>,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Vec<proto::ChannelBufferVersion>> {
|
||||
let latest_operations = self
|
||||
.get_latest_operations_for_buffers(channel_ids_by_buffer_id.keys().copied(), &*tx)
|
||||
.await?;
|
||||
|
||||
Ok(latest_operations
|
||||
.iter()
|
||||
.flat_map(|op| {
|
||||
Some(proto::ChannelBufferVersion {
|
||||
channel_id: channel_ids_by_buffer_id.get(&op.buffer_id)?.to_proto(),
|
||||
epoch: op.epoch as u64,
|
||||
version: vec![proto::VectorClockEntry {
|
||||
replica_id: op.replica_id as u32,
|
||||
timestamp: op.lamport_timestamp as u32,
|
||||
}],
|
||||
})
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn observed_channel_buffer_changes(
|
||||
&self,
|
||||
channel_ids_by_buffer_id: &HashMap<BufferId, ChannelId>,
|
||||
@@ -783,7 +771,7 @@ impl Database {
|
||||
observed_buffer_edits::Column::BufferId
|
||||
.is_in(channel_ids_by_buffer_id.keys().copied()),
|
||||
)
|
||||
.all(&*tx)
|
||||
.all(tx)
|
||||
.await?;
|
||||
|
||||
Ok(observed_operations
|
||||
@@ -800,55 +788,6 @@ impl Database {
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Returns the latest operations for the buffers with the specified IDs.
|
||||
pub async fn get_latest_operations_for_buffers(
|
||||
&self,
|
||||
buffer_ids: impl IntoIterator<Item = BufferId>,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Vec<buffer_operation::Model>> {
|
||||
let mut values = String::new();
|
||||
for id in buffer_ids {
|
||||
if !values.is_empty() {
|
||||
values.push_str(", ");
|
||||
}
|
||||
write!(&mut values, "({})", id).unwrap();
|
||||
}
|
||||
|
||||
if values.is_empty() {
|
||||
return Ok(Vec::default());
|
||||
}
|
||||
|
||||
let sql = format!(
|
||||
r#"
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
(
|
||||
SELECT
|
||||
*,
|
||||
row_number() OVER (
|
||||
PARTITION BY buffer_id
|
||||
ORDER BY
|
||||
epoch DESC,
|
||||
lamport_timestamp DESC,
|
||||
replica_id DESC
|
||||
) as row_number
|
||||
FROM buffer_operations
|
||||
WHERE
|
||||
buffer_id in ({values})
|
||||
) AS last_operations
|
||||
WHERE
|
||||
row_number = 1
|
||||
"#,
|
||||
);
|
||||
|
||||
let stmt = Statement::from_string(self.pool.get_database_backend(), sql);
|
||||
Ok(buffer_operation::Entity::find()
|
||||
.from_raw_sql(stmt)
|
||||
.all(&*tx)
|
||||
.await?)
|
||||
}
|
||||
}
|
||||
|
||||
fn operation_to_storage(
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use super::*;
|
||||
use rpc::{proto::channel_member::Kind, ErrorCode, ErrorCodeExt};
|
||||
use rpc::{
|
||||
proto::{channel_member::Kind, ChannelBufferVersion, VectorClockEntry},
|
||||
ErrorCode, ErrorCodeExt,
|
||||
};
|
||||
use sea_orm::TryGetableMany;
|
||||
|
||||
impl Database {
|
||||
@@ -53,8 +56,8 @@ impl Database {
|
||||
let mut membership = None;
|
||||
|
||||
if let Some(parent_channel_id) = parent_channel_id {
|
||||
let parent_channel = self.get_channel_internal(parent_channel_id, &*tx).await?;
|
||||
self.check_user_is_channel_admin(&parent_channel, admin_id, &*tx)
|
||||
let parent_channel = self.get_channel_internal(parent_channel_id, &tx).await?;
|
||||
self.check_user_is_channel_admin(&parent_channel, admin_id, &tx)
|
||||
.await?;
|
||||
parent = Some(parent_channel);
|
||||
}
|
||||
@@ -105,14 +108,14 @@ impl Database {
|
||||
connection: ConnectionId,
|
||||
) -> Result<(JoinRoom, Option<MembershipUpdated>, ChannelRole)> {
|
||||
self.transaction(move |tx| async move {
|
||||
let channel = self.get_channel_internal(channel_id, &*tx).await?;
|
||||
let mut role = self.channel_role_for_user(&channel, user_id, &*tx).await?;
|
||||
let channel = self.get_channel_internal(channel_id, &tx).await?;
|
||||
let mut role = self.channel_role_for_user(&channel, user_id, &tx).await?;
|
||||
|
||||
let mut accept_invite_result = None;
|
||||
|
||||
if role.is_none() {
|
||||
if let Some(invitation) = self
|
||||
.pending_invite_for_channel(&channel, user_id, &*tx)
|
||||
.pending_invite_for_channel(&channel, user_id, &tx)
|
||||
.await?
|
||||
{
|
||||
// note, this may be a parent channel
|
||||
@@ -125,12 +128,12 @@ impl Database {
|
||||
.await?;
|
||||
|
||||
accept_invite_result = Some(
|
||||
self.calculate_membership_updated(&channel, user_id, &*tx)
|
||||
self.calculate_membership_updated(&channel, user_id, &tx)
|
||||
.await?,
|
||||
);
|
||||
|
||||
debug_assert!(
|
||||
self.channel_role_for_user(&channel, user_id, &*tx).await? == role
|
||||
self.channel_role_for_user(&channel, user_id, &tx).await? == role
|
||||
);
|
||||
} else if channel.visibility == ChannelVisibility::Public {
|
||||
role = Some(ChannelRole::Guest);
|
||||
@@ -145,12 +148,12 @@ impl Database {
|
||||
.await?;
|
||||
|
||||
accept_invite_result = Some(
|
||||
self.calculate_membership_updated(&channel, user_id, &*tx)
|
||||
self.calculate_membership_updated(&channel, user_id, &tx)
|
||||
.await?,
|
||||
);
|
||||
|
||||
debug_assert!(
|
||||
self.channel_role_for_user(&channel, user_id, &*tx).await? == role
|
||||
self.channel_role_for_user(&channel, user_id, &tx).await? == role
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -162,10 +165,10 @@ impl Database {
|
||||
|
||||
let live_kit_room = format!("channel-{}", nanoid::nanoid!(30));
|
||||
let room_id = self
|
||||
.get_or_create_channel_room(channel_id, &live_kit_room, &*tx)
|
||||
.get_or_create_channel_room(channel_id, &live_kit_room, &tx)
|
||||
.await?;
|
||||
|
||||
self.join_channel_room_internal(room_id, user_id, connection, role, &*tx)
|
||||
self.join_channel_room_internal(room_id, user_id, connection, role, &tx)
|
||||
.await
|
||||
.map(|jr| (jr, accept_invite_result, role))
|
||||
})
|
||||
@@ -180,13 +183,13 @@ impl Database {
|
||||
admin_id: UserId,
|
||||
) -> Result<(Channel, Vec<channel_member::Model>)> {
|
||||
self.transaction(move |tx| async move {
|
||||
let channel = self.get_channel_internal(channel_id, &*tx).await?;
|
||||
self.check_user_is_channel_admin(&channel, admin_id, &*tx)
|
||||
let channel = self.get_channel_internal(channel_id, &tx).await?;
|
||||
self.check_user_is_channel_admin(&channel, admin_id, &tx)
|
||||
.await?;
|
||||
|
||||
if visibility == ChannelVisibility::Public {
|
||||
if let Some(parent_id) = channel.parent_id() {
|
||||
let parent = self.get_channel_internal(parent_id, &*tx).await?;
|
||||
let parent = self.get_channel_internal(parent_id, &tx).await?;
|
||||
|
||||
if parent.visibility != ChannelVisibility::Public {
|
||||
Err(ErrorCode::BadPublicNesting
|
||||
@@ -196,7 +199,7 @@ impl Database {
|
||||
}
|
||||
} else if visibility == ChannelVisibility::Members {
|
||||
if self
|
||||
.get_channel_descendants_excluding_self([&channel], &*tx)
|
||||
.get_channel_descendants_excluding_self([&channel], &tx)
|
||||
.await?
|
||||
.into_iter()
|
||||
.any(|channel| channel.visibility == ChannelVisibility::Public)
|
||||
@@ -228,7 +231,7 @@ impl Database {
|
||||
requires_zed_cla: bool,
|
||||
) -> Result<()> {
|
||||
self.transaction(move |tx| async move {
|
||||
let channel = self.get_channel_internal(channel_id, &*tx).await?;
|
||||
let channel = self.get_channel_internal(channel_id, &tx).await?;
|
||||
let mut model = channel.into_active_model();
|
||||
model.requires_zed_cla = ActiveValue::Set(requires_zed_cla);
|
||||
model.update(&*tx).await?;
|
||||
@@ -244,8 +247,8 @@ impl Database {
|
||||
user_id: UserId,
|
||||
) -> Result<(Vec<ChannelId>, Vec<UserId>)> {
|
||||
self.transaction(move |tx| async move {
|
||||
let channel = self.get_channel_internal(channel_id, &*tx).await?;
|
||||
self.check_user_is_channel_admin(&channel, user_id, &*tx)
|
||||
let channel = self.get_channel_internal(channel_id, &tx).await?;
|
||||
self.check_user_is_channel_admin(&channel, user_id, &tx)
|
||||
.await?;
|
||||
|
||||
let members_to_notify: Vec<UserId> = channel_member::Entity::find()
|
||||
@@ -258,7 +261,7 @@ impl Database {
|
||||
.await?;
|
||||
|
||||
let channels_to_remove = self
|
||||
.get_channel_descendants_excluding_self([&channel], &*tx)
|
||||
.get_channel_descendants_excluding_self([&channel], &tx)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|channel| channel.id)
|
||||
@@ -284,8 +287,8 @@ impl Database {
|
||||
role: ChannelRole,
|
||||
) -> Result<InviteMemberResult> {
|
||||
self.transaction(move |tx| async move {
|
||||
let channel = self.get_channel_internal(channel_id, &*tx).await?;
|
||||
self.check_user_is_channel_admin(&channel, inviter_id, &*tx)
|
||||
let channel = self.get_channel_internal(channel_id, &tx).await?;
|
||||
self.check_user_is_channel_admin(&channel, inviter_id, &tx)
|
||||
.await?;
|
||||
if !channel.is_root() {
|
||||
Err(ErrorCode::NotARootChannel.anyhow())?
|
||||
@@ -312,7 +315,7 @@ impl Database {
|
||||
inviter_id: inviter_id.to_proto(),
|
||||
},
|
||||
true,
|
||||
&*tx,
|
||||
&tx,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
@@ -344,8 +347,8 @@ impl Database {
|
||||
self.transaction(move |tx| async move {
|
||||
let new_name = Self::sanitize_channel_name(new_name)?.to_string();
|
||||
|
||||
let channel = self.get_channel_internal(channel_id, &*tx).await?;
|
||||
self.check_user_is_channel_admin(&channel, admin_id, &*tx)
|
||||
let channel = self.get_channel_internal(channel_id, &tx).await?;
|
||||
self.check_user_is_channel_admin(&channel, admin_id, &tx)
|
||||
.await?;
|
||||
|
||||
let mut model = channel.into_active_model();
|
||||
@@ -370,7 +373,7 @@ impl Database {
|
||||
accept: bool,
|
||||
) -> Result<RespondToChannelInvite> {
|
||||
self.transaction(move |tx| async move {
|
||||
let channel = self.get_channel_internal(channel_id, &*tx).await?;
|
||||
let channel = self.get_channel_internal(channel_id, &tx).await?;
|
||||
|
||||
let membership_update = if accept {
|
||||
let rows_affected = channel_member::Entity::update_many()
|
||||
@@ -393,7 +396,7 @@ impl Database {
|
||||
}
|
||||
|
||||
Some(
|
||||
self.calculate_membership_updated(&channel, user_id, &*tx)
|
||||
self.calculate_membership_updated(&channel, user_id, &tx)
|
||||
.await?,
|
||||
)
|
||||
} else {
|
||||
@@ -425,7 +428,7 @@ impl Database {
|
||||
inviter_id: Default::default(),
|
||||
},
|
||||
accept,
|
||||
&*tx,
|
||||
&tx,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
@@ -441,9 +444,9 @@ impl Database {
|
||||
user_id: UserId,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<MembershipUpdated> {
|
||||
let new_channels = self.get_user_channels(user_id, Some(channel), &*tx).await?;
|
||||
let new_channels = self.get_user_channels(user_id, Some(channel), tx).await?;
|
||||
let removed_channels = self
|
||||
.get_channel_descendants_excluding_self([channel], &*tx)
|
||||
.get_channel_descendants_excluding_self([channel], tx)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|channel| channel.id)
|
||||
@@ -466,10 +469,10 @@ impl Database {
|
||||
admin_id: UserId,
|
||||
) -> Result<RemoveChannelMemberResult> {
|
||||
self.transaction(|tx| async move {
|
||||
let channel = self.get_channel_internal(channel_id, &*tx).await?;
|
||||
let channel = self.get_channel_internal(channel_id, &tx).await?;
|
||||
|
||||
if member_id != admin_id {
|
||||
self.check_user_is_channel_admin(&channel, admin_id, &*tx)
|
||||
self.check_user_is_channel_admin(&channel, admin_id, &tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -488,7 +491,7 @@ impl Database {
|
||||
|
||||
Ok(RemoveChannelMemberResult {
|
||||
membership_update: self
|
||||
.calculate_membership_updated(&channel, member_id, &*tx)
|
||||
.calculate_membership_updated(&channel, member_id, &tx)
|
||||
.await?,
|
||||
notification_id: self
|
||||
.remove_notification(
|
||||
@@ -498,7 +501,7 @@ impl Database {
|
||||
channel_name: Default::default(),
|
||||
inviter_id: Default::default(),
|
||||
},
|
||||
&*tx,
|
||||
&tx,
|
||||
)
|
||||
.await?,
|
||||
})
|
||||
@@ -529,10 +532,7 @@ impl Database {
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
let channels = channels
|
||||
.into_iter()
|
||||
.filter_map(|channel| Some(Channel::from_model(channel)))
|
||||
.collect();
|
||||
let channels = channels.into_iter().map(Channel::from_model).collect();
|
||||
|
||||
Ok(channels)
|
||||
})
|
||||
@@ -567,16 +567,16 @@ impl Database {
|
||||
|
||||
let channel_memberships = channel_member::Entity::find()
|
||||
.filter(filter)
|
||||
.all(&*tx)
|
||||
.all(tx)
|
||||
.await?;
|
||||
|
||||
let channels = channel::Entity::find()
|
||||
.filter(channel::Column::Id.is_in(channel_memberships.iter().map(|m| m.channel_id)))
|
||||
.all(&*tx)
|
||||
.all(tx)
|
||||
.await?;
|
||||
|
||||
let mut descendants = self
|
||||
.get_channel_descendants_excluding_self(channels.iter(), &*tx)
|
||||
.get_channel_descendants_excluding_self(channels.iter(), tx)
|
||||
.await?;
|
||||
|
||||
for channel in channels {
|
||||
@@ -617,7 +617,7 @@ impl Database {
|
||||
.column(room::Column::ChannelId)
|
||||
.column(room_participant::Column::UserId)
|
||||
.into_values::<_, QueryUserIdsAndChannelIds>()
|
||||
.stream(&*tx)
|
||||
.stream(tx)
|
||||
.await?;
|
||||
while let Some(row) = rows.next().await {
|
||||
let row: (ChannelId, UserId) = row?;
|
||||
@@ -628,32 +628,44 @@ impl Database {
|
||||
let channel_ids = channels.iter().map(|c| c.id).collect::<Vec<_>>();
|
||||
|
||||
let mut channel_ids_by_buffer_id = HashMap::default();
|
||||
let mut latest_buffer_versions: Vec<ChannelBufferVersion> = vec![];
|
||||
let mut rows = buffer::Entity::find()
|
||||
.filter(buffer::Column::ChannelId.is_in(channel_ids.iter().copied()))
|
||||
.stream(&*tx)
|
||||
.stream(tx)
|
||||
.await?;
|
||||
while let Some(row) = rows.next().await {
|
||||
let row = row?;
|
||||
channel_ids_by_buffer_id.insert(row.id, row.channel_id);
|
||||
latest_buffer_versions.push(ChannelBufferVersion {
|
||||
channel_id: row.channel_id.0 as u64,
|
||||
epoch: row.latest_operation_epoch.unwrap_or_default() as u64,
|
||||
version: if let Some((latest_lamport_timestamp, latest_replica_id)) = row
|
||||
.latest_operation_lamport_timestamp
|
||||
.zip(row.latest_operation_replica_id)
|
||||
{
|
||||
vec![VectorClockEntry {
|
||||
timestamp: latest_lamport_timestamp as u32,
|
||||
replica_id: latest_replica_id as u32,
|
||||
}]
|
||||
} else {
|
||||
vec![]
|
||||
},
|
||||
});
|
||||
}
|
||||
drop(rows);
|
||||
|
||||
let latest_buffer_versions = self
|
||||
.latest_channel_buffer_changes(&channel_ids_by_buffer_id, &*tx)
|
||||
.await?;
|
||||
|
||||
let latest_channel_messages = self.latest_channel_messages(&channel_ids, &*tx).await?;
|
||||
let latest_channel_messages = self.latest_channel_messages(&channel_ids, tx).await?;
|
||||
|
||||
let observed_buffer_versions = self
|
||||
.observed_channel_buffer_changes(&channel_ids_by_buffer_id, user_id, &*tx)
|
||||
.observed_channel_buffer_changes(&channel_ids_by_buffer_id, user_id, tx)
|
||||
.await?;
|
||||
|
||||
let observed_channel_messages = self
|
||||
.observed_channel_messages(&channel_ids, user_id, &*tx)
|
||||
.observed_channel_messages(&channel_ids, user_id, tx)
|
||||
.await?;
|
||||
|
||||
let hosted_projects = self
|
||||
.get_hosted_projects(&channel_ids, &roles_by_channel_id, &*tx)
|
||||
.get_hosted_projects(&channel_ids, &roles_by_channel_id, tx)
|
||||
.await?;
|
||||
|
||||
Ok(ChannelsForUser {
|
||||
@@ -677,8 +689,8 @@ impl Database {
|
||||
role: ChannelRole,
|
||||
) -> Result<SetMemberRoleResult> {
|
||||
self.transaction(|tx| async move {
|
||||
let channel = self.get_channel_internal(channel_id, &*tx).await?;
|
||||
self.check_user_is_channel_admin(&channel, admin_id, &*tx)
|
||||
let channel = self.get_channel_internal(channel_id, &tx).await?;
|
||||
self.check_user_is_channel_admin(&channel, admin_id, &tx)
|
||||
.await?;
|
||||
|
||||
let membership = channel_member::Entity::find()
|
||||
@@ -700,7 +712,7 @@ impl Database {
|
||||
|
||||
if updated.accepted {
|
||||
Ok(SetMemberRoleResult::MembershipUpdated(
|
||||
self.calculate_membership_updated(&channel, for_user, &*tx)
|
||||
self.calculate_membership_updated(&channel, for_user, &tx)
|
||||
.await?,
|
||||
))
|
||||
} else {
|
||||
@@ -720,13 +732,13 @@ impl Database {
|
||||
) -> Result<Vec<proto::ChannelMember>> {
|
||||
let (role, members) = self
|
||||
.transaction(move |tx| async move {
|
||||
let channel = self.get_channel_internal(channel_id, &*tx).await?;
|
||||
let channel = self.get_channel_internal(channel_id, &tx).await?;
|
||||
let role = self
|
||||
.check_user_is_channel_participant(&channel, user_id, &*tx)
|
||||
.check_user_is_channel_participant(&channel, user_id, &tx)
|
||||
.await?;
|
||||
Ok((
|
||||
role,
|
||||
self.get_channel_participant_details_internal(&channel, &*tx)
|
||||
self.get_channel_participant_details_internal(&channel, &tx)
|
||||
.await?,
|
||||
))
|
||||
})
|
||||
@@ -781,7 +793,7 @@ impl Database {
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Vec<UserId>> {
|
||||
let participants = self
|
||||
.get_channel_participant_details_internal(channel, &*tx)
|
||||
.get_channel_participant_details_internal(channel, tx)
|
||||
.await?;
|
||||
Ok(participants
|
||||
.into_iter()
|
||||
@@ -858,7 +870,7 @@ impl Database {
|
||||
.filter(channel_member::Column::ChannelId.eq(channel.root_id()))
|
||||
.filter(channel_member::Column::UserId.eq(user_id))
|
||||
.filter(channel_member::Column::Accepted.eq(false))
|
||||
.one(&*tx)
|
||||
.one(tx)
|
||||
.await?;
|
||||
|
||||
Ok(row)
|
||||
@@ -878,7 +890,7 @@ impl Database {
|
||||
.and(channel_member::Column::UserId.eq(user_id))
|
||||
.and(channel_member::Column::Accepted.eq(true)),
|
||||
)
|
||||
.one(&*tx)
|
||||
.one(tx)
|
||||
.await?;
|
||||
|
||||
let Some(membership) = membership else {
|
||||
@@ -918,8 +930,8 @@ impl Database {
|
||||
/// Returns the channel with the given ID.
|
||||
pub async fn get_channel(&self, channel_id: ChannelId, user_id: UserId) -> Result<Channel> {
|
||||
self.transaction(|tx| async move {
|
||||
let channel = self.get_channel_internal(channel_id, &*tx).await?;
|
||||
self.check_user_is_channel_participant(&channel, user_id, &*tx)
|
||||
let channel = self.get_channel_internal(channel_id, &tx).await?;
|
||||
self.check_user_is_channel_participant(&channel, user_id, &tx)
|
||||
.await?;
|
||||
|
||||
Ok(Channel::from_model(channel))
|
||||
@@ -933,7 +945,7 @@ impl Database {
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<channel::Model> {
|
||||
Ok(channel::Entity::find_by_id(channel_id)
|
||||
.one(&*tx)
|
||||
.one(tx)
|
||||
.await?
|
||||
.ok_or_else(|| proto::ErrorCode::NoSuchChannel.anyhow())?)
|
||||
}
|
||||
@@ -946,7 +958,7 @@ impl Database {
|
||||
) -> Result<RoomId> {
|
||||
let room = room::Entity::find()
|
||||
.filter(room::Column::ChannelId.eq(channel_id))
|
||||
.one(&*tx)
|
||||
.one(tx)
|
||||
.await?;
|
||||
|
||||
let room_id = if let Some(room) = room {
|
||||
@@ -957,7 +969,7 @@ impl Database {
|
||||
live_kit_room: ActiveValue::Set(live_kit_room.to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.exec(tx)
|
||||
.await?;
|
||||
|
||||
result.last_insert_id
|
||||
@@ -974,10 +986,10 @@ impl Database {
|
||||
admin_id: UserId,
|
||||
) -> Result<(Vec<Channel>, Vec<channel_member::Model>)> {
|
||||
self.transaction(|tx| async move {
|
||||
let channel = self.get_channel_internal(channel_id, &*tx).await?;
|
||||
self.check_user_is_channel_admin(&channel, admin_id, &*tx)
|
||||
let channel = self.get_channel_internal(channel_id, &tx).await?;
|
||||
self.check_user_is_channel_admin(&channel, admin_id, &tx)
|
||||
.await?;
|
||||
let new_parent = self.get_channel_internal(new_parent_id, &*tx).await?;
|
||||
let new_parent = self.get_channel_internal(new_parent_id, &tx).await?;
|
||||
|
||||
if new_parent.root_id() != channel.root_id() {
|
||||
Err(anyhow!(ErrorCode::WrongMoveTarget))?;
|
||||
|
||||
@@ -177,7 +177,7 @@ impl Database {
|
||||
sender_id: sender_id.to_proto(),
|
||||
},
|
||||
true,
|
||||
&*tx,
|
||||
&tx,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
@@ -227,7 +227,7 @@ impl Database {
|
||||
rpc::Notification::ContactRequest {
|
||||
sender_id: requester_id.to_proto(),
|
||||
},
|
||||
&*tx,
|
||||
&tx,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -335,7 +335,7 @@ impl Database {
|
||||
sender_id: requester_id.to_proto(),
|
||||
},
|
||||
accept,
|
||||
&*tx,
|
||||
&tx,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
@@ -348,7 +348,7 @@ impl Database {
|
||||
responder_id: responder_id.to_proto(),
|
||||
},
|
||||
true,
|
||||
&*tx,
|
||||
&tx,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
|
||||
@@ -72,7 +72,7 @@ impl Database {
|
||||
github_login,
|
||||
github_user_id,
|
||||
github_email,
|
||||
&*tx,
|
||||
&tx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use rpc::proto;
|
||||
use rpc::{proto, ErrorCode};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -9,20 +9,21 @@ impl Database {
|
||||
roles: &HashMap<ChannelId, ChannelRole>,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Vec<proto::HostedProject>> {
|
||||
Ok(hosted_project::Entity::find()
|
||||
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)
|
||||
.all(tx)
|
||||
.await?
|
||||
.into_iter()
|
||||
.flat_map(|project| {
|
||||
if project.deleted_at.is_some() {
|
||||
.flat_map(|(hosted_project, project)| {
|
||||
if hosted_project.deleted_at.is_some() {
|
||||
return None;
|
||||
}
|
||||
match project.visibility {
|
||||
match hosted_project.visibility {
|
||||
ChannelVisibility::Public => {}
|
||||
ChannelVisibility::Members => {
|
||||
let is_visible = roles
|
||||
.get(&project.channel_id)
|
||||
.get(&hosted_project.channel_id)
|
||||
.map(|role| role.can_see_all_descendants())
|
||||
.unwrap_or(false);
|
||||
if !is_visible {
|
||||
@@ -31,12 +32,54 @@ impl Database {
|
||||
}
|
||||
};
|
||||
Some(proto::HostedProject {
|
||||
id: project.id.to_proto(),
|
||||
channel_id: project.channel_id.to_proto(),
|
||||
name: project.name.clone(),
|
||||
visibility: project.visibility.into(),
|
||||
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())
|
||||
.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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ impl Database {
|
||||
user_id: UserId,
|
||||
) -> Result<()> {
|
||||
self.transaction(|tx| async move {
|
||||
let channel = self.get_channel_internal(channel_id, &*tx).await?;
|
||||
self.check_user_is_channel_participant(&channel, user_id, &*tx)
|
||||
let channel = self.get_channel_internal(channel_id, &tx).await?;
|
||||
self.check_user_is_channel_participant(&channel, user_id, &tx)
|
||||
.await?;
|
||||
channel_chat_participant::ActiveModel {
|
||||
id: ActiveValue::NotSet,
|
||||
@@ -87,8 +87,8 @@ impl Database {
|
||||
before_message_id: Option<MessageId>,
|
||||
) -> Result<Vec<proto::ChannelMessage>> {
|
||||
self.transaction(|tx| async move {
|
||||
let channel = self.get_channel_internal(channel_id, &*tx).await?;
|
||||
self.check_user_is_channel_participant(&channel, user_id, &*tx)
|
||||
let channel = self.get_channel_internal(channel_id, &tx).await?;
|
||||
self.check_user_is_channel_participant(&channel, user_id, &tx)
|
||||
.await?;
|
||||
|
||||
let mut condition =
|
||||
@@ -105,7 +105,7 @@ impl Database {
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
self.load_channel_messages(rows, &*tx).await
|
||||
self.load_channel_messages(rows, &tx).await
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -127,16 +127,16 @@ impl Database {
|
||||
for row in &rows {
|
||||
channels.insert(
|
||||
row.channel_id,
|
||||
self.get_channel_internal(row.channel_id, &*tx).await?,
|
||||
self.get_channel_internal(row.channel_id, &tx).await?,
|
||||
);
|
||||
}
|
||||
|
||||
for (_, channel) in channels {
|
||||
self.check_user_is_channel_participant(&channel, user_id, &*tx)
|
||||
self.check_user_is_channel_participant(&channel, user_id, &tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let messages = self.load_channel_messages(rows, &*tx).await?;
|
||||
let messages = self.load_channel_messages(rows, &tx).await?;
|
||||
Ok(messages)
|
||||
})
|
||||
.await
|
||||
@@ -171,7 +171,7 @@ impl Database {
|
||||
.filter(channel_message_mention::Column::MessageId.is_in(messages.iter().map(|m| m.id)))
|
||||
.order_by_asc(channel_message_mention::Column::MessageId)
|
||||
.order_by_asc(channel_message_mention::Column::StartOffset)
|
||||
.stream(&*tx)
|
||||
.stream(tx)
|
||||
.await?;
|
||||
|
||||
let mut message_ix = 0;
|
||||
@@ -200,6 +200,7 @@ impl Database {
|
||||
}
|
||||
|
||||
/// Creates a new channel message.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn create_channel_message(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
@@ -211,8 +212,8 @@ impl Database {
|
||||
reply_to_message_id: Option<MessageId>,
|
||||
) -> Result<CreatedChannelMessage> {
|
||||
self.transaction(|tx| async move {
|
||||
let channel = self.get_channel_internal(channel_id, &*tx).await?;
|
||||
self.check_user_is_channel_participant(&channel, user_id, &*tx)
|
||||
let channel = self.get_channel_internal(channel_id, &tx).await?;
|
||||
self.check_user_is_channel_participant(&channel, user_id, &tx)
|
||||
.await?;
|
||||
|
||||
let mut rows = channel_chat_participant::Entity::find()
|
||||
@@ -302,13 +303,13 @@ impl Database {
|
||||
channel_id: channel_id.to_proto(),
|
||||
},
|
||||
false,
|
||||
&*tx,
|
||||
&tx,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
|
||||
self.observe_channel_message_internal(channel_id, user_id, message_id, &*tx)
|
||||
self.observe_channel_message_internal(channel_id, user_id, message_id, &tx)
|
||||
.await?;
|
||||
}
|
||||
_ => {
|
||||
@@ -321,7 +322,7 @@ impl Database {
|
||||
}
|
||||
}
|
||||
|
||||
let mut channel_members = self.get_channel_participants(&channel, &*tx).await?;
|
||||
let mut channel_members = self.get_channel_participants(&channel, &tx).await?;
|
||||
channel_members.retain(|member| !participant_user_ids.contains(member));
|
||||
|
||||
Ok(CreatedChannelMessage {
|
||||
@@ -341,7 +342,7 @@ impl Database {
|
||||
message_id: MessageId,
|
||||
) -> Result<NotificationBatch> {
|
||||
self.transaction(|tx| async move {
|
||||
self.observe_channel_message_internal(channel_id, user_id, message_id, &*tx)
|
||||
self.observe_channel_message_internal(channel_id, user_id, message_id, &tx)
|
||||
.await?;
|
||||
let mut batch = NotificationBatch::default();
|
||||
batch.extend(
|
||||
@@ -352,7 +353,7 @@ impl Database {
|
||||
sender_id: Default::default(),
|
||||
channel_id: Default::default(),
|
||||
},
|
||||
&*tx,
|
||||
&tx,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
@@ -383,7 +384,7 @@ impl Database {
|
||||
.to_owned(),
|
||||
)
|
||||
// TODO: Try to upgrade SeaORM so we don't have to do this hack around their bug
|
||||
.exec_without_returning(&*tx)
|
||||
.exec_without_returning(tx)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -400,7 +401,7 @@ impl Database {
|
||||
observed_channel_messages::Column::ChannelId
|
||||
.is_in(channel_ids.iter().map(|id| id.0)),
|
||||
)
|
||||
.all(&*tx)
|
||||
.all(tx)
|
||||
.await?;
|
||||
|
||||
Ok(rows
|
||||
@@ -451,7 +452,7 @@ impl Database {
|
||||
|
||||
let stmt = Statement::from_string(self.pool.get_database_backend(), sql);
|
||||
let mut last_messages = channel_message::Model::find_by_statement(stmt)
|
||||
.stream(&*tx)
|
||||
.stream(tx)
|
||||
.await?;
|
||||
|
||||
let mut results = Vec::new();
|
||||
@@ -500,9 +501,9 @@ impl Database {
|
||||
.await?;
|
||||
|
||||
if result.rows_affected == 0 {
|
||||
let channel = self.get_channel_internal(channel_id, &*tx).await?;
|
||||
let channel = self.get_channel_internal(channel_id, &tx).await?;
|
||||
if self
|
||||
.check_user_is_channel_admin(&channel, user_id, &*tx)
|
||||
.check_user_is_channel_admin(&channel, user_id, &tx)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
|
||||
@@ -95,7 +95,7 @@ impl Database {
|
||||
content: ActiveValue::Set(proto.content.clone()),
|
||||
..Default::default()
|
||||
}
|
||||
.save(&*tx)
|
||||
.save(tx)
|
||||
.await?;
|
||||
|
||||
Ok(Some((
|
||||
@@ -184,7 +184,7 @@ impl Database {
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Option<(UserId, proto::Notification)>> {
|
||||
if let Some(id) = self
|
||||
.find_notification(recipient_id, notification, &*tx)
|
||||
.find_notification(recipient_id, notification, tx)
|
||||
.await?
|
||||
{
|
||||
let row = notification::Entity::update(notification::ActiveModel {
|
||||
@@ -236,7 +236,7 @@ impl Database {
|
||||
}),
|
||||
)
|
||||
.into_values::<_, QueryIds>()
|
||||
.one(&*tx)
|
||||
.one(tx)
|
||||
.await?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,13 +57,14 @@ impl Database {
|
||||
}
|
||||
|
||||
let project = project::ActiveModel {
|
||||
room_id: ActiveValue::set(participant.room_id),
|
||||
host_user_id: ActiveValue::set(participant.user_id),
|
||||
room_id: ActiveValue::set(Some(participant.room_id)),
|
||||
host_user_id: ActiveValue::set(Some(participant.user_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()
|
||||
id: ActiveValue::NotSet,
|
||||
hosted_project_id: ActiveValue::Set(None),
|
||||
}
|
||||
.insert(&*tx)
|
||||
.await?;
|
||||
@@ -153,8 +154,12 @@ impl Database {
|
||||
self.update_project_worktrees(project.id, worktrees, &tx)
|
||||
.await?;
|
||||
|
||||
let room_id = project
|
||||
.room_id
|
||||
.ok_or_else(|| anyhow!("project not in a room"))?;
|
||||
|
||||
let guest_connection_ids = self.project_guest_connection_ids(project.id, &tx).await?;
|
||||
let room = self.get_room(project.room_id, &tx).await?;
|
||||
let room = self.get_room(room_id, &tx).await?;
|
||||
Ok((room, guest_connection_ids))
|
||||
})
|
||||
.await
|
||||
@@ -181,7 +186,7 @@ impl Database {
|
||||
.update_column(worktree::Column::RootName)
|
||||
.to_owned(),
|
||||
)
|
||||
.exec(&*tx)
|
||||
.exec(tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -189,7 +194,7 @@ impl Database {
|
||||
.filter(worktree::Column::ProjectId.eq(project_id).and(
|
||||
worktree::Column::Id.is_not_in(worktrees.iter().map(|worktree| worktree.id as i64)),
|
||||
))
|
||||
.exec(&*tx)
|
||||
.exec(tx)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
@@ -382,7 +387,6 @@ impl Database {
|
||||
language_server_id: ActiveValue::set(summary.language_server_id as i64),
|
||||
error_count: ActiveValue::set(summary.error_count as i32),
|
||||
warning_count: ActiveValue::set(summary.warning_count as i32),
|
||||
..Default::default()
|
||||
})
|
||||
.on_conflict(
|
||||
OnConflict::columns([
|
||||
@@ -434,7 +438,6 @@ impl Database {
|
||||
project_id: ActiveValue::set(project_id),
|
||||
id: ActiveValue::set(server.id as i64),
|
||||
name: ActiveValue::set(server.name.clone()),
|
||||
..Default::default()
|
||||
})
|
||||
.on_conflict(
|
||||
OnConflict::columns([
|
||||
@@ -506,8 +509,42 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Adds the given connection to the specified project.
|
||||
pub async fn join_project(
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// Adds the given connection to the specified project
|
||||
/// in the current room.
|
||||
pub async fn join_project_in_room(
|
||||
&self,
|
||||
project_id: ProjectId,
|
||||
connection: ConnectionId,
|
||||
@@ -534,180 +571,240 @@ impl Database {
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("no such project"))?;
|
||||
if project.room_id != participant.room_id {
|
||||
if project.room_id != Some(participant.room_id) {
|
||||
return Err(anyhow!("no such project"))?;
|
||||
}
|
||||
self.join_project_internal(
|
||||
project,
|
||||
participant.user_id,
|
||||
connection,
|
||||
participant.role.unwrap_or(ChannelRole::Member),
|
||||
&tx,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
let mut collaborators = project
|
||||
async fn join_project_internal(
|
||||
&self,
|
||||
project: project::Model,
|
||||
user_id: UserId,
|
||||
connection: ConnectionId,
|
||||
role: ChannelRole,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<(Project, ReplicaId)> {
|
||||
let mut collaborators = project
|
||||
.find_related(project_collaborator::Entity)
|
||||
.all(tx)
|
||||
.await?;
|
||||
let replica_ids = collaborators
|
||||
.iter()
|
||||
.map(|c| c.replica_id)
|
||||
.collect::<HashSet<_>>();
|
||||
let mut replica_id = ReplicaId(1);
|
||||
while replica_ids.contains(&replica_id) {
|
||||
replica_id.0 += 1;
|
||||
}
|
||||
let new_collaborator = project_collaborator::ActiveModel {
|
||||
project_id: ActiveValue::set(project.id),
|
||||
connection_id: ActiveValue::set(connection.id as i32),
|
||||
connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)),
|
||||
user_id: ActiveValue::set(user_id),
|
||||
replica_id: ActiveValue::set(replica_id),
|
||||
is_host: ActiveValue::set(false),
|
||||
..Default::default()
|
||||
}
|
||||
.insert(tx)
|
||||
.await?;
|
||||
collaborators.push(new_collaborator);
|
||||
|
||||
let db_worktrees = project.find_related(worktree::Entity).all(tx).await?;
|
||||
let mut worktrees = db_worktrees
|
||||
.into_iter()
|
||||
.map(|db_worktree| {
|
||||
(
|
||||
db_worktree.id as u64,
|
||||
Worktree {
|
||||
id: db_worktree.id as u64,
|
||||
abs_path: db_worktree.abs_path,
|
||||
root_name: db_worktree.root_name,
|
||||
visible: db_worktree.visible,
|
||||
entries: Default::default(),
|
||||
repository_entries: Default::default(),
|
||||
diagnostic_summaries: Default::default(),
|
||||
settings_files: Default::default(),
|
||||
scan_id: db_worktree.scan_id as u64,
|
||||
completed_scan_id: db_worktree.completed_scan_id as u64,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
// Populate worktree entries.
|
||||
{
|
||||
let mut db_entries = worktree_entry::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(worktree_entry::Column::ProjectId.eq(project.id))
|
||||
.add(worktree_entry::Column::IsDeleted.eq(false)),
|
||||
)
|
||||
.stream(tx)
|
||||
.await?;
|
||||
while let Some(db_entry) = db_entries.next().await {
|
||||
let db_entry = db_entry?;
|
||||
if let Some(worktree) = worktrees.get_mut(&(db_entry.worktree_id as u64)) {
|
||||
worktree.entries.push(proto::Entry {
|
||||
id: db_entry.id as u64,
|
||||
is_dir: db_entry.is_dir,
|
||||
path: db_entry.path,
|
||||
inode: db_entry.inode as u64,
|
||||
mtime: Some(proto::Timestamp {
|
||||
seconds: db_entry.mtime_seconds as u64,
|
||||
nanos: db_entry.mtime_nanos as u32,
|
||||
}),
|
||||
is_symlink: db_entry.is_symlink,
|
||||
is_ignored: db_entry.is_ignored,
|
||||
is_external: db_entry.is_external,
|
||||
git_status: db_entry.git_status.map(|status| status as i32),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Populate repository entries.
|
||||
{
|
||||
let mut db_repository_entries = worktree_repository::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(worktree_repository::Column::ProjectId.eq(project.id))
|
||||
.add(worktree_repository::Column::IsDeleted.eq(false)),
|
||||
)
|
||||
.stream(tx)
|
||||
.await?;
|
||||
while let Some(db_repository_entry) = db_repository_entries.next().await {
|
||||
let db_repository_entry = db_repository_entry?;
|
||||
if let Some(worktree) = worktrees.get_mut(&(db_repository_entry.worktree_id as u64))
|
||||
{
|
||||
worktree.repository_entries.insert(
|
||||
db_repository_entry.work_directory_id as u64,
|
||||
proto::RepositoryEntry {
|
||||
work_directory_id: db_repository_entry.work_directory_id as u64,
|
||||
branch: db_repository_entry.branch,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Populate worktree diagnostic summaries.
|
||||
{
|
||||
let mut db_summaries = worktree_diagnostic_summary::Entity::find()
|
||||
.filter(worktree_diagnostic_summary::Column::ProjectId.eq(project.id))
|
||||
.stream(tx)
|
||||
.await?;
|
||||
while let Some(db_summary) = db_summaries.next().await {
|
||||
let db_summary = db_summary?;
|
||||
if let Some(worktree) = worktrees.get_mut(&(db_summary.worktree_id as u64)) {
|
||||
worktree
|
||||
.diagnostic_summaries
|
||||
.push(proto::DiagnosticSummary {
|
||||
path: db_summary.path,
|
||||
language_server_id: db_summary.language_server_id as u64,
|
||||
error_count: db_summary.error_count as u32,
|
||||
warning_count: db_summary.warning_count as u32,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Populate worktree settings files
|
||||
{
|
||||
let mut db_settings_files = worktree_settings_file::Entity::find()
|
||||
.filter(worktree_settings_file::Column::ProjectId.eq(project.id))
|
||||
.stream(tx)
|
||||
.await?;
|
||||
while let Some(db_settings_file) = db_settings_files.next().await {
|
||||
let db_settings_file = db_settings_file?;
|
||||
if let Some(worktree) = worktrees.get_mut(&(db_settings_file.worktree_id as u64)) {
|
||||
worktree.settings_files.push(WorktreeSettingsFile {
|
||||
path: db_settings_file.path,
|
||||
content: db_settings_file.content,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Populate language servers.
|
||||
let language_servers = project
|
||||
.find_related(language_server::Entity)
|
||||
.all(tx)
|
||||
.await?;
|
||||
|
||||
let project = Project {
|
||||
id: project.id,
|
||||
role,
|
||||
collaborators: collaborators
|
||||
.into_iter()
|
||||
.map(|collaborator| ProjectCollaborator {
|
||||
connection_id: collaborator.connection(),
|
||||
user_id: collaborator.user_id,
|
||||
replica_id: collaborator.replica_id,
|
||||
is_host: collaborator.is_host,
|
||||
})
|
||||
.collect(),
|
||||
worktrees,
|
||||
language_servers: language_servers
|
||||
.into_iter()
|
||||
.map(|language_server| proto::LanguageServer {
|
||||
id: language_server.id as u64,
|
||||
name: language_server.name,
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
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 replica_ids = collaborators
|
||||
.iter()
|
||||
.map(|c| c.replica_id)
|
||||
.collect::<HashSet<_>>();
|
||||
let mut replica_id = ReplicaId(1);
|
||||
while replica_ids.contains(&replica_id) {
|
||||
replica_id.0 += 1;
|
||||
}
|
||||
let new_collaborator = project_collaborator::ActiveModel {
|
||||
project_id: ActiveValue::set(project_id),
|
||||
connection_id: ActiveValue::set(connection.id as i32),
|
||||
connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)),
|
||||
user_id: ActiveValue::set(participant.user_id),
|
||||
replica_id: ActiveValue::set(replica_id),
|
||||
is_host: ActiveValue::set(false),
|
||||
..Default::default()
|
||||
}
|
||||
.insert(&*tx)
|
||||
.await?;
|
||||
collaborators.push(new_collaborator);
|
||||
|
||||
let db_worktrees = project.find_related(worktree::Entity).all(&*tx).await?;
|
||||
let mut worktrees = db_worktrees
|
||||
let connection_ids = collaborators
|
||||
.into_iter()
|
||||
.map(|db_worktree| {
|
||||
(
|
||||
db_worktree.id as u64,
|
||||
Worktree {
|
||||
id: db_worktree.id as u64,
|
||||
abs_path: db_worktree.abs_path,
|
||||
root_name: db_worktree.root_name,
|
||||
visible: db_worktree.visible,
|
||||
entries: Default::default(),
|
||||
repository_entries: Default::default(),
|
||||
diagnostic_summaries: Default::default(),
|
||||
settings_files: Default::default(),
|
||||
scan_id: db_worktree.scan_id as u64,
|
||||
completed_scan_id: db_worktree.completed_scan_id as u64,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
// Populate worktree entries.
|
||||
{
|
||||
let mut db_entries = worktree_entry::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(worktree_entry::Column::ProjectId.eq(project_id))
|
||||
.add(worktree_entry::Column::IsDeleted.eq(false)),
|
||||
)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
while let Some(db_entry) = db_entries.next().await {
|
||||
let db_entry = db_entry?;
|
||||
if let Some(worktree) = worktrees.get_mut(&(db_entry.worktree_id as u64)) {
|
||||
worktree.entries.push(proto::Entry {
|
||||
id: db_entry.id as u64,
|
||||
is_dir: db_entry.is_dir,
|
||||
path: db_entry.path,
|
||||
inode: db_entry.inode as u64,
|
||||
mtime: Some(proto::Timestamp {
|
||||
seconds: db_entry.mtime_seconds as u64,
|
||||
nanos: db_entry.mtime_nanos as u32,
|
||||
}),
|
||||
is_symlink: db_entry.is_symlink,
|
||||
is_ignored: db_entry.is_ignored,
|
||||
is_external: db_entry.is_external,
|
||||
git_status: db_entry.git_status.map(|status| status as i32),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Populate repository entries.
|
||||
{
|
||||
let mut db_repository_entries = worktree_repository::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(worktree_repository::Column::ProjectId.eq(project_id))
|
||||
.add(worktree_repository::Column::IsDeleted.eq(false)),
|
||||
)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
while let Some(db_repository_entry) = db_repository_entries.next().await {
|
||||
let db_repository_entry = db_repository_entry?;
|
||||
if let Some(worktree) =
|
||||
worktrees.get_mut(&(db_repository_entry.worktree_id as u64))
|
||||
{
|
||||
worktree.repository_entries.insert(
|
||||
db_repository_entry.work_directory_id as u64,
|
||||
proto::RepositoryEntry {
|
||||
work_directory_id: db_repository_entry.work_directory_id as u64,
|
||||
branch: db_repository_entry.branch,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Populate worktree diagnostic summaries.
|
||||
{
|
||||
let mut db_summaries = worktree_diagnostic_summary::Entity::find()
|
||||
.filter(worktree_diagnostic_summary::Column::ProjectId.eq(project_id))
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
while let Some(db_summary) = db_summaries.next().await {
|
||||
let db_summary = db_summary?;
|
||||
if let Some(worktree) = worktrees.get_mut(&(db_summary.worktree_id as u64)) {
|
||||
worktree
|
||||
.diagnostic_summaries
|
||||
.push(proto::DiagnosticSummary {
|
||||
path: db_summary.path,
|
||||
language_server_id: db_summary.language_server_id as u64,
|
||||
error_count: db_summary.error_count as u32,
|
||||
warning_count: db_summary.warning_count as u32,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Populate worktree settings files
|
||||
{
|
||||
let mut db_settings_files = worktree_settings_file::Entity::find()
|
||||
.filter(worktree_settings_file::Column::ProjectId.eq(project_id))
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
while let Some(db_settings_file) = db_settings_files.next().await {
|
||||
let db_settings_file = db_settings_file?;
|
||||
if let Some(worktree) =
|
||||
worktrees.get_mut(&(db_settings_file.worktree_id as u64))
|
||||
{
|
||||
worktree.settings_files.push(WorktreeSettingsFile {
|
||||
path: db_settings_file.path,
|
||||
content: db_settings_file.content,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Populate language servers.
|
||||
let language_servers = project
|
||||
.find_related(language_server::Entity)
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
let project = Project {
|
||||
collaborators: collaborators
|
||||
.into_iter()
|
||||
.map(|collaborator| ProjectCollaborator {
|
||||
connection_id: collaborator.connection(),
|
||||
user_id: collaborator.user_id,
|
||||
replica_id: collaborator.replica_id,
|
||||
is_host: collaborator.is_host,
|
||||
})
|
||||
.collect(),
|
||||
worktrees,
|
||||
language_servers: language_servers
|
||||
.into_iter()
|
||||
.map(|language_server| proto::LanguageServer {
|
||||
id: language_server.id as u64,
|
||||
name: language_server.name,
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
Ok((project, replica_id as ReplicaId))
|
||||
.map(|collaborator| collaborator.connection())
|
||||
.collect();
|
||||
Ok(LeftProject {
|
||||
id: project.id,
|
||||
connection_ids,
|
||||
host_user_id: None,
|
||||
host_connection_id: None,
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -774,7 +871,7 @@ impl Database {
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
let room = self.get_room(project.room_id, &tx).await?;
|
||||
let room = self.get_room(room_id, &tx).await?;
|
||||
let left_project = LeftProject {
|
||||
id: project_id,
|
||||
host_user_id: project.host_user_id,
|
||||
@@ -998,7 +1095,9 @@ impl Database {
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("project {} not found", project_id))?;
|
||||
Ok(project.room_id)
|
||||
Ok(project
|
||||
.room_id
|
||||
.ok_or_else(|| anyhow!("project not in room"))?)
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -1061,7 +1160,7 @@ impl Database {
|
||||
.insert(&*tx)
|
||||
.await?;
|
||||
|
||||
let room = self.get_room(room_id, &*tx).await?;
|
||||
let room = self.get_room(room_id, &tx).await?;
|
||||
Ok(room)
|
||||
})
|
||||
.await
|
||||
@@ -1095,7 +1194,7 @@ impl Database {
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
let room = self.get_room(room_id, &*tx).await?;
|
||||
let room = self.get_room(room_id, &tx).await?;
|
||||
Ok(room)
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -321,7 +321,7 @@ impl Database {
|
||||
}
|
||||
|
||||
let participant_index = self
|
||||
.get_next_participant_index_internal(room_id, &*tx)
|
||||
.get_next_participant_index_internal(room_id, &tx)
|
||||
.await?;
|
||||
|
||||
let result = room_participant::Entity::update_many()
|
||||
@@ -374,7 +374,7 @@ impl Database {
|
||||
.select_only()
|
||||
.column(room_participant::Column::ParticipantIndex)
|
||||
.into_values::<_, QueryParticipantIndices>()
|
||||
.all(&*tx)
|
||||
.all(tx)
|
||||
.await?;
|
||||
|
||||
let mut participant_index = 0;
|
||||
@@ -407,7 +407,7 @@ impl Database {
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<JoinRoom> {
|
||||
let participant_index = self
|
||||
.get_next_participant_index_internal(room_id, &*tx)
|
||||
.get_next_participant_index_internal(room_id, tx)
|
||||
.await?;
|
||||
|
||||
room_participant::Entity::insert_many([room_participant::ActiveModel {
|
||||
@@ -441,12 +441,12 @@ impl Database {
|
||||
])
|
||||
.to_owned(),
|
||||
)
|
||||
.exec(&*tx)
|
||||
.exec(tx)
|
||||
.await?;
|
||||
|
||||
let (channel, room) = self.get_channel_room(room_id, &tx).await?;
|
||||
let channel = channel.ok_or_else(|| anyhow!("no channel for room"))?;
|
||||
let channel_members = self.get_channel_participants(&channel, &*tx).await?;
|
||||
let channel_members = self.get_channel_participants(&channel, tx).await?;
|
||||
Ok(JoinRoom {
|
||||
room,
|
||||
channel_id: Some(channel.id),
|
||||
@@ -491,7 +491,7 @@ impl Database {
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("project does not exist"))?;
|
||||
if project.host_user_id != user_id {
|
||||
if project.host_user_id != Some(user_id) {
|
||||
return Err(anyhow!("no such project"))?;
|
||||
}
|
||||
|
||||
@@ -851,7 +851,7 @@ impl Database {
|
||||
}
|
||||
|
||||
if collaborator.is_host {
|
||||
left_project.host_user_id = collaborator.user_id;
|
||||
left_project.host_user_id = Some(collaborator.user_id);
|
||||
left_project.host_connection_id = Some(collaborator_connection_id);
|
||||
}
|
||||
}
|
||||
@@ -1010,7 +1010,7 @@ impl Database {
|
||||
.ok_or_else(|| anyhow!("only admins can set participant role"))?;
|
||||
|
||||
if role.requires_cla() {
|
||||
self.check_user_has_signed_cla(user_id, room_id, &*tx)
|
||||
self.check_user_has_signed_cla(user_id, room_id, &tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -1021,7 +1021,7 @@ impl Database {
|
||||
.add(room_participant::Column::UserId.eq(user_id)),
|
||||
)
|
||||
.set(room_participant::ActiveModel {
|
||||
role: ActiveValue::set(Some(ChannelRole::from(role))),
|
||||
role: ActiveValue::set(Some(role)),
|
||||
..Default::default()
|
||||
})
|
||||
.exec(&*tx)
|
||||
@@ -1030,7 +1030,7 @@ impl Database {
|
||||
if result.rows_affected != 1 {
|
||||
Err(anyhow!("could not update room participant role"))?;
|
||||
}
|
||||
Ok(self.get_room(room_id, &tx).await?)
|
||||
self.get_room(room_id, &tx).await
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -1042,11 +1042,11 @@ impl Database {
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<()> {
|
||||
let channel = room::Entity::find_by_id(room_id)
|
||||
.one(&*tx)
|
||||
.one(tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("could not find room"))?
|
||||
.find_related(channel::Entity)
|
||||
.one(&*tx)
|
||||
.one(tx)
|
||||
.await?;
|
||||
|
||||
if let Some(channel) = channel {
|
||||
@@ -1057,13 +1057,13 @@ impl Database {
|
||||
.is_in(channel.ancestors())
|
||||
.and(channel::Column::RequiresZedCla.eq(true)),
|
||||
)
|
||||
.count(&*tx)
|
||||
.count(tx)
|
||||
.await?
|
||||
> 0;
|
||||
if requires_zed_cla {
|
||||
if contributor::Entity::find()
|
||||
.filter(contributor::Column::UserId.eq(user_id))
|
||||
.one(&*tx)
|
||||
.one(tx)
|
||||
.await?
|
||||
.is_none()
|
||||
{
|
||||
@@ -1076,10 +1076,9 @@ impl Database {
|
||||
|
||||
pub async fn connection_lost(&self, connection: ConnectionId) -> Result<()> {
|
||||
self.transaction(|tx| async move {
|
||||
self.room_connection_lost(connection, &*tx).await?;
|
||||
self.channel_buffer_connection_lost(connection, &*tx)
|
||||
.await?;
|
||||
self.channel_chat_connection_lost(connection, &*tx).await?;
|
||||
self.room_connection_lost(connection, &tx).await?;
|
||||
self.channel_buffer_connection_lost(connection, &tx).await?;
|
||||
self.channel_chat_connection_lost(connection, &tx).await?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
@@ -1099,7 +1098,7 @@ impl Database {
|
||||
.eq(connection.owner_id as i32),
|
||||
),
|
||||
)
|
||||
.one(&*tx)
|
||||
.one(tx)
|
||||
.await?;
|
||||
|
||||
if let Some(participant) = participant {
|
||||
@@ -1107,7 +1106,7 @@ impl Database {
|
||||
answering_connection_lost: ActiveValue::set(true),
|
||||
..participant.into_active_model()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.exec(tx)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -1296,7 +1295,7 @@ impl Database {
|
||||
drop(db_followers);
|
||||
|
||||
let channel = if let Some(channel_id) = db_room.channel_id {
|
||||
Some(self.get_channel_internal(channel_id, &*tx).await?)
|
||||
Some(self.get_channel_internal(channel_id, tx).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
@@ -98,7 +98,7 @@ impl Database {
|
||||
.add(server::Column::Environment.eq(environment))
|
||||
.add(server::Column::Id.ne(new_server_id)),
|
||||
)
|
||||
.all(&*tx)
|
||||
.all(tx)
|
||||
.await?;
|
||||
Ok(stale_servers.into_iter().map(|server| server.id).collect())
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ impl Database {
|
||||
github_login,
|
||||
github_user_id,
|
||||
github_email,
|
||||
&*tx,
|
||||
&tx,
|
||||
)
|
||||
.await
|
||||
})
|
||||
@@ -122,7 +122,7 @@ impl Database {
|
||||
metrics_id: ActiveValue::set(Uuid::new_v4()),
|
||||
..Default::default()
|
||||
})
|
||||
.exec_with_returning(&*tx)
|
||||
.exec_with_returning(tx)
|
||||
.await?;
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ pub struct Model {
|
||||
pub id: BufferId,
|
||||
pub epoch: i32,
|
||||
pub channel_id: ChannelId,
|
||||
pub latest_operation_epoch: Option<i32>,
|
||||
pub latest_operation_lamport_timestamp: Option<i32>,
|
||||
pub latest_operation_replica_id: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
||||
@@ -15,4 +15,13 @@ pub struct Model {
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_one = "super::project::Entity")]
|
||||
Project,
|
||||
}
|
||||
|
||||
impl Related<super::project::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Project.def()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::db::{ProjectId, Result, RoomId, ServerId, UserId};
|
||||
use crate::db::{HostedProjectId, ProjectId, Result, RoomId, ServerId, UserId};
|
||||
use anyhow::anyhow;
|
||||
use rpc::ConnectionId;
|
||||
use sea_orm::entity::prelude::*;
|
||||
@@ -8,10 +8,11 @@ use sea_orm::entity::prelude::*;
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: ProjectId,
|
||||
pub room_id: RoomId,
|
||||
pub host_user_id: UserId,
|
||||
pub room_id: Option<RoomId>,
|
||||
pub host_user_id: Option<UserId>,
|
||||
pub host_connection_id: Option<i32>,
|
||||
pub host_connection_server_id: Option<ServerId>,
|
||||
pub hosted_project_id: Option<HostedProjectId>,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
@@ -49,6 +50,12 @@ pub enum Relation {
|
||||
Collaborators,
|
||||
#[sea_orm(has_many = "super::language_server::Entity")]
|
||||
LanguageServers,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::hosted_project::Entity",
|
||||
from = "Column::HostedProjectId",
|
||||
to = "super::hosted_project::Column::Id"
|
||||
)]
|
||||
HostedProject,
|
||||
}
|
||||
|
||||
impl Related<super::user::Entity> for Entity {
|
||||
@@ -81,4 +88,10 @@ impl Related<super::language_server::Entity> for Entity {
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::hosted_project::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::HostedProject.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
@@ -23,7 +23,7 @@ pub struct TestDb {
|
||||
|
||||
impl TestDb {
|
||||
pub fn sqlite(background: BackgroundExecutor) -> Self {
|
||||
let url = format!("sqlite::memory:");
|
||||
let url = "sqlite::memory:";
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_io()
|
||||
.enable_time()
|
||||
@@ -109,13 +109,13 @@ macro_rules! test_both_dbs {
|
||||
($test_name:ident, $postgres_test_name:ident, $sqlite_test_name:ident) => {
|
||||
#[gpui::test]
|
||||
async fn $postgres_test_name(cx: &mut gpui::TestAppContext) {
|
||||
let test_db = crate::db::TestDb::postgres(cx.executor().clone());
|
||||
let test_db = $crate::db::TestDb::postgres(cx.executor().clone());
|
||||
$test_name(test_db.db()).await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn $sqlite_test_name(cx: &mut gpui::TestAppContext) {
|
||||
let test_db = crate::db::TestDb::sqlite(cx.executor().clone());
|
||||
let test_db = $crate::db::TestDb::sqlite(cx.executor().clone());
|
||||
$test_name(test_db.db()).await;
|
||||
}
|
||||
};
|
||||
@@ -168,7 +168,7 @@ async fn new_test_user(db: &Arc<Database>, email: &str) -> UserId {
|
||||
email,
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: email[0..email.find("@").unwrap()].to_string(),
|
||||
github_login: email[0..email.find('@').unwrap()].to_string(),
|
||||
github_user_id: GITHUB_USER_ID.fetch_add(1, SeqCst),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -68,11 +68,12 @@ async fn test_channel_buffers(db: &Arc<Database>) {
|
||||
.unwrap();
|
||||
|
||||
let mut buffer_a = Buffer::new(0, text::BufferId::new(1).unwrap(), "".to_string());
|
||||
let mut operations = Vec::new();
|
||||
operations.push(buffer_a.edit([(0..0, "hello world")]));
|
||||
operations.push(buffer_a.edit([(5..5, ", cruel")]));
|
||||
operations.push(buffer_a.edit([(0..5, "goodbye")]));
|
||||
operations.push(buffer_a.undo().unwrap().1);
|
||||
let operations = vec![
|
||||
buffer_a.edit([(0..0, "hello world")]),
|
||||
buffer_a.edit([(5..5, ", cruel")]),
|
||||
buffer_a.edit([(0..5, "goodbye")]),
|
||||
buffer_a.undo().unwrap().1,
|
||||
];
|
||||
assert_eq!(buffer_a.text(), "hello, cruel world");
|
||||
|
||||
let operations = operations
|
||||
@@ -222,7 +223,7 @@ async fn test_channel_buffers_last_operations(db: &Database) {
|
||||
.unwrap();
|
||||
|
||||
buffers.push(
|
||||
db.transaction(|tx| async move { db.get_channel_buffer(channel, &*tx).await })
|
||||
db.transaction(|tx| async move { db.get_channel_buffer(channel, &tx).await })
|
||||
.await
|
||||
.unwrap(),
|
||||
);
|
||||
@@ -234,19 +235,6 @@ async fn test_channel_buffers_last_operations(db: &Database) {
|
||||
));
|
||||
}
|
||||
|
||||
let operations = db
|
||||
.transaction(|tx| {
|
||||
let buffers = &buffers;
|
||||
async move {
|
||||
db.get_latest_operations_for_buffers([buffers[0].id, buffers[2].id], &*tx)
|
||||
.await
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(operations.is_empty());
|
||||
|
||||
update_buffer(
|
||||
buffers[0].channel_id,
|
||||
user_id,
|
||||
@@ -298,57 +286,10 @@ async fn test_channel_buffers_last_operations(db: &Database) {
|
||||
)
|
||||
.await;
|
||||
|
||||
let operations = db
|
||||
.transaction(|tx| {
|
||||
let buffers = &buffers;
|
||||
async move {
|
||||
db.get_latest_operations_for_buffers([buffers[1].id, buffers[2].id], &*tx)
|
||||
.await
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_operations(
|
||||
&operations,
|
||||
&[
|
||||
(buffers[1].id, 1, &text_buffers[1]),
|
||||
(buffers[2].id, 0, &text_buffers[2]),
|
||||
],
|
||||
);
|
||||
|
||||
let operations = db
|
||||
.transaction(|tx| {
|
||||
let buffers = &buffers;
|
||||
async move {
|
||||
db.get_latest_operations_for_buffers([buffers[0].id, buffers[1].id], &*tx)
|
||||
.await
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_operations(
|
||||
&operations,
|
||||
&[
|
||||
(buffers[0].id, 0, &text_buffers[0]),
|
||||
(buffers[1].id, 1, &text_buffers[1]),
|
||||
],
|
||||
);
|
||||
|
||||
let buffer_changes = db
|
||||
.transaction(|tx| {
|
||||
let buffers = &buffers;
|
||||
let mut hash = HashMap::default();
|
||||
hash.insert(buffers[0].id, buffers[0].channel_id);
|
||||
hash.insert(buffers[1].id, buffers[1].channel_id);
|
||||
hash.insert(buffers[2].id, buffers[2].channel_id);
|
||||
|
||||
async move { db.latest_channel_buffer_changes(&hash, &*tx).await }
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let channels_for_user = db.get_channels_for_user(user_id).await.unwrap();
|
||||
|
||||
pretty_assertions::assert_eq!(
|
||||
buffer_changes,
|
||||
channels_for_user.latest_buffer_versions,
|
||||
[
|
||||
rpc::proto::ChannelBufferVersion {
|
||||
channel_id: buffers[0].channel_id.to_proto(),
|
||||
@@ -360,8 +301,7 @@ async fn test_channel_buffers_last_operations(db: &Database) {
|
||||
epoch: 1,
|
||||
version: serialize_version(&text_buffers[1].version())
|
||||
.into_iter()
|
||||
.filter(|vector| vector.replica_id
|
||||
== buffer_changes[1].version.first().unwrap().replica_id)
|
||||
.filter(|vector| vector.replica_id == text_buffers[1].replica_id() as u32)
|
||||
.collect::<Vec<_>>(),
|
||||
},
|
||||
rpc::proto::ChannelBufferVersion {
|
||||
@@ -387,30 +327,3 @@ async fn update_buffer(
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn assert_operations(
|
||||
operations: &[buffer_operation::Model],
|
||||
expected: &[(BufferId, i32, &text::Buffer)],
|
||||
) {
|
||||
let actual = operations
|
||||
.iter()
|
||||
.map(|op| buffer_operation::Model {
|
||||
buffer_id: op.buffer_id,
|
||||
epoch: op.epoch,
|
||||
lamport_timestamp: op.lamport_timestamp,
|
||||
replica_id: op.replica_id,
|
||||
value: vec![],
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let expected = expected
|
||||
.iter()
|
||||
.map(|(buffer_id, epoch, buffer)| buffer_operation::Model {
|
||||
buffer_id: *buffer_id,
|
||||
epoch: *epoch,
|
||||
lamport_timestamp: buffer.lamport_clock.value as i32 - 1,
|
||||
replica_id: buffer.replica_id() as i32,
|
||||
value: vec![],
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(actual, expected, "unexpected operations")
|
||||
}
|
||||
|
||||
@@ -42,8 +42,8 @@ async fn test_channels(db: &Arc<Database>) {
|
||||
|
||||
let mut members = db
|
||||
.transaction(|tx| async move {
|
||||
let channel = db.get_channel_internal(replace_id, &*tx).await?;
|
||||
Ok(db.get_channel_participants(&channel, &*tx).await?)
|
||||
let channel = db.get_channel_internal(replace_id, &tx).await?;
|
||||
db.get_channel_participants(&channel, &tx).await
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -464,9 +464,9 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
|
||||
db.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(
|
||||
&db.get_channel_internal(public_channel_id, &*tx).await?,
|
||||
&db.get_channel_internal(public_channel_id, &tx).await?,
|
||||
admin,
|
||||
&*tx,
|
||||
&tx,
|
||||
)
|
||||
.await
|
||||
})
|
||||
@@ -474,9 +474,9 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
.unwrap();
|
||||
db.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(
|
||||
&db.get_channel_internal(public_channel_id, &*tx).await?,
|
||||
&db.get_channel_internal(public_channel_id, &tx).await?,
|
||||
member,
|
||||
&*tx,
|
||||
&tx,
|
||||
)
|
||||
.await
|
||||
})
|
||||
@@ -517,9 +517,9 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
|
||||
db.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(
|
||||
&db.get_channel_internal(public_channel_id, &*tx).await?,
|
||||
&db.get_channel_internal(public_channel_id, &tx).await?,
|
||||
guest,
|
||||
&*tx,
|
||||
&tx,
|
||||
)
|
||||
.await
|
||||
})
|
||||
@@ -547,11 +547,11 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
assert!(db
|
||||
.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(
|
||||
&db.get_channel_internal(public_channel_id, &*tx)
|
||||
&db.get_channel_internal(public_channel_id, &tx)
|
||||
.await
|
||||
.unwrap(),
|
||||
guest,
|
||||
&*tx,
|
||||
&tx,
|
||||
)
|
||||
.await
|
||||
})
|
||||
@@ -629,9 +629,9 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
|
||||
db.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(
|
||||
&db.get_channel_internal(zed_channel, &*tx).await.unwrap(),
|
||||
&db.get_channel_internal(zed_channel, &tx).await.unwrap(),
|
||||
guest,
|
||||
&*tx,
|
||||
&tx,
|
||||
)
|
||||
.await
|
||||
})
|
||||
@@ -640,11 +640,11 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
assert!(db
|
||||
.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(
|
||||
&db.get_channel_internal(internal_channel_id, &*tx)
|
||||
&db.get_channel_internal(internal_channel_id, &tx)
|
||||
.await
|
||||
.unwrap(),
|
||||
guest,
|
||||
&*tx,
|
||||
&tx,
|
||||
)
|
||||
.await
|
||||
})
|
||||
@@ -653,11 +653,11 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
|
||||
db.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(
|
||||
&db.get_channel_internal(public_channel_id, &*tx)
|
||||
&db.get_channel_internal(public_channel_id, &tx)
|
||||
.await
|
||||
.unwrap(),
|
||||
guest,
|
||||
&*tx,
|
||||
&tx,
|
||||
)
|
||||
.await
|
||||
})
|
||||
|
||||
@@ -10,16 +10,15 @@ test_both_dbs!(
|
||||
|
||||
async fn test_contributors(db: &Arc<Database>) {
|
||||
db.create_user(
|
||||
&format!("user1@example.com"),
|
||||
"user1@example.com",
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: format!("user1"),
|
||||
github_login: "user1".to_string(),
|
||||
github_user_id: 1,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(db.get_contributors().await.unwrap(), Vec::<String>::new());
|
||||
|
||||
|
||||
@@ -87,8 +87,7 @@ async fn test_get_or_create_user_by_github_account(db: &Arc<Database>) {
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
.unwrap();
|
||||
let user_id2 = db
|
||||
.create_user(
|
||||
"user2@example.com",
|
||||
@@ -495,7 +494,7 @@ async fn test_project_count(db: &Arc<Database>) {
|
||||
|
||||
let user1 = db
|
||||
.create_user(
|
||||
&format!("admin@example.com"),
|
||||
"admin@example.com",
|
||||
true,
|
||||
NewUserParams {
|
||||
github_login: "admin".into(),
|
||||
@@ -506,7 +505,7 @@ async fn test_project_count(db: &Arc<Database>) {
|
||||
.unwrap();
|
||||
let user2 = db
|
||||
.create_user(
|
||||
&format!("user@example.com"),
|
||||
"user@example.com",
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "user".into(),
|
||||
|
||||
@@ -13,10 +13,10 @@ test_both_dbs!(
|
||||
async fn test_get_user_flags(db: &Arc<Database>) {
|
||||
let user_1 = db
|
||||
.create_user(
|
||||
&format!("user1@example.com"),
|
||||
"user1@example.com",
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: format!("user1"),
|
||||
github_login: "user1".to_string(),
|
||||
github_user_id: 1,
|
||||
},
|
||||
)
|
||||
@@ -26,10 +26,10 @@ async fn test_get_user_flags(db: &Arc<Database>) {
|
||||
|
||||
let user_2 = db
|
||||
.create_user(
|
||||
&format!("user2@example.com"),
|
||||
"user2@example.com",
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: format!("user2"),
|
||||
github_login: "user2".to_string(),
|
||||
github_user_id: 2,
|
||||
},
|
||||
)
|
||||
@@ -37,8 +37,8 @@ async fn test_get_user_flags(db: &Arc<Database>) {
|
||||
.unwrap()
|
||||
.user_id;
|
||||
|
||||
const CHANNELS_ALPHA: &'static str = "channels-alpha";
|
||||
const NEW_SEARCH: &'static str = "new-search";
|
||||
const CHANNELS_ALPHA: &str = "channels-alpha";
|
||||
const NEW_SEARCH: &str = "new-search";
|
||||
|
||||
let channels_flag = db.create_user_flag(CHANNELS_ALPHA).await.unwrap();
|
||||
let search_flag = db.create_user_flag(NEW_SEARCH).await.unwrap();
|
||||
|
||||
@@ -297,7 +297,7 @@ async fn test_unseen_channel_messages(db: &Arc<Database>) {
|
||||
// Check that observer has new messages
|
||||
let latest_messages = db
|
||||
.transaction(|tx| async move {
|
||||
db.latest_channel_messages(&[channel_1, channel_2], &*tx)
|
||||
db.latest_channel_messages(&[channel_1, channel_2], &tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -43,8 +43,8 @@ impl From<axum::Error> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<hyper::Error> for Error {
|
||||
fn from(error: hyper::Error) -> Self {
|
||||
impl From<axum::http::Error> for Error {
|
||||
fn from(error: axum::http::Error) -> Self {
|
||||
Self::Internal(error.into())
|
||||
}
|
||||
}
|
||||
@@ -127,6 +127,7 @@ pub struct Config {
|
||||
pub blob_store_bucket: Option<String>,
|
||||
pub zed_environment: Arc<str>,
|
||||
pub zed_client_checksum_seed: Option<String>,
|
||||
pub slack_panics_webhook: Option<String>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -175,7 +176,10 @@ impl AppState {
|
||||
db: Arc::new(db),
|
||||
live_kit_client,
|
||||
blob_store_client: build_blob_store_client(&config).await.log_err(),
|
||||
clickhouse_client: build_clickhouse_client(&config).log_err(),
|
||||
clickhouse_client: config
|
||||
.clickhouse_url
|
||||
.as_ref()
|
||||
.and_then(|_| build_clickhouse_client(&config).log_err()),
|
||||
config,
|
||||
};
|
||||
Ok(Arc::new(this))
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use anyhow::anyhow;
|
||||
use axum::{extract::MatchedPath, routing::get, Extension, Router};
|
||||
use axum::{extract::MatchedPath, http::Request, routing::get, Extension, Router};
|
||||
use collab::{
|
||||
api::fetch_extensions_from_blob_store_periodically, db, env, executor::Executor, AppState,
|
||||
Config, MigrateConfig, Result,
|
||||
};
|
||||
use db::Database;
|
||||
use hyper::Request;
|
||||
use std::{
|
||||
env::args,
|
||||
net::{SocketAddr, TcpListener},
|
||||
@@ -16,11 +15,12 @@ use std::{
|
||||
use tokio::signal::unix::SignalKind;
|
||||
use tower_http::trace::{self, TraceLayer};
|
||||
use tracing::Level;
|
||||
use tracing_log::LogTracer;
|
||||
use tracing_subscriber::{filter::EnvFilter, fmt::format::JsonFields, Layer};
|
||||
use tracing_subscriber::{
|
||||
filter::EnvFilter, fmt::format::JsonFields, util::SubscriberInitExt, Layer,
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
const REVISION: Option<&'static str> = option_env!("GITHUB_SHA");
|
||||
|
||||
#[tokio::main]
|
||||
@@ -111,7 +111,8 @@ async fn main() -> Result<()> {
|
||||
);
|
||||
|
||||
#[cfg(unix)]
|
||||
axum::Server::from_tcp(listener)?
|
||||
axum::Server::from_tcp(listener)
|
||||
.map_err(|e| anyhow!(e))?
|
||||
.serve(app.into_make_service_with_connect_info::<SocketAddr>())
|
||||
.with_graceful_shutdown(async move {
|
||||
let mut sigterm = tokio::signal::unix::signal(SignalKind::terminate())
|
||||
@@ -128,7 +129,8 @@ async fn main() -> Result<()> {
|
||||
rpc_server.teardown();
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|e| anyhow!(e))?;
|
||||
|
||||
// todo("windows")
|
||||
#[cfg(windows)]
|
||||
@@ -178,11 +180,10 @@ async fn handle_liveness_probe(Extension(state): Extension<Arc<AppState>>) -> Re
|
||||
pub fn init_tracing(config: &Config) -> Option<()> {
|
||||
use std::str::FromStr;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
let rust_log = config.rust_log.clone()?;
|
||||
|
||||
LogTracer::init().log_err()?;
|
||||
let filter = EnvFilter::from_str(config.rust_log.as_deref()?).log_err()?;
|
||||
|
||||
let subscriber = tracing_subscriber::Registry::default()
|
||||
tracing_subscriber::registry()
|
||||
.with(if config.log_json.unwrap_or(false) {
|
||||
Box::new(
|
||||
tracing_subscriber::fmt::layer()
|
||||
@@ -192,17 +193,17 @@ pub fn init_tracing(config: &Config) -> Option<()> {
|
||||
.json()
|
||||
.flatten_event(true)
|
||||
.with_span_list(true),
|
||||
),
|
||||
)
|
||||
.with_filter(filter),
|
||||
) as Box<dyn Layer<_> + Send + Sync>
|
||||
} else {
|
||||
Box::new(
|
||||
tracing_subscriber::fmt::layer()
|
||||
.event_format(tracing_subscriber::fmt::format().pretty()),
|
||||
.event_format(tracing_subscriber::fmt::format().pretty())
|
||||
.with_filter(filter),
|
||||
)
|
||||
})
|
||||
.with(EnvFilter::from_str(rust_log.as_str()).log_err()?);
|
||||
|
||||
tracing::subscriber::set_global_default(subscriber).unwrap();
|
||||
.init();
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@ use crate::{
|
||||
auth::{self, Impersonator},
|
||||
db::{
|
||||
self, BufferId, ChannelId, ChannelRole, ChannelsForUser, CreatedChannelMessage, Database,
|
||||
InviteMemberResult, MembershipUpdated, MessageId, NotificationId, ProjectId,
|
||||
RemoveChannelMemberResult, RespondToChannelInvite, RoomId, ServerId, User, UserId,
|
||||
InviteMemberResult, MembershipUpdated, MessageId, NotificationId, Project, ProjectId,
|
||||
RemoveChannelMemberResult, ReplicaId, RespondToChannelInvite, RoomId, ServerId, User,
|
||||
UserId,
|
||||
},
|
||||
executor::Executor,
|
||||
AppState, Error, Result,
|
||||
@@ -35,7 +36,6 @@ use futures::{
|
||||
stream::FuturesUnordered,
|
||||
FutureExt, SinkExt, StreamExt, TryStreamExt,
|
||||
};
|
||||
use lazy_static::lazy_static;
|
||||
use prometheus::{register_int_gauge, IntGauge};
|
||||
use rpc::{
|
||||
proto::{
|
||||
@@ -56,7 +56,7 @@ use std::{
|
||||
rc::Rc,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering::SeqCst},
|
||||
Arc,
|
||||
Arc, OnceLock,
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
@@ -67,22 +67,14 @@ use tracing::{field, info_span, instrument, Instrument};
|
||||
use util::SemanticVersion;
|
||||
|
||||
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
// kubernetes gives terminated pods 10s to shutdown gracefully. After they're gone, we can clean up old resources.
|
||||
pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(15);
|
||||
|
||||
const MESSAGE_COUNT_PER_PAGE: usize = 100;
|
||||
const MAX_MESSAGE_LEN: usize = 1024;
|
||||
const NOTIFICATION_COUNT_PER_PAGE: usize = 50;
|
||||
|
||||
lazy_static! {
|
||||
static ref METRIC_CONNECTIONS: IntGauge =
|
||||
register_int_gauge!("connections", "number of connections").unwrap();
|
||||
static ref METRIC_SHARED_PROJECTS: IntGauge = register_int_gauge!(
|
||||
"shared_projects",
|
||||
"number of open projects with one or more guests"
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
type MessageHandler =
|
||||
Box<dyn Send + Sync + Fn(Box<dyn AnyTypedEnvelope>, Session) -> BoxFuture<'static, ()>>;
|
||||
|
||||
@@ -158,7 +150,7 @@ pub struct Server {
|
||||
app_state: Arc<AppState>,
|
||||
executor: Executor,
|
||||
handlers: HashMap<TypeId, MessageHandler>,
|
||||
teardown: watch::Sender<()>,
|
||||
teardown: watch::Sender<bool>,
|
||||
}
|
||||
|
||||
pub(crate) struct ConnectionPoolGuard<'a> {
|
||||
@@ -191,7 +183,7 @@ impl Server {
|
||||
executor,
|
||||
connection_pool: Default::default(),
|
||||
handlers: Default::default(),
|
||||
teardown: watch::channel(()).0,
|
||||
teardown: watch::channel(false).0,
|
||||
};
|
||||
|
||||
server
|
||||
@@ -208,6 +200,7 @@ impl Server {
|
||||
.add_request_handler(share_project)
|
||||
.add_message_handler(unshare_project)
|
||||
.add_request_handler(join_project)
|
||||
.add_request_handler(join_hosted_project)
|
||||
.add_message_handler(leave_project)
|
||||
.add_request_handler(update_project)
|
||||
.add_request_handler(update_worktree)
|
||||
@@ -364,7 +357,7 @@ impl Server {
|
||||
&refreshed_room.room,
|
||||
&refreshed_room.channel_members,
|
||||
&peer,
|
||||
&*pool.lock(),
|
||||
&pool.lock(),
|
||||
);
|
||||
}
|
||||
contacts_to_update
|
||||
@@ -447,7 +440,7 @@ impl Server {
|
||||
pub fn teardown(&self) {
|
||||
self.peer.teardown();
|
||||
self.connection_pool.lock().reset();
|
||||
let _ = self.teardown.send(());
|
||||
let _ = self.teardown.send(true);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -455,6 +448,7 @@ impl Server {
|
||||
self.teardown();
|
||||
*self.id.lock() = id;
|
||||
self.peer.reset(id.0 as u32);
|
||||
let _ = self.teardown.send(false);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -472,6 +466,7 @@ impl Server {
|
||||
TypeId::of::<M>(),
|
||||
Box::new(move |envelope, session| {
|
||||
let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
|
||||
let received_at = envelope.received_at;
|
||||
let span = info_span!(
|
||||
"handle message",
|
||||
payload_type = envelope.payload_type_name()
|
||||
@@ -486,12 +481,14 @@ impl Server {
|
||||
let future = (handler)(*envelope, session);
|
||||
async move {
|
||||
let result = future.await;
|
||||
let duration_ms = start_time.elapsed().as_micros() as f64 / 1000.0;
|
||||
let total_duration_ms = received_at.elapsed().as_micros() as f64 / 1000.0;
|
||||
let processing_duration_ms = start_time.elapsed().as_micros() as f64 / 1000.0;
|
||||
let queue_duration_ms = total_duration_ms - processing_duration_ms;
|
||||
match result {
|
||||
Err(error) => {
|
||||
tracing::error!(%error, ?duration_ms, "error handling message")
|
||||
tracing::error!(%error, ?total_duration_ms, ?processing_duration_ms, ?queue_duration_ms, "error handling message")
|
||||
}
|
||||
Ok(()) => tracing::info!(?duration_ms, "finished handling message"),
|
||||
Ok(()) => tracing::info!(?total_duration_ms, ?processing_duration_ms, ?queue_duration_ms, "finished handling message"),
|
||||
}
|
||||
}
|
||||
.instrument(span)
|
||||
@@ -553,6 +550,7 @@ impl Server {
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn handle_connection(
|
||||
self: &Arc<Self>,
|
||||
connection: Connection,
|
||||
@@ -572,6 +570,9 @@ impl Server {
|
||||
}
|
||||
let mut teardown = self.teardown.subscribe();
|
||||
async move {
|
||||
if *teardown.borrow() {
|
||||
return Err(anyhow!("server is tearing down"))?;
|
||||
}
|
||||
let (connection_id, handle_io, mut incoming_rx) = this
|
||||
.peer
|
||||
.add_connection(connection, {
|
||||
@@ -760,13 +761,13 @@ impl<'a> Deref for ConnectionPoolGuard<'a> {
|
||||
type Target = ConnectionPool;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&*self.guard
|
||||
&self.guard
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> DerefMut for ConnectionPoolGuard<'a> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut *self.guard
|
||||
&mut self.guard
|
||||
}
|
||||
}
|
||||
|
||||
@@ -793,16 +794,12 @@ fn broadcast<F>(
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref ZED_PROTOCOL_VERSION: HeaderName = HeaderName::from_static("x-zed-protocol-version");
|
||||
static ref ZED_APP_VERSION: HeaderName = HeaderName::from_static("x-zed-app-version");
|
||||
}
|
||||
|
||||
pub struct ProtocolVersion(u32);
|
||||
|
||||
impl Header for ProtocolVersion {
|
||||
fn name() -> &'static HeaderName {
|
||||
&ZED_PROTOCOL_VERSION
|
||||
static ZED_PROTOCOL_VERSION: OnceLock<HeaderName> = OnceLock::new();
|
||||
ZED_PROTOCOL_VERSION.get_or_init(|| HeaderName::from_static("x-zed-protocol-version"))
|
||||
}
|
||||
|
||||
fn decode<'i, I>(values: &mut I) -> Result<Self, axum::headers::Error>
|
||||
@@ -828,7 +825,8 @@ impl Header for ProtocolVersion {
|
||||
pub struct AppVersionHeader(SemanticVersion);
|
||||
impl Header for AppVersionHeader {
|
||||
fn name() -> &'static HeaderName {
|
||||
&ZED_APP_VERSION
|
||||
static ZED_APP_VERSION: OnceLock<HeaderName> = OnceLock::new();
|
||||
ZED_APP_VERSION.get_or_init(|| HeaderName::from_static("x-zed-app-version"))
|
||||
}
|
||||
|
||||
fn decode<'i, I>(values: &mut I) -> Result<Self, axum::headers::Error>
|
||||
@@ -851,7 +849,7 @@ impl Header for AppVersionHeader {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn routes(server: Arc<Server>) -> Router<Body> {
|
||||
pub fn routes(server: Arc<Server>) -> Router<(), Body> {
|
||||
Router::new()
|
||||
.route("/rpc", get(handle_websocket_request))
|
||||
.layer(
|
||||
@@ -922,17 +920,29 @@ pub async fn handle_websocket_request(
|
||||
}
|
||||
|
||||
pub async fn handle_metrics(Extension(server): Extension<Arc<Server>>) -> Result<String> {
|
||||
static CONNECTIONS_METRIC: OnceLock<IntGauge> = OnceLock::new();
|
||||
let connections_metric = CONNECTIONS_METRIC
|
||||
.get_or_init(|| register_int_gauge!("connections", "number of connections").unwrap());
|
||||
|
||||
let connections = server
|
||||
.connection_pool
|
||||
.lock()
|
||||
.connections()
|
||||
.filter(|connection| !connection.admin)
|
||||
.count();
|
||||
connections_metric.set(connections as _);
|
||||
|
||||
METRIC_CONNECTIONS.set(connections as _);
|
||||
static SHARED_PROJECTS_METRIC: OnceLock<IntGauge> = OnceLock::new();
|
||||
let shared_projects_metric = SHARED_PROJECTS_METRIC.get_or_init(|| {
|
||||
register_int_gauge!(
|
||||
"shared_projects",
|
||||
"number of open projects with one or more guests"
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
let shared_projects = server.app_state.db.project_count_excluding_admins().await?;
|
||||
METRIC_SHARED_PROJECTS.set(shared_projects as _);
|
||||
shared_projects_metric.set(shared_projects as _);
|
||||
|
||||
let encoder = prometheus::TextEncoder::new();
|
||||
let metric_families = prometheus::gather();
|
||||
@@ -945,7 +955,7 @@ pub async fn handle_metrics(Extension(server): Extension<Arc<Server>>) -> Result
|
||||
#[instrument(err, skip(executor))]
|
||||
async fn connection_lost(
|
||||
session: Session,
|
||||
mut teardown: watch::Receiver<()>,
|
||||
mut teardown: watch::Receiver<bool>,
|
||||
executor: Executor,
|
||||
) -> Result<()> {
|
||||
session.peer.disconnect(session.connection_id);
|
||||
@@ -1581,22 +1591,46 @@ async fn join_project(
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let project_id = ProjectId::from_proto(request.project_id);
|
||||
let guest_user_id = session.user_id;
|
||||
|
||||
tracing::info!(%project_id, "join project");
|
||||
|
||||
let (project, replica_id) = &mut *session
|
||||
.db()
|
||||
.await
|
||||
.join_project(project_id, session.connection_id)
|
||||
.join_project_in_room(project_id, session.connection_id)
|
||||
.await?;
|
||||
|
||||
join_project_internal(response, session, project, replica_id)
|
||||
}
|
||||
|
||||
trait JoinProjectInternalResponse {
|
||||
fn send(self, result: proto::JoinProjectResponse) -> Result<()>;
|
||||
}
|
||||
impl JoinProjectInternalResponse for Response<proto::JoinProject> {
|
||||
fn send(self, result: proto::JoinProjectResponse) -> Result<()> {
|
||||
Response::<proto::JoinProject>::send(self, result)
|
||||
}
|
||||
}
|
||||
impl JoinProjectInternalResponse for Response<proto::JoinHostedProject> {
|
||||
fn send(self, result: proto::JoinProjectResponse) -> Result<()> {
|
||||
Response::<proto::JoinHostedProject>::send(self, result)
|
||||
}
|
||||
}
|
||||
|
||||
fn join_project_internal(
|
||||
response: impl JoinProjectInternalResponse,
|
||||
session: Session,
|
||||
project: &mut Project,
|
||||
replica_id: &ReplicaId,
|
||||
) -> Result<()> {
|
||||
let collaborators = project
|
||||
.collaborators
|
||||
.iter()
|
||||
.filter(|collaborator| collaborator.connection_id != session.connection_id)
|
||||
.map(|collaborator| collaborator.to_proto())
|
||||
.collect::<Vec<_>>();
|
||||
let project_id = project.id;
|
||||
let guest_user_id = session.user_id;
|
||||
|
||||
let worktrees = project
|
||||
.worktrees
|
||||
@@ -1628,10 +1662,12 @@ async fn join_project(
|
||||
|
||||
// First, we send the metadata associated with each worktree.
|
||||
response.send(proto::JoinProjectResponse {
|
||||
project_id: project.id.0 as u64,
|
||||
worktrees: worktrees.clone(),
|
||||
replica_id: replica_id.0 as u32,
|
||||
collaborators: collaborators.clone(),
|
||||
language_servers: project.language_servers.clone(),
|
||||
role: project.role.into(), // todo
|
||||
})?;
|
||||
|
||||
for (worktree_id, worktree) in mem::take(&mut project.worktrees) {
|
||||
@@ -1704,15 +1740,17 @@ async fn join_project(
|
||||
async fn leave_project(request: proto::LeaveProject, session: Session) -> Result<()> {
|
||||
let sender_id = session.connection_id;
|
||||
let project_id = ProjectId::from_proto(request.project_id);
|
||||
let db = session.db().await;
|
||||
if db.is_hosted_project(project_id).await? {
|
||||
let project = db.leave_hosted_project(project_id, sender_id).await?;
|
||||
project_left(&project, &session);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (room, project) = &*session
|
||||
.db()
|
||||
.await
|
||||
.leave_project(project_id, sender_id)
|
||||
.await?;
|
||||
let (room, project) = &*db.leave_project(project_id, sender_id).await?;
|
||||
tracing::info!(
|
||||
%project_id,
|
||||
host_user_id = %project.host_user_id,
|
||||
host_user_id = ?project.host_user_id,
|
||||
host_connection_id = ?project.host_connection_id,
|
||||
"leave project"
|
||||
);
|
||||
@@ -1723,6 +1761,24 @@ async fn leave_project(request: proto::LeaveProject, session: Session) -> Result
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn join_hosted_project(
|
||||
request: proto::JoinHostedProject,
|
||||
response: Response<proto::JoinHostedProject>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let (mut project, replica_id) = session
|
||||
.db()
|
||||
.await
|
||||
.join_hosted_project(
|
||||
ProjectId(request.project_id as i32),
|
||||
session.user_id,
|
||||
session.connection_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
join_project_internal(response, session, &mut project, &replica_id)
|
||||
}
|
||||
|
||||
/// Updates other participants with changes to the project
|
||||
async fn update_project(
|
||||
request: proto::UpdateProject,
|
||||
@@ -2223,7 +2279,7 @@ async fn request_contact(
|
||||
session.peer.send(connection_id, update.clone())?;
|
||||
}
|
||||
|
||||
send_notifications(&*connection_pool, &session.peer, notifications);
|
||||
send_notifications(&connection_pool, &session.peer, notifications);
|
||||
|
||||
response.send(proto::Ack {})?;
|
||||
Ok(())
|
||||
@@ -2280,7 +2336,7 @@ async fn respond_to_contact_request(
|
||||
session.peer.send(connection_id, update.clone())?;
|
||||
}
|
||||
|
||||
send_notifications(&*pool, &session.peer, notifications);
|
||||
send_notifications(&pool, &session.peer, notifications);
|
||||
}
|
||||
|
||||
response.send(proto::Ack {})?;
|
||||
@@ -2448,7 +2504,7 @@ async fn invite_channel_member(
|
||||
session.peer.send(connection_id, update.clone())?;
|
||||
}
|
||||
|
||||
send_notifications(&*connection_pool, &session.peer, notifications);
|
||||
send_notifications(&connection_pool, &session.peer, notifications);
|
||||
|
||||
response.send(proto::Ack {})?;
|
||||
Ok(())
|
||||
@@ -2710,7 +2766,7 @@ async fn respond_to_channel_invite(
|
||||
}
|
||||
};
|
||||
|
||||
send_notifications(&*connection_pool, &session.peer, notifications);
|
||||
send_notifications(&connection_pool, &session.peer, notifications);
|
||||
|
||||
response.send(proto::Ack {})?;
|
||||
|
||||
@@ -2880,7 +2936,7 @@ async fn update_channel_buffer(
|
||||
.flat_map(|user_id| pool.user_connection_ids(*user_id)),
|
||||
|peer_id| {
|
||||
session.peer.send(
|
||||
peer_id.into(),
|
||||
peer_id,
|
||||
proto::UpdateChannels {
|
||||
latest_channel_buffer_versions: vec![proto::ChannelBufferVersion {
|
||||
channel_id: channel_id.to_proto(),
|
||||
@@ -2965,8 +3021,8 @@ fn channel_buffer_updated<T: EnvelopedMessage>(
|
||||
message: &T,
|
||||
peer: &Peer,
|
||||
) {
|
||||
broadcast(Some(sender_id), collaborators.into_iter(), |peer_id| {
|
||||
peer.send(peer_id.into(), message.clone())
|
||||
broadcast(Some(sender_id), collaborators, |peer_id| {
|
||||
peer.send(peer_id, message.clone())
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3071,7 +3127,7 @@ async fn send_channel_message(
|
||||
.flat_map(|user_id| pool.user_connection_ids(*user_id)),
|
||||
|peer_id| {
|
||||
session.peer.send(
|
||||
peer_id.into(),
|
||||
peer_id,
|
||||
proto::UpdateChannels {
|
||||
latest_channel_message_ids: vec![proto::ChannelMessageId {
|
||||
channel_id: channel_id.to_proto(),
|
||||
@@ -3367,7 +3423,6 @@ fn build_update_user_channels(channels: &ChannelsForUser) -> proto::UpdateUserCh
|
||||
.collect(),
|
||||
observed_channel_buffer_version: channels.observed_buffer_versions.clone(),
|
||||
observed_channel_message_id: channels.observed_channel_messages.clone(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3444,7 +3499,7 @@ fn room_updated(room: &proto::Room, peer: &Peer) {
|
||||
.filter_map(|participant| Some(participant.peer_id?.into())),
|
||||
|peer_id| {
|
||||
peer.send(
|
||||
peer_id.into(),
|
||||
peer_id,
|
||||
proto::RoomUpdated {
|
||||
room: Some(room.clone()),
|
||||
},
|
||||
@@ -3473,7 +3528,7 @@ fn channel_updated(
|
||||
.flat_map(|user_id| pool.user_connection_ids(*user_id)),
|
||||
|peer_id| {
|
||||
peer.send(
|
||||
peer_id.into(),
|
||||
peer_id,
|
||||
proto::UpdateChannels {
|
||||
channel_participants: vec![proto::ChannelParticipants {
|
||||
channel_id: channel_id.to_proto(),
|
||||
@@ -3622,7 +3677,7 @@ async fn leave_channel_buffers_for_session(session: &Session) -> Result<()> {
|
||||
|
||||
fn project_left(project: &db::LeftProject, session: &Session) {
|
||||
for connection_id in &project.connection_ids {
|
||||
if project.host_user_id == session.user_id {
|
||||
if project.host_user_id == Some(session.user_id) {
|
||||
session
|
||||
.peer
|
||||
.send(
|
||||
|
||||
@@ -94,21 +94,19 @@ impl ConnectionPool {
|
||||
self.connected_users
|
||||
.get(&user_id)
|
||||
.into_iter()
|
||||
.map(|state| {
|
||||
.flat_map(|state| {
|
||||
state
|
||||
.connection_ids
|
||||
.iter()
|
||||
.flat_map(|cid| self.connections.get(cid))
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
|
||||
pub fn user_connection_ids(&self, user_id: UserId) -> impl Iterator<Item = ConnectionId> + '_ {
|
||||
self.connected_users
|
||||
.get(&user_id)
|
||||
.into_iter()
|
||||
.map(|state| &state.connection_ids)
|
||||
.flatten()
|
||||
.flat_map(|state| &state.connection_ids)
|
||||
.copied()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use call::Room;
|
||||
use client::ChannelId;
|
||||
use gpui::{Model, TestAppContext};
|
||||
@@ -15,6 +17,7 @@ mod random_project_collaboration_tests;
|
||||
mod randomized_test_helpers;
|
||||
mod test_server;
|
||||
|
||||
use language::{tree_sitter_rust, Language, LanguageConfig, LanguageMatcher};
|
||||
pub use randomized_test_helpers::{
|
||||
run_randomized_test, save_randomized_test_plan, RandomizedTest, TestError, UserTestPlan,
|
||||
};
|
||||
@@ -47,3 +50,17 @@ fn room_participants(room: &Model<Room>, cx: &mut TestAppContext) -> RoomPartici
|
||||
fn channel_id(room: &Model<Room>, cx: &mut TestAppContext) -> Option<ChannelId> {
|
||||
cx.read(|cx| room.read(cx).channel_id())
|
||||
}
|
||||
|
||||
fn rust_lang() -> Arc<Language> {
|
||||
Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -1431,7 +1431,7 @@ fn assert_channels(
|
||||
.ordered_channels()
|
||||
.map(|(depth, channel)| ExpectedChannel {
|
||||
depth,
|
||||
name: channel.name.clone().into(),
|
||||
name: channel.name.clone(),
|
||||
id: channel.id,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
use std::{
|
||||
path::Path,
|
||||
sync::{
|
||||
atomic::{self, AtomicBool, AtomicUsize},
|
||||
Arc,
|
||||
},
|
||||
use crate::{
|
||||
rpc::RECONNECT_TIMEOUT,
|
||||
tests::{rust_lang, TestServer},
|
||||
};
|
||||
|
||||
use call::ActiveCall;
|
||||
use editor::{
|
||||
actions::{
|
||||
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Redo, Rename, ToggleCodeActions, Undo,
|
||||
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Redo, Rename, RevertSelectedHunks,
|
||||
ToggleCodeActions, Undo,
|
||||
},
|
||||
test::editor_test_context::{AssertionContextManager, EditorTestContext},
|
||||
Editor,
|
||||
@@ -19,16 +16,22 @@ use gpui::{TestAppContext, VisualContext, VisualTestContext};
|
||||
use indoc::indoc;
|
||||
use language::{
|
||||
language_settings::{AllLanguageSettings, InlayHintSettings},
|
||||
tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher,
|
||||
FakeLspAdapter,
|
||||
};
|
||||
use project::SERVER_PROGRESS_DEBOUNCE_TIMEOUT;
|
||||
use rpc::RECEIVE_TIMEOUT;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::{
|
||||
path::Path,
|
||||
sync::{
|
||||
atomic::{self, AtomicBool, AtomicUsize},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use text::Point;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_host_disconnect(
|
||||
cx_a: &mut TestAppContext,
|
||||
@@ -265,20 +268,10 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
// Set up a fake language server.
|
||||
let mut language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let mut fake_language_servers = language
|
||||
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||||
client_a.language_registry().add(rust_lang());
|
||||
let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
capabilities: lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string()]),
|
||||
@@ -288,9 +281,8 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
client_a.language_registry().add(Arc::new(language));
|
||||
},
|
||||
);
|
||||
|
||||
client_a
|
||||
.fs()
|
||||
@@ -455,19 +447,10 @@ async fn test_collaborating_with_code_actions(
|
||||
cx_b.update(editor::init);
|
||||
|
||||
// Set up a fake language server.
|
||||
let mut language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
|
||||
client_a.language_registry().add(Arc::new(language));
|
||||
client_a.language_registry().add(rust_lang());
|
||||
let mut fake_language_servers = client_a
|
||||
.language_registry()
|
||||
.register_fake_lsp_adapter("Rust", FakeLspAdapter::default());
|
||||
|
||||
client_a
|
||||
.fs()
|
||||
@@ -671,19 +654,10 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
cx_b.update(editor::init);
|
||||
|
||||
// Set up a fake language server.
|
||||
let mut language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let mut fake_language_servers = language
|
||||
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||||
client_a.language_registry().add(rust_lang());
|
||||
let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
capabilities: lsp::ServerCapabilities {
|
||||
rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
|
||||
prepare_provider: Some(true),
|
||||
@@ -692,9 +666,8 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
client_a.language_registry().add(Arc::new(language));
|
||||
},
|
||||
);
|
||||
|
||||
client_a
|
||||
.fs()
|
||||
@@ -858,25 +831,14 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
|
||||
cx_b.update(editor::init);
|
||||
|
||||
// Set up a fake language server.
|
||||
let mut language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let mut fake_language_servers = language
|
||||
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||||
client_a.language_registry().add(rust_lang());
|
||||
let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
name: "the-language-server",
|
||||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
client_a.language_registry().add(Arc::new(language));
|
||||
},
|
||||
);
|
||||
|
||||
client_a
|
||||
.fs()
|
||||
@@ -905,6 +867,7 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
},
|
||||
)),
|
||||
});
|
||||
executor.advance_clock(SERVER_PROGRESS_DEBOUNCE_TIMEOUT);
|
||||
executor.run_until_parked();
|
||||
|
||||
project_a.read_with(cx_a, |project, _| {
|
||||
@@ -938,6 +901,7 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
},
|
||||
)),
|
||||
});
|
||||
executor.advance_clock(SERVER_PROGRESS_DEBOUNCE_TIMEOUT);
|
||||
executor.run_until_parked();
|
||||
|
||||
project_a.read_with(cx_a, |project, _| {
|
||||
@@ -1152,20 +1116,10 @@ async fn test_on_input_format_from_host_to_guest(
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
// Set up a fake language server.
|
||||
let mut language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let mut fake_language_servers = language
|
||||
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||||
client_a.language_registry().add(rust_lang());
|
||||
let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
capabilities: lsp::ServerCapabilities {
|
||||
document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
|
||||
first_trigger_character: ":".to_string(),
|
||||
@@ -1174,9 +1128,8 @@ async fn test_on_input_format_from_host_to_guest(
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
client_a.language_registry().add(Arc::new(language));
|
||||
},
|
||||
);
|
||||
|
||||
client_a
|
||||
.fs()
|
||||
@@ -1283,20 +1236,10 @@ async fn test_on_input_format_from_guest_to_host(
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
// Set up a fake language server.
|
||||
let mut language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let mut fake_language_servers = language
|
||||
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||||
client_a.language_registry().add(rust_lang());
|
||||
let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
capabilities: lsp::ServerCapabilities {
|
||||
document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
|
||||
first_trigger_character: ":".to_string(),
|
||||
@@ -1305,9 +1248,8 @@ async fn test_on_input_format_from_guest_to_host(
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
client_a.language_registry().add(Arc::new(language));
|
||||
},
|
||||
);
|
||||
|
||||
client_a
|
||||
.fs()
|
||||
@@ -1450,29 +1392,18 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
||||
});
|
||||
});
|
||||
|
||||
let mut language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let mut fake_language_servers = language
|
||||
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||||
client_a.language_registry().add(rust_lang());
|
||||
client_b.language_registry().add(rust_lang());
|
||||
let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
capabilities: lsp::ServerCapabilities {
|
||||
inlay_hint_provider: Some(lsp::OneOf::Left(true)),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
let language = Arc::new(language);
|
||||
client_a.language_registry().add(Arc::clone(&language));
|
||||
client_b.language_registry().add(language);
|
||||
},
|
||||
);
|
||||
|
||||
// Client A opens a project.
|
||||
client_a
|
||||
@@ -1723,29 +1654,18 @@ async fn test_inlay_hint_refresh_is_forwarded(
|
||||
});
|
||||
});
|
||||
|
||||
let mut language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let mut fake_language_servers = language
|
||||
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||||
client_a.language_registry().add(rust_lang());
|
||||
client_b.language_registry().add(rust_lang());
|
||||
let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
capabilities: lsp::ServerCapabilities {
|
||||
inlay_hint_provider: Some(lsp::OneOf::Left(true)),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
let language = Arc::new(language);
|
||||
client_a.language_registry().add(Arc::clone(&language));
|
||||
client_b.language_registry().add(language);
|
||||
},
|
||||
);
|
||||
|
||||
client_a
|
||||
.fs()
|
||||
@@ -1895,6 +1815,171 @@ async fn test_inlay_hint_refresh_is_forwarded(
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_multiple_types_reverts(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
let mut server = TestServer::start(cx_a.executor()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
cx_a.update(editor::init);
|
||||
cx_b.update(editor::init);
|
||||
|
||||
client_a.language_registry().add(rust_lang());
|
||||
client_b.language_registry().add(rust_lang());
|
||||
|
||||
let base_text = indoc! {r#"struct Row;
|
||||
struct Row1;
|
||||
struct Row2;
|
||||
|
||||
struct Row4;
|
||||
struct Row5;
|
||||
struct Row6;
|
||||
|
||||
struct Row8;
|
||||
struct Row9;
|
||||
struct Row10;"#};
|
||||
|
||||
client_a
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
"main.rs": base_text,
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
|
||||
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
|
||||
|
||||
let editor_a = workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
let editor_b = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
let mut editor_cx_a = EditorTestContext {
|
||||
cx: cx_a.clone(),
|
||||
window: cx_a.handle(),
|
||||
editor: editor_a,
|
||||
assertion_cx: AssertionContextManager::new(),
|
||||
};
|
||||
let mut editor_cx_b = EditorTestContext {
|
||||
cx: cx_b.clone(),
|
||||
window: cx_b.handle(),
|
||||
editor: editor_b,
|
||||
assertion_cx: AssertionContextManager::new(),
|
||||
};
|
||||
|
||||
// host edits the file, that differs from the base text, producing diff hunks
|
||||
editor_cx_a.set_state(indoc! {r#"struct Row;
|
||||
struct Row0.1;
|
||||
struct Row0.2;
|
||||
struct Row1;
|
||||
|
||||
struct Row4;
|
||||
struct Row5444;
|
||||
struct Row6;
|
||||
|
||||
struct Row9;
|
||||
struct Row1220;ˇ"#});
|
||||
editor_cx_a.update_editor(|editor, cx| {
|
||||
editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.unwrap()
|
||||
.update(cx, |buffer, cx| {
|
||||
buffer.set_diff_base(Some(base_text.to_string()), cx);
|
||||
});
|
||||
});
|
||||
editor_cx_b.update_editor(|editor, cx| {
|
||||
editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.unwrap()
|
||||
.update(cx, |buffer, cx| {
|
||||
buffer.set_diff_base(Some(base_text.to_string()), cx);
|
||||
});
|
||||
});
|
||||
cx_a.executor().run_until_parked();
|
||||
cx_b.executor().run_until_parked();
|
||||
|
||||
// client, selects a range in the updated buffer, and reverts it
|
||||
// both host and the client observe the reverted state (with one hunk left, not covered by client's selection)
|
||||
editor_cx_b.set_selections_state(indoc! {r#"«ˇstruct Row;
|
||||
struct Row0.1;
|
||||
struct Row0.2;
|
||||
struct Row1;
|
||||
|
||||
struct Row4;
|
||||
struct Row5444;
|
||||
struct Row6;
|
||||
|
||||
struct R»ow9;
|
||||
struct Row1220;"#});
|
||||
editor_cx_b.update_editor(|editor, cx| {
|
||||
editor.revert_selected_hunks(&RevertSelectedHunks, cx);
|
||||
});
|
||||
cx_a.executor().run_until_parked();
|
||||
cx_b.executor().run_until_parked();
|
||||
editor_cx_a.assert_editor_state(indoc! {r#"struct Row;
|
||||
struct Row1;
|
||||
struct Row2;
|
||||
|
||||
struct Row4;
|
||||
struct Row5;
|
||||
struct Row6;
|
||||
|
||||
struct Row8;
|
||||
struct Row9;
|
||||
struct Row1220;ˇ"#});
|
||||
editor_cx_b.assert_editor_state(indoc! {r#"«ˇstruct Row;
|
||||
struct Row1;
|
||||
struct Row2;
|
||||
|
||||
struct Row4;
|
||||
struct Row5;
|
||||
struct Row6;
|
||||
|
||||
struct Row8;
|
||||
struct R»ow9;
|
||||
struct Row1220;"#});
|
||||
}
|
||||
|
||||
fn extract_hint_labels(editor: &Editor) -> Vec<String> {
|
||||
let mut labels = Vec::new();
|
||||
for hint in editor.inlay_hint_cache().hints() {
|
||||
|
||||
@@ -373,8 +373,10 @@ async fn test_basic_following(
|
||||
editor_a1.update(cx_a, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
|
||||
});
|
||||
executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
|
||||
executor.run_until_parked();
|
||||
cx_b.background_executor.run_until_parked();
|
||||
|
||||
editor_b1.update(cx_b, |editor, cx| {
|
||||
assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
|
||||
});
|
||||
@@ -387,6 +389,7 @@ async fn test_basic_following(
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
|
||||
editor.set_scroll_position(point(0., 100.), cx);
|
||||
});
|
||||
executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
|
||||
executor.run_until_parked();
|
||||
editor_b1.update(cx_b, |editor, cx| {
|
||||
assert_eq!(editor.selections.ranges(cx), &[3..3]);
|
||||
@@ -1437,14 +1440,13 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
|
||||
});
|
||||
|
||||
executor.run_until_parked();
|
||||
let window_b_project_a = cx_b
|
||||
let window_b_project_a = *cx_b
|
||||
.windows()
|
||||
.iter()
|
||||
.max_by_key(|window| window.window_id())
|
||||
.unwrap()
|
||||
.clone();
|
||||
.unwrap();
|
||||
|
||||
let mut cx_b2 = VisualTestContext::from_window(window_b_project_a.clone(), cx_b);
|
||||
let mut cx_b2 = VisualTestContext::from_window(window_b_project_a, cx_b);
|
||||
|
||||
let workspace_b_project_a = window_b_project_a
|
||||
.downcast::<Workspace>()
|
||||
@@ -1535,7 +1537,7 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
|
||||
executor.run_until_parked();
|
||||
assert_eq!(visible_push_notifications(cx_a).len(), 1);
|
||||
cx_a.update(|cx| {
|
||||
workspace::join_remote_project(
|
||||
workspace::join_in_room_project(
|
||||
project_b_id,
|
||||
client_b.user_id().unwrap(),
|
||||
client_a.app_state.clone(),
|
||||
@@ -1548,13 +1550,12 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
|
||||
executor.run_until_parked();
|
||||
|
||||
assert_eq!(visible_push_notifications(cx_a).len(), 0);
|
||||
let window_a_project_b = cx_a
|
||||
let window_a_project_b = *cx_a
|
||||
.windows()
|
||||
.iter()
|
||||
.max_by_key(|window| window.window_id())
|
||||
.unwrap()
|
||||
.clone();
|
||||
let cx_a2 = &mut VisualTestContext::from_window(window_a_project_b.clone(), cx_a);
|
||||
.unwrap();
|
||||
let cx_a2 = &mut VisualTestContext::from_window(window_a_project_b, cx_a);
|
||||
let workspace_a_project_b = window_a_project_b
|
||||
.downcast::<Workspace>()
|
||||
.unwrap()
|
||||
@@ -1578,7 +1579,7 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
let (_, client_a, client_b, channel_id) = TestServer::start2(cx_a, cx_b).await;
|
||||
let (_server, client_a, client_b, channel_id) = TestServer::start2(cx_a, cx_b).await;
|
||||
|
||||
let (workspace_a, cx_a) = client_a.build_test_workspace(cx_a).await;
|
||||
client_a
|
||||
@@ -1600,6 +1601,8 @@ async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
editor_a.update(cx_a, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([1..1]))
|
||||
});
|
||||
cx_a.executor()
|
||||
.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
|
||||
cx_a.run_until_parked();
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
assert_eq!(editor.selections.ranges(cx), vec![1..1])
|
||||
@@ -1618,6 +1621,8 @@ async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
editor_a.update(cx_a, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([2..2]))
|
||||
});
|
||||
cx_a.executor()
|
||||
.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
|
||||
cx_a.run_until_parked();
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
assert_eq!(editor.selections.ranges(cx), vec![1..1])
|
||||
@@ -1722,6 +1727,7 @@ async fn test_following_into_excluded_file(
|
||||
|
||||
// When client B starts following client A, currently visible file is replicated
|
||||
workspace_b.update(cx_b, |workspace, cx| workspace.follow(peer_id_a, cx));
|
||||
executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
|
||||
executor.run_until_parked();
|
||||
|
||||
let editor_for_excluded_b = workspace_b.update(cx_b, |workspace, cx| {
|
||||
@@ -1743,6 +1749,7 @@ async fn test_following_into_excluded_file(
|
||||
editor_for_excluded_a.update(cx_a, |editor, cx| {
|
||||
editor.select_right(&Default::default(), cx);
|
||||
});
|
||||
executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
|
||||
executor.run_until_parked();
|
||||
|
||||
// Changes from B to the excluded file are replicated in A's editor
|
||||
@@ -2024,7 +2031,7 @@ async fn test_following_to_channel_notes_other_workspace(
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
let (_, client_a, client_b, channel) = TestServer::start2(cx_a, cx_b).await;
|
||||
let (_server, client_a, client_b, channel) = TestServer::start2(cx_a, cx_b).await;
|
||||
|
||||
let mut cx_a2 = cx_a.clone();
|
||||
let (workspace_a, cx_a) = client_a.build_test_workspace(cx_a).await;
|
||||
@@ -2081,7 +2088,7 @@ async fn test_following_to_channel_notes_other_workspace(
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_following_while_deactivated(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
let (_, client_a, client_b, channel) = TestServer::start2(cx_a, cx_b).await;
|
||||
let (_server, client_a, client_b, channel) = TestServer::start2(cx_a, cx_b).await;
|
||||
|
||||
let mut cx_a2 = cx_a.clone();
|
||||
let (workspace_a, cx_a) = client_a.build_test_workspace(cx_a).await;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
|
||||
tests::{channel_id, room_participants, RoomParticipants, TestClient, TestServer},
|
||||
tests::{channel_id, room_participants, rust_lang, RoomParticipants, TestClient, TestServer},
|
||||
};
|
||||
use call::{room, ActiveCall, ParticipantLocation, Room};
|
||||
use client::{User, RECEIVE_TIMEOUT};
|
||||
@@ -22,7 +22,6 @@ use project::{
|
||||
search::SearchQuery, DiagnosticSummary, FormatTrigger, HoverBlockKind, Project, ProjectPath,
|
||||
};
|
||||
use rand::prelude::*;
|
||||
use rpc::proto::ChannelRole;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::{
|
||||
@@ -2821,8 +2820,8 @@ async fn test_git_status_sync(
|
||||
)
|
||||
.await;
|
||||
|
||||
const A_TXT: &'static str = "a.txt";
|
||||
const B_TXT: &'static str = "b.txt";
|
||||
const A_TXT: &str = "a.txt";
|
||||
const B_TXT: &str = "b.txt";
|
||||
|
||||
client_a.fs().set_status_for_repo_via_git_operation(
|
||||
Path::new("/dir/.git"),
|
||||
@@ -3742,7 +3741,6 @@ async fn test_leaving_project(
|
||||
client_b.user_store().clone(),
|
||||
client_b.language_registry().clone(),
|
||||
FakeFs::new(cx.background_executor().clone()),
|
||||
ChannelRole::Member,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -3785,8 +3783,7 @@ async fn test_collaborating_with_diagnostics(
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
// Set up a fake language server.
|
||||
let mut language = Language::new(
|
||||
client_a.language_registry().add(Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
@@ -3796,9 +3793,10 @@ async fn test_collaborating_with_diagnostics(
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
|
||||
client_a.language_registry().add(Arc::new(language));
|
||||
)));
|
||||
let mut fake_language_servers = client_a
|
||||
.language_registry()
|
||||
.register_fake_lsp_adapter("Rust", Default::default());
|
||||
|
||||
// Share a project as client A
|
||||
client_a
|
||||
@@ -3885,7 +3883,6 @@ async fn test_collaborating_with_diagnostics(
|
||||
DiagnosticSummary {
|
||||
error_count: 1,
|
||||
warning_count: 0,
|
||||
..Default::default()
|
||||
},
|
||||
)]
|
||||
)
|
||||
@@ -3922,7 +3919,6 @@ async fn test_collaborating_with_diagnostics(
|
||||
DiagnosticSummary {
|
||||
error_count: 1,
|
||||
warning_count: 0,
|
||||
..Default::default()
|
||||
},
|
||||
)]
|
||||
);
|
||||
@@ -4066,26 +4062,15 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
|
||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
|
||||
// Set up a fake language server.
|
||||
let mut language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let mut fake_language_servers = language
|
||||
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||||
client_a.language_registry().add(rust_lang());
|
||||
let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
disk_based_diagnostics_progress_token: Some("the-disk-based-token".into()),
|
||||
disk_based_diagnostics_sources: vec!["the-disk-based-diagnostics-source".into()],
|
||||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
client_a.language_registry().add(Arc::new(language));
|
||||
},
|
||||
);
|
||||
|
||||
let file_names = &["one.rs", "two.rs", "three.rs", "four.rs", "five.rs"];
|
||||
client_a
|
||||
@@ -4298,20 +4283,10 @@ async fn test_formatting_buffer(
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
// Set up a fake language server.
|
||||
let mut language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
|
||||
client_a.language_registry().add(Arc::new(language));
|
||||
client_a.language_registry().add(rust_lang());
|
||||
let mut fake_language_servers = client_a
|
||||
.language_registry()
|
||||
.register_fake_lsp_adapter("Rust", FakeLspAdapter::default());
|
||||
|
||||
// Here we insert a fake tree with a directory that exists on disk. This is needed
|
||||
// because later we'll invoke a command, which requires passing a working directory
|
||||
@@ -4406,8 +4381,9 @@ async fn test_prettier_formatting_buffer(
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
// Set up a fake language server.
|
||||
let mut language = Language::new(
|
||||
let test_plugin = "test_plugin";
|
||||
|
||||
client_a.language_registry().add(Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
@@ -4418,16 +4394,14 @@ async fn test_prettier_formatting_buffer(
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let test_plugin = "test_plugin";
|
||||
let mut fake_language_servers = language
|
||||
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||||
)));
|
||||
let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
prettier_plugins: vec![test_plugin],
|
||||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
let language = Arc::new(language);
|
||||
client_a.language_registry().add(Arc::clone(&language));
|
||||
},
|
||||
);
|
||||
|
||||
// Here we insert a fake tree with a directory that exists on disk. This is needed
|
||||
// because later we'll invoke a command, which requires passing a working directory
|
||||
@@ -4525,20 +4499,10 @@ async fn test_definition(
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
// Set up a fake language server.
|
||||
let mut language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
|
||||
client_a.language_registry().add(Arc::new(language));
|
||||
let mut fake_language_servers = client_a
|
||||
.language_registry()
|
||||
.register_fake_lsp_adapter("Rust", Default::default());
|
||||
client_a.language_registry().add(rust_lang());
|
||||
|
||||
client_a
|
||||
.fs()
|
||||
@@ -4672,20 +4636,10 @@ async fn test_references(
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
// Set up a fake language server.
|
||||
let mut language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
|
||||
client_a.language_registry().add(Arc::new(language));
|
||||
client_a.language_registry().add(rust_lang());
|
||||
let mut fake_language_servers = client_a
|
||||
.language_registry()
|
||||
.register_fake_lsp_adapter("Rust", Default::default());
|
||||
|
||||
client_a
|
||||
.fs()
|
||||
@@ -4872,20 +4826,10 @@ async fn test_document_highlights(
|
||||
)
|
||||
.await;
|
||||
|
||||
// Set up a fake language server.
|
||||
let mut language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
|
||||
client_a.language_registry().add(Arc::new(language));
|
||||
let mut fake_language_servers = client_a
|
||||
.language_registry()
|
||||
.register_fake_lsp_adapter("Rust", Default::default());
|
||||
client_a.language_registry().add(rust_lang());
|
||||
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
|
||||
let project_id = active_call_a
|
||||
@@ -4978,20 +4922,10 @@ async fn test_lsp_hover(
|
||||
)
|
||||
.await;
|
||||
|
||||
// Set up a fake language server.
|
||||
let mut language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
|
||||
client_a.language_registry().add(Arc::new(language));
|
||||
client_a.language_registry().add(rust_lang());
|
||||
let mut fake_language_servers = client_a
|
||||
.language_registry()
|
||||
.register_fake_lsp_adapter("Rust", Default::default());
|
||||
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
|
||||
let project_id = active_call_a
|
||||
@@ -5077,20 +5011,10 @@ async fn test_project_symbols(
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
// Set up a fake language server.
|
||||
let mut language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
|
||||
client_a.language_registry().add(Arc::new(language));
|
||||
client_a.language_registry().add(rust_lang());
|
||||
let mut fake_language_servers = client_a
|
||||
.language_registry()
|
||||
.register_fake_lsp_adapter("Rust", Default::default());
|
||||
|
||||
client_a
|
||||
.fs()
|
||||
@@ -5189,20 +5113,10 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
// Set up a fake language server.
|
||||
let mut language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
|
||||
client_a.language_registry().add(Arc::new(language));
|
||||
client_a.language_registry().add(rust_lang());
|
||||
let mut fake_language_servers = client_a
|
||||
.language_registry()
|
||||
.register_fake_lsp_adapter("Rust", Default::default());
|
||||
|
||||
client_a
|
||||
.fs()
|
||||
|
||||
@@ -996,7 +996,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
|
||||
let statuses = statuses
|
||||
.iter()
|
||||
.map(|(path, val)| (path.as_path(), val.clone()))
|
||||
.map(|(path, val)| (path.as_path(), *val))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if client.fs().metadata(&dot_git_dir).await?.is_none() {
|
||||
@@ -1021,7 +1021,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
}
|
||||
|
||||
async fn on_client_added(client: &Rc<TestClient>, _: &mut TestAppContext) {
|
||||
let mut language = Language::new(
|
||||
client.language_registry().add(Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
@@ -1031,9 +1031,10 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
..Default::default()
|
||||
},
|
||||
None,
|
||||
);
|
||||
language
|
||||
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||||
)));
|
||||
client.language_registry().register_fake_lsp_adapter(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
name: "the-fake-language-server",
|
||||
capabilities: lsp::LanguageServer::full_capabilities(),
|
||||
initializer: Some(Box::new({
|
||||
@@ -1132,9 +1133,8 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
}
|
||||
})),
|
||||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
client.app_state.languages.add(Arc::new(language));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async fn on_quiesce(_: &mut TestServer, clients: &mut [(Rc<TestClient>, TestAppContext)]) {
|
||||
@@ -1483,10 +1483,10 @@ fn project_for_root_name(
|
||||
root_name: &str,
|
||||
cx: &TestAppContext,
|
||||
) -> Option<Model<Project>> {
|
||||
if let Some(ix) = project_ix_for_root_name(&*client.local_projects().deref(), root_name, cx) {
|
||||
if let Some(ix) = project_ix_for_root_name(client.local_projects().deref(), root_name, cx) {
|
||||
return Some(client.local_projects()[ix].clone());
|
||||
}
|
||||
if let Some(ix) = project_ix_for_root_name(&*client.remote_projects().deref(), root_name, cx) {
|
||||
if let Some(ix) = project_ix_for_root_name(client.remote_projects().deref(), root_name, cx) {
|
||||
return Some(client.remote_projects()[ix].clone());
|
||||
}
|
||||
None
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user