Compare commits
141 Commits
piecemeal-
...
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 |
@@ -1,6 +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
|
||||
|
||||
98
.github/workflows/ci.yml
vendored
98
.github/workflows/ci.yml
vendored
@@ -86,12 +86,6 @@ jobs:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Install cargo-component
|
||||
run: |
|
||||
if ! which cargo-component > /dev/null; then
|
||||
cargo install cargo-component
|
||||
fi
|
||||
|
||||
- name: cargo clippy
|
||||
run: cargo xtask clippy
|
||||
|
||||
@@ -152,12 +146,12 @@ jobs:
|
||||
- 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 }}
|
||||
@@ -212,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
|
||||
@@ -232,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 }}
|
||||
|
||||
6
.github/workflows/deploy_collab.yml
vendored
6
.github/workflows/deploy_collab.yml
vendored
@@ -104,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
|
||||
@@ -120,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}"
|
||||
|
||||
55
.github/workflows/release_nightly.yml
vendored
55
.github/workflows/release_nightly.yml
vendored
@@ -50,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
|
||||
@@ -77,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
|
||||
@@ -90,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
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -23,3 +23,4 @@ DerivedData/
|
||||
.pytest_cache
|
||||
.venv
|
||||
.blob_store
|
||||
.vscode
|
||||
|
||||
376
Cargo.lock
generated
376
Cargo.lock
generated
@@ -113,9 +113,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "alacritty_terminal"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc7ceabf6fc76511f616ca216b51398a2511f19ba9f71bcbd977999edff1b0d1"
|
||||
version = "0.22.1-dev"
|
||||
source = "git+https://github.com/alacritty/alacritty?rev=992011a4cd9a35f197acc0a0bd430d89a0d01013#992011a4cd9a35f197acc0a0bd430d89a0d01013"
|
||||
dependencies = [
|
||||
"base64 0.21.4",
|
||||
"bitflags 2.4.2",
|
||||
@@ -1224,13 +1223,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.5.17"
|
||||
version = "0.6.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acee9fd5073ab6b045a275b3e709c163dd36c90685219cb21804a147b58dba43"
|
||||
checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum-core",
|
||||
"base64 0.13.1",
|
||||
"base64 0.21.4",
|
||||
"bitflags 1.3.2",
|
||||
"bytes 1.5.0",
|
||||
"futures-util",
|
||||
@@ -1244,24 +1243,25 @@ dependencies = [
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"serde_urlencoded",
|
||||
"sha-1 0.10.1",
|
||||
"sha1",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"tower",
|
||||
"tower-http 0.3.5",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-core"
|
||||
version = "0.2.9"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37e5939e02c56fecd5c017c37df4238c0a839fa76b7f97acdd7efb804fd181cc"
|
||||
checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes 1.5.0",
|
||||
@@ -1269,15 +1269,16 @@ dependencies = [
|
||||
"http 0.2.9",
|
||||
"http-body",
|
||||
"mime",
|
||||
"rustversion",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-extra"
|
||||
version = "0.3.7"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69034b3b0fd97923eee2ce8a47540edb21e07f48f87f67d44bb4271cec622bdb"
|
||||
checksum = "f9a320103719de37b7b4da4c8eb629d4573f6bcfd3dfe80d3208806895ccf81d"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"bytes 1.5.0",
|
||||
@@ -1451,7 +1452,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "blade-graphics"
|
||||
version = "0.3.0"
|
||||
source = "git+https://github.com/kvark/blade?rev=e9d93a4d41f3946a03ffb76136290d6ccf7f2b80#e9d93a4d41f3946a03ffb76136290d6ccf7f2b80"
|
||||
source = "git+https://github.com/kvark/blade?rev=43721bf42d298b7cbee2195ee66f73a5f1c7b2fc#43721bf42d298b7cbee2195ee66f73a5f1c7b2fc"
|
||||
dependencies = [
|
||||
"ash",
|
||||
"ash-window",
|
||||
@@ -1481,7 +1482,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "blade-macros"
|
||||
version = "0.2.1"
|
||||
source = "git+https://github.com/kvark/blade?rev=e9d93a4d41f3946a03ffb76136290d6ccf7f2b80#e9d93a4d41f3946a03ffb76136290d6ccf7f2b80"
|
||||
source = "git+https://github.com/kvark/blade?rev=43721bf42d298b7cbee2195ee66f73a5f1c7b2fc#43721bf42d298b7cbee2195ee66f73a5f1c7b2fc"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2221,6 +2222,7 @@ dependencies = [
|
||||
"aws-sdk-s3",
|
||||
"axum",
|
||||
"axum-extra",
|
||||
"base64 0.13.1",
|
||||
"call",
|
||||
"channel",
|
||||
"chrono",
|
||||
@@ -2240,7 +2242,6 @@ dependencies = [
|
||||
"git",
|
||||
"gpui",
|
||||
"hex",
|
||||
"hyper",
|
||||
"indoc",
|
||||
"language",
|
||||
"lazy_static",
|
||||
@@ -2271,6 +2272,7 @@ dependencies = [
|
||||
"settings",
|
||||
"sha2 0.10.7",
|
||||
"sqlx",
|
||||
"subtle",
|
||||
"telemetry_events",
|
||||
"text",
|
||||
"theme",
|
||||
@@ -2280,7 +2282,6 @@ dependencies = [
|
||||
"tower",
|
||||
"tower-http 0.4.4",
|
||||
"tracing",
|
||||
"tracing-log",
|
||||
"tracing-subscriber",
|
||||
"unindent",
|
||||
"util",
|
||||
@@ -2301,8 +2302,8 @@ dependencies = [
|
||||
"collections",
|
||||
"db",
|
||||
"editor",
|
||||
"emojis",
|
||||
"extensions_ui",
|
||||
"feedback",
|
||||
"futures 0.3.28",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
@@ -2977,6 +2978,12 @@ version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
|
||||
|
||||
[[package]]
|
||||
name = "data-url"
|
||||
version = "0.1.1"
|
||||
@@ -3003,15 +3010,6 @@ dependencies = [
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "debugid"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d"
|
||||
dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deflate"
|
||||
version = "0.8.6"
|
||||
@@ -3151,16 +3149,6 @@ dependencies = [
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "directories-next"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"dirs-sys-next",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "3.0.2"
|
||||
@@ -3275,6 +3263,7 @@ dependencies = [
|
||||
"copilot",
|
||||
"ctor",
|
||||
"db",
|
||||
"emojis",
|
||||
"env_logger",
|
||||
"futures 0.3.28",
|
||||
"fuzzy",
|
||||
@@ -3343,6 +3332,15 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "emojis"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ee61eb945bff65ee7d19d157d39c67c33290ff0742907413fd5eefd29edc979"
|
||||
dependencies = [
|
||||
"phf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
version = "0.3.6"
|
||||
@@ -3518,7 +3516,10 @@ dependencies = [
|
||||
"async-compression",
|
||||
"async-tar",
|
||||
"async-trait",
|
||||
"cap-std",
|
||||
"collections",
|
||||
"ctor",
|
||||
"env_logger",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
@@ -3526,6 +3527,7 @@ dependencies = [
|
||||
"log",
|
||||
"lsp",
|
||||
"node_runtime",
|
||||
"parking_lot 0.11.2",
|
||||
"project",
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -3534,22 +3536,28 @@ dependencies = [
|
||||
"theme",
|
||||
"toml 0.8.10",
|
||||
"util",
|
||||
"wasm-encoder",
|
||||
"wasmparser",
|
||||
"wasmtime",
|
||||
"wasmtime-wasi",
|
||||
"wit-component 0.20.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "extensions_ui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"editor",
|
||||
"extension",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"settings",
|
||||
"smallvec",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
@@ -3927,7 +3935,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "fsevent"
|
||||
version = "2.0.2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"fsevent-sys 3.1.0",
|
||||
@@ -4138,28 +4146,6 @@ dependencies = [
|
||||
"thread_local",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fxhash"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fxprof-processed-profile"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27d12c0aed7f1e24276a241aadc4cb8ea9f83000f34bc062b7cc2d51e3b0fabd"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"debugid",
|
||||
"fxhash",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
@@ -4301,9 +4287,16 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"editor",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"language",
|
||||
"menu",
|
||||
"project",
|
||||
"rope",
|
||||
"serde_json",
|
||||
"text",
|
||||
"theme",
|
||||
"tree-sitter-rust",
|
||||
"tree-sitter-typescript",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
@@ -5033,26 +5026,6 @@ version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
|
||||
|
||||
[[package]]
|
||||
name = "ittapi"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b996fe614c41395cdaedf3cf408a9534851090959d90d54a535f675550b64b1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"ittapi-sys",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ittapi-sys"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52f5385394064fa2c886205dba02598013ce83d3e92d33dbdc0c52fe0e7bf4fc"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.19.0"
|
||||
@@ -5331,7 +5304,6 @@ dependencies = [
|
||||
"tree-sitter-elm",
|
||||
"tree-sitter-embedded-template",
|
||||
"tree-sitter-erlang",
|
||||
"tree-sitter-gitcommit",
|
||||
"tree-sitter-gleam",
|
||||
"tree-sitter-glsl",
|
||||
"tree-sitter-go",
|
||||
@@ -5359,7 +5331,6 @@ dependencies = [
|
||||
"tree-sitter-svelte",
|
||||
"tree-sitter-toml",
|
||||
"tree-sitter-typescript",
|
||||
"tree-sitter-uiua",
|
||||
"tree-sitter-vue",
|
||||
"tree-sitter-yaml",
|
||||
"tree-sitter-zig",
|
||||
@@ -5699,9 +5670,9 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
version = "0.5.0"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73cbba799671b762df5a175adf59ce145165747bb891505c43d09aefbbf38beb"
|
||||
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
||||
|
||||
[[package]]
|
||||
name = "matrixmultiply"
|
||||
@@ -5899,9 +5870,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.8"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
|
||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
@@ -6164,7 +6135,7 @@ dependencies = [
|
||||
"kqueue",
|
||||
"libc",
|
||||
"log",
|
||||
"mio 0.8.8",
|
||||
"mio 0.8.11",
|
||||
"walkdir",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
@@ -6650,12 +6621,19 @@ dependencies = [
|
||||
"editor",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"language",
|
||||
"menu",
|
||||
"ordered-float 2.10.0",
|
||||
"picker",
|
||||
"project",
|
||||
"rope",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smol",
|
||||
"theme",
|
||||
"tree-sitter-rust",
|
||||
"tree-sitter-typescript",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
@@ -6887,10 +6865,29 @@ dependencies = [
|
||||
"indexmap 2.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
|
||||
dependencies = [
|
||||
"siphasher 0.3.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "picker"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"ctor",
|
||||
"editor",
|
||||
"env_logger",
|
||||
@@ -8707,6 +8704,16 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_path_to_error"
|
||||
version = "0.1.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebd154a240de39fdebcf5775d2675c204d7c13cf39a4c697be6493c8e734337c"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_repr"
|
||||
version = "0.1.16"
|
||||
@@ -8778,17 +8785,6 @@ dependencies = [
|
||||
"opaque-debug",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha-1"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cpufeatures",
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
@@ -8943,6 +8939,12 @@ version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac"
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.9"
|
||||
@@ -9585,7 +9587,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c536faaff1a10837cfe373142583f6e27d81e96beba339147e77b67c9f260ff"
|
||||
dependencies = [
|
||||
"float-cmp",
|
||||
"siphasher",
|
||||
"siphasher 0.2.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10070,7 +10072,7 @@ dependencies = [
|
||||
"backtrace",
|
||||
"bytes 1.5.0",
|
||||
"libc",
|
||||
"mio 0.8.8",
|
||||
"mio 0.8.11",
|
||||
"num_cpus",
|
||||
"parking_lot 0.12.1",
|
||||
"pin-project-lite",
|
||||
@@ -10135,14 +10137,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.17.2"
|
||||
version = "0.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f714dd15bead90401d77e04243611caec13726c2408afd5b31901dfcdcb3b181"
|
||||
checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"tokio",
|
||||
"tungstenite 0.17.3",
|
||||
"tungstenite 0.20.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10255,7 +10257,6 @@ dependencies = [
|
||||
"http-body",
|
||||
"http-range-header",
|
||||
"pin-project-lite",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
@@ -10508,15 +10509,6 @@ dependencies = [
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-gitcommit"
|
||||
version = "0.3.3"
|
||||
source = "git+https://github.com/gbprod/tree-sitter-gitcommit#7c01af8d227b5344f62aade2ff00f19bd0c458ca"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-gleam"
|
||||
version = "0.34.0"
|
||||
@@ -10776,15 +10768,6 @@ dependencies = [
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-uiua"
|
||||
version = "0.10.0"
|
||||
source = "git+https://github.com/shnarazk/tree-sitter-uiua?rev=21dc2db39494585bf29a3f86d5add6e9d11a22ba#21dc2db39494585bf29a3f86d5add6e9d11a22ba"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-vue"
|
||||
version = "0.0.1"
|
||||
@@ -10856,7 +10839,7 @@ dependencies = [
|
||||
"log",
|
||||
"native-tls",
|
||||
"rand 0.8.5",
|
||||
"sha-1 0.9.8",
|
||||
"sha-1",
|
||||
"thiserror",
|
||||
"url",
|
||||
"utf-8",
|
||||
@@ -10864,18 +10847,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.17.3"
|
||||
version = "0.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0"
|
||||
checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9"
|
||||
dependencies = [
|
||||
"base64 0.13.1",
|
||||
"byteorder",
|
||||
"bytes 1.5.0",
|
||||
"data-encoding",
|
||||
"http 0.2.9",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"sha-1 0.10.1",
|
||||
"sha1",
|
||||
"thiserror",
|
||||
"url",
|
||||
"utf-8",
|
||||
@@ -11063,7 +11046,7 @@ dependencies = [
|
||||
"roxmltree 0.14.1",
|
||||
"rustybuzz 0.3.0",
|
||||
"simplecss",
|
||||
"siphasher",
|
||||
"siphasher 0.2.3",
|
||||
"svgtypes",
|
||||
"ttf-parser 0.12.3",
|
||||
"unicode-bidi",
|
||||
@@ -11100,6 +11083,7 @@ dependencies = [
|
||||
"log",
|
||||
"parking_lot 0.11.2",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"rust-embed",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -11400,15 +11384,6 @@ dependencies = [
|
||||
"leb128",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-encoder"
|
||||
version = "0.201.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9c7d2731df60006819b013f64ccc2019691deccf6e11a1804bc850cd6748f1a"
|
||||
dependencies = [
|
||||
"leb128",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-metadata"
|
||||
version = "0.10.20"
|
||||
@@ -11421,7 +11396,7 @@ dependencies = [
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"spdx",
|
||||
"wasm-encoder 0.41.2",
|
||||
"wasm-encoder",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
@@ -11452,41 +11427,33 @@ version = "18.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c843b8bc4dd4f3a76173ba93405c71111d570af0d90ea5f6299c705d0c2add2"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"bincode",
|
||||
"bumpalo",
|
||||
"cfg-if 1.0.0",
|
||||
"encoding_rs",
|
||||
"fxprof-processed-profile",
|
||||
"gimli",
|
||||
"indexmap 2.0.0",
|
||||
"ittapi",
|
||||
"libc",
|
||||
"log",
|
||||
"object",
|
||||
"once_cell",
|
||||
"paste",
|
||||
"rayon",
|
||||
"rustix 0.38.30",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"target-lexicon",
|
||||
"wasm-encoder 0.41.2",
|
||||
"wasmparser",
|
||||
"wasmtime-cache",
|
||||
"wasmtime-component-macro",
|
||||
"wasmtime-component-util",
|
||||
"wasmtime-cranelift",
|
||||
"wasmtime-environ",
|
||||
"wasmtime-fiber",
|
||||
"wasmtime-jit-debug",
|
||||
"wasmtime-jit-icache-coherence",
|
||||
"wasmtime-runtime",
|
||||
"wasmtime-winch",
|
||||
"wat",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
@@ -11523,26 +11490,6 @@ dependencies = [
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmtime-cache"
|
||||
version = "18.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fb4fc2bbf9c790a57875eba65588fa97acf57a7d784dc86d057e648d9a1ed91"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.21.4",
|
||||
"bincode",
|
||||
"directories-next",
|
||||
"log",
|
||||
"rustix 0.38.30",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"sha2 0.10.7",
|
||||
"toml 0.5.11",
|
||||
"windows-sys 0.52.0",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmtime-component-macro"
|
||||
version = "18.0.2"
|
||||
@@ -11624,7 +11571,7 @@ dependencies = [
|
||||
"serde_derive",
|
||||
"target-lexicon",
|
||||
"thiserror",
|
||||
"wasm-encoder 0.41.2",
|
||||
"wasm-encoder",
|
||||
"wasmparser",
|
||||
"wasmprinter",
|
||||
"wasmtime-component-util",
|
||||
@@ -11646,18 +11593,6 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmtime-jit-debug"
|
||||
version = "18.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "833dae95bc7a4f9177bf93f9497419763535b74e37eb8c37be53937d3281e287"
|
||||
dependencies = [
|
||||
"object",
|
||||
"once_cell",
|
||||
"rustix 0.38.30",
|
||||
"wasmtime-versioned-export-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmtime-jit-icache-coherence"
|
||||
version = "18.0.2"
|
||||
@@ -11689,11 +11624,10 @@ dependencies = [
|
||||
"psm",
|
||||
"rustix 0.38.30",
|
||||
"sptr",
|
||||
"wasm-encoder 0.41.2",
|
||||
"wasm-encoder",
|
||||
"wasmtime-asm-macros",
|
||||
"wasmtime-environ",
|
||||
"wasmtime-fiber",
|
||||
"wasmtime-jit-debug",
|
||||
"wasmtime-versioned-export-macros",
|
||||
"wasmtime-wmemcheck",
|
||||
"windows-sys 0.52.0",
|
||||
@@ -11800,28 +11734,6 @@ dependencies = [
|
||||
"leb128",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wast"
|
||||
version = "201.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ef6e1ef34d7da3e2b374fd2b1a9c0227aff6cad596e1b24df9b58d0f6222faa"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"leb128",
|
||||
"memchr",
|
||||
"unicode-width",
|
||||
"wasm-encoder 0.201.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wat"
|
||||
version = "1.201.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "453d5b37a45b98dee4f4cb68015fc73634d7883bbef1c65e6e9c78d454cf3f32"
|
||||
dependencies = [
|
||||
"wast 201.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-backend"
|
||||
version = "0.3.3"
|
||||
@@ -12124,6 +12036,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "efc5cf48f83140dcaab716eeaea345f9e93d0018fb81162753a3f76c3397b538"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-targets 0.52.4",
|
||||
]
|
||||
|
||||
@@ -12137,6 +12051,28 @@ dependencies = [
|
||||
"windows-targets 0.52.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "942ac266be9249c84ca862f0a164a39533dc2f6f33dc98ec89c8da99b82ea0bd"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da33557140a288fae4e1d5f8873aaf9eb6613a9cf82c3e070223ff177f598b60"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.1.0"
|
||||
@@ -12421,7 +12357,7 @@ dependencies = [
|
||||
"heck 0.4.1",
|
||||
"wasm-metadata",
|
||||
"wit-bindgen-core",
|
||||
"wit-component",
|
||||
"wit-component 0.21.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -12438,6 +12374,25 @@ dependencies = [
|
||||
"wit-bindgen-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-component"
|
||||
version = "0.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4436190e87b4e539807bcdcf5b817e79d2e29e16bc5ddb6445413fe3d1f5716"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.4.2",
|
||||
"indexmap 2.0.0",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"wasm-encoder",
|
||||
"wasm-metadata",
|
||||
"wasmparser",
|
||||
"wit-parser 0.13.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-component"
|
||||
version = "0.21.0"
|
||||
@@ -12451,7 +12406,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"wasm-encoder 0.41.2",
|
||||
"wasm-encoder",
|
||||
"wasm-metadata",
|
||||
"wasmparser",
|
||||
"wit-parser 0.14.0",
|
||||
@@ -12501,7 +12456,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"log",
|
||||
"thiserror",
|
||||
"wast 35.0.2",
|
||||
"wast",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -12819,7 +12774,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.126.0"
|
||||
version = "0.127.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
@@ -12849,13 +12804,11 @@ dependencies = [
|
||||
"feedback",
|
||||
"file_finder",
|
||||
"fs",
|
||||
"fsevent",
|
||||
"futures 0.3.28",
|
||||
"go_to_line",
|
||||
"gpui",
|
||||
"install_cli",
|
||||
"isahc",
|
||||
"itertools 0.11.0",
|
||||
"journal",
|
||||
"language",
|
||||
"language_selector",
|
||||
@@ -12921,6 +12874,13 @@ dependencies = [
|
||||
"zed_extension_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_uiua"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"zed_extension_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeno"
|
||||
version = "0.2.3"
|
||||
|
||||
87
Cargo.toml
87
Cargo.toml
@@ -92,7 +92,10 @@ members = [
|
||||
"crates/workspace",
|
||||
"crates/zed",
|
||||
"crates/zed_actions",
|
||||
|
||||
"extensions/gleam",
|
||||
"extensions/uiua",
|
||||
|
||||
"tooling/xtask",
|
||||
]
|
||||
default-members = ["crates/zed"]
|
||||
@@ -105,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" }
|
||||
@@ -197,9 +201,10 @@ async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
|
||||
async-tar = "0.4.2"
|
||||
async-trait = "0.1"
|
||||
bitflags = "2.4.2"
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e9d93a4d41f3946a03ffb76136290d6ccf7f2b80" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "e9d93a4d41f3946a03ffb76136290d6ccf7f2b80" }
|
||||
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" }
|
||||
@@ -207,6 +212,7 @@ 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 }
|
||||
@@ -215,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"
|
||||
@@ -238,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"] }
|
||||
@@ -266,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" }
|
||||
@@ -295,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" }
|
||||
@@ -304,22 +319,37 @@ unicase = "2.6"
|
||||
url = "2.2"
|
||||
uuid = { version = "1.1.2", features = ["v4"] }
|
||||
wasmparser = "0.121"
|
||||
wasmtime = "18.0"
|
||||
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.
|
||||
@@ -337,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]
|
||||
@@ -344,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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -153,7 +153,8 @@
|
||||
}
|
||||
],
|
||||
"ctrl-cmd-space": "editor::ShowCharacterPalette",
|
||||
"cmd-;": "editor::ToggleLineNumbers"
|
||||
"cmd-;": "editor::ToggleLineNumbers",
|
||||
"cmd-alt-z": "editor::RevertSelectedHunks"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -315,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",
|
||||
{
|
||||
@@ -347,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",
|
||||
@@ -454,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",
|
||||
@@ -353,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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
@@ -590,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
|
||||
|
||||
@@ -5,6 +5,9 @@ edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/ai.rs"
|
||||
doctest = false
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}),
|
||||
@@ -2413,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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5,6 +5,9 @@ edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/channel.rs"
|
||||
doctest = false
|
||||
|
||||
@@ -3,9 +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, ClientSettings, HostedProjectId, 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::{
|
||||
@@ -29,7 +27,7 @@ pub fn init(client: &Arc<Client>, user_store: Model<UserStore>, cx: &mut AppCont
|
||||
cx.set_global(GlobalChannelStore(channel_store));
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
struct NotesVersion {
|
||||
epoch: u64,
|
||||
version: clock::Global,
|
||||
@@ -37,7 +35,7 @@ struct NotesVersion {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HostedProject {
|
||||
id: HostedProjectId,
|
||||
project_id: ProjectId,
|
||||
channel_id: ChannelId,
|
||||
name: SharedString,
|
||||
_visibility: proto::ChannelVisibility,
|
||||
@@ -46,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(),
|
||||
@@ -59,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>,
|
||||
@@ -81,14 +79,14 @@ 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 {
|
||||
@@ -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())
|
||||
@@ -1159,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1236,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 {
|
||||
@@ -1275,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/cli.rs"
|
||||
doctest = false
|
||||
|
||||
@@ -5,6 +5,9 @@ edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/client.rs"
|
||||
doctest = false
|
||||
|
||||
@@ -25,7 +25,7 @@ impl std::fmt::Display for ChannelId {
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
pub struct HostedProjectId(pub u64);
|
||||
pub struct ProjectId(pub u64);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ParticipantIndex(pub u32);
|
||||
|
||||
@@ -5,6 +5,9 @@ edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/clock.rs"
|
||||
doctest = false
|
||||
|
||||
@@ -7,6 +7,9 @@ version = "0.44.0"
|
||||
publish = false
|
||||
license = "AGPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "collab"
|
||||
|
||||
@@ -18,8 +21,9 @@ 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
|
||||
@@ -28,7 +32,6 @@ dashmap = "5.4"
|
||||
envy = "0.4.2"
|
||||
futures.workspace = true
|
||||
hex.workspace = true
|
||||
hyper = "0.14"
|
||||
live_kit_server.workspace = true
|
||||
log.workspace = true
|
||||
nanoid = "0.4"
|
||||
@@ -46,6 +49,7 @@ 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
|
||||
@@ -55,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"
|
||||
@@ -176,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"
|
||||
|
||||
@@ -248,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,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;
|
||||
@@ -11,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,
|
||||
@@ -26,7 +26,7 @@ use tower::ServiceBuilder;
|
||||
|
||||
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))
|
||||
@@ -176,8 +176,8 @@ 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<()> {
|
||||
app.db
|
||||
.add_contributor(
|
||||
|
||||
@@ -3,9 +3,12 @@ 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::{HeaderMap, StatusCode};
|
||||
use serde::{Serialize, Serializer};
|
||||
use sha2::{Digest, Sha256};
|
||||
use telemetry_events::{
|
||||
@@ -81,8 +84,8 @@ impl Header for CloudflareIpCountryHeader {
|
||||
|
||||
pub async fn post_crash(
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
body: Bytes,
|
||||
headers: HeaderMap,
|
||||
body: Bytes,
|
||||
) -> Result<()> {
|
||||
static CRASH_REPORTS_BUCKET: &str = "zed-crash-reports";
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -9,14 +9,15 @@ use axum::{
|
||||
response::IntoResponse,
|
||||
};
|
||||
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};
|
||||
use subtle::ConstantTimeEq;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct Impersonator(pub Option<db::User>);
|
||||
@@ -115,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,
|
||||
@@ -132,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
|
||||
@@ -190,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);
|
||||
|
||||
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() {
|
||||
@@ -208,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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,7 +308,7 @@ impl Database {
|
||||
connection_lost: ActiveValue::set(true),
|
||||
..Default::default()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.exec(tx)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -363,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"))?;
|
||||
@@ -375,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?;
|
||||
@@ -429,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)
|
||||
@@ -558,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),
|
||||
@@ -602,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"))?)
|
||||
}
|
||||
@@ -617,7 +628,7 @@ impl Database {
|
||||
..Default::default()
|
||||
}
|
||||
.find_related(buffer::Entity)
|
||||
.one(&*tx)
|
||||
.one(tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("no such buffer"))?)
|
||||
}
|
||||
@@ -639,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"))?;
|
||||
|
||||
@@ -657,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();
|
||||
@@ -711,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?;
|
||||
@@ -745,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>,
|
||||
@@ -781,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
|
||||
@@ -798,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 {
|
||||
@@ -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)
|
||||
@@ -564,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 {
|
||||
@@ -614,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?;
|
||||
@@ -625,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 {
|
||||
@@ -778,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()
|
||||
@@ -855,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)
|
||||
@@ -875,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 {
|
||||
@@ -930,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())?)
|
||||
}
|
||||
@@ -943,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 {
|
||||
@@ -954,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
|
||||
|
||||
@@ -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,13 +32,15 @@ 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(
|
||||
@@ -47,11 +50,11 @@ impl Database {
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<(hosted_project::Model, ChannelRole)> {
|
||||
let project = hosted_project::Entity::find_by_id(hosted_project_id)
|
||||
.one(&*tx)
|
||||
.one(tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!(ErrorCode::NoSuchProject))?;
|
||||
let channel = channel::Entity::find_by_id(project.channel_id)
|
||||
.one(&*tx)
|
||||
.one(tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!(ErrorCode::NoSuchChannel))?;
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -384,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(())
|
||||
}
|
||||
@@ -401,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
|
||||
@@ -452,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();
|
||||
|
||||
@@ -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?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ impl Database {
|
||||
.update_column(worktree::Column::RootName)
|
||||
.to_owned(),
|
||||
)
|
||||
.exec(&*tx)
|
||||
.exec(tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -194,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(())
|
||||
@@ -512,18 +512,30 @@ impl Database {
|
||||
/// Adds the given connection to the specified hosted project
|
||||
pub async fn join_hosted_project(
|
||||
&self,
|
||||
id: HostedProjectId,
|
||||
id: ProjectId,
|
||||
user_id: UserId,
|
||||
connection: ConnectionId,
|
||||
) -> Result<(Project, ReplicaId)> {
|
||||
self.transaction(|tx| async move {
|
||||
let (hosted_project, role) = self.get_hosted_project(id, user_id, &tx).await?;
|
||||
let project = project::Entity::find()
|
||||
.filter(project::Column::HostedProjectId.eq(hosted_project.id))
|
||||
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
|
||||
})
|
||||
@@ -584,7 +596,7 @@ impl Database {
|
||||
) -> Result<(Project, ReplicaId)> {
|
||||
let mut collaborators = project
|
||||
.find_related(project_collaborator::Entity)
|
||||
.all(&*tx)
|
||||
.all(tx)
|
||||
.await?;
|
||||
let replica_ids = collaborators
|
||||
.iter()
|
||||
@@ -603,11 +615,11 @@ impl Database {
|
||||
is_host: ActiveValue::set(false),
|
||||
..Default::default()
|
||||
}
|
||||
.insert(&*tx)
|
||||
.insert(tx)
|
||||
.await?;
|
||||
collaborators.push(new_collaborator);
|
||||
|
||||
let db_worktrees = project.find_related(worktree::Entity).all(&*tx).await?;
|
||||
let db_worktrees = project.find_related(worktree::Entity).all(tx).await?;
|
||||
let mut worktrees = db_worktrees
|
||||
.into_iter()
|
||||
.map(|db_worktree| {
|
||||
@@ -637,7 +649,7 @@ impl Database {
|
||||
.add(worktree_entry::Column::ProjectId.eq(project.id))
|
||||
.add(worktree_entry::Column::IsDeleted.eq(false)),
|
||||
)
|
||||
.stream(&*tx)
|
||||
.stream(tx)
|
||||
.await?;
|
||||
while let Some(db_entry) = db_entries.next().await {
|
||||
let db_entry = db_entry?;
|
||||
@@ -668,7 +680,7 @@ impl Database {
|
||||
.add(worktree_repository::Column::ProjectId.eq(project.id))
|
||||
.add(worktree_repository::Column::IsDeleted.eq(false)),
|
||||
)
|
||||
.stream(&*tx)
|
||||
.stream(tx)
|
||||
.await?;
|
||||
while let Some(db_repository_entry) = db_repository_entries.next().await {
|
||||
let db_repository_entry = db_repository_entry?;
|
||||
@@ -689,7 +701,7 @@ impl Database {
|
||||
{
|
||||
let mut db_summaries = worktree_diagnostic_summary::Entity::find()
|
||||
.filter(worktree_diagnostic_summary::Column::ProjectId.eq(project.id))
|
||||
.stream(&*tx)
|
||||
.stream(tx)
|
||||
.await?;
|
||||
while let Some(db_summary) = db_summaries.next().await {
|
||||
let db_summary = db_summary?;
|
||||
@@ -710,7 +722,7 @@ impl Database {
|
||||
{
|
||||
let mut db_settings_files = worktree_settings_file::Entity::find()
|
||||
.filter(worktree_settings_file::Column::ProjectId.eq(project.id))
|
||||
.stream(&*tx)
|
||||
.stream(tx)
|
||||
.await?;
|
||||
while let Some(db_settings_file) = db_settings_files.next().await {
|
||||
let db_settings_file = db_settings_file?;
|
||||
@@ -726,7 +738,7 @@ impl Database {
|
||||
// Populate language servers.
|
||||
let language_servers = project
|
||||
.find_related(language_server::Entity)
|
||||
.all(&*tx)
|
||||
.all(tx)
|
||||
.await?;
|
||||
|
||||
let project = Project {
|
||||
|
||||
@@ -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),
|
||||
@@ -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()
|
||||
{
|
||||
@@ -1098,7 +1098,7 @@ impl Database {
|
||||
.eq(connection.owner_id as i32),
|
||||
),
|
||||
)
|
||||
.one(&*tx)
|
||||
.one(tx)
|
||||
.await?;
|
||||
|
||||
if let Some(participant) = participant {
|
||||
@@ -1106,7 +1106,7 @@ impl Database {
|
||||
answering_connection_lost: ActiveValue::set(true),
|
||||
..participant.into_active_model()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.exec(tx)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -1295,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())
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,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 {
|
||||
@@ -82,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 {}
|
||||
|
||||
@@ -235,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,
|
||||
@@ -299,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(),
|
||||
@@ -361,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 {
|
||||
@@ -388,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")
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,8 +15,9 @@ 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: &str = env!("CARGO_PKG_VERSION");
|
||||
@@ -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,9 +4,9 @@ use crate::{
|
||||
auth::{self, Impersonator},
|
||||
db::{
|
||||
self, BufferId, ChannelId, ChannelRole, ChannelsForUser, CreatedChannelMessage, Database,
|
||||
HostedProjectId, InviteMemberResult, MembershipUpdated, MessageId, NotificationId, Project,
|
||||
ProjectId, RemoveChannelMemberResult, ReplicaId, RespondToChannelInvite, RoomId, ServerId,
|
||||
User, UserId,
|
||||
InviteMemberResult, MembershipUpdated, MessageId, NotificationId, Project, ProjectId,
|
||||
RemoveChannelMemberResult, ReplicaId, RespondToChannelInvite, RoomId, ServerId, User,
|
||||
UserId,
|
||||
},
|
||||
executor::Executor,
|
||||
AppState, Error, Result,
|
||||
@@ -67,7 +67,9 @@ 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;
|
||||
@@ -464,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()
|
||||
@@ -478,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)
|
||||
@@ -844,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(
|
||||
@@ -1765,7 +1770,7 @@ async fn join_hosted_project(
|
||||
.db()
|
||||
.await
|
||||
.join_hosted_project(
|
||||
HostedProjectId(request.id as i32),
|
||||
ProjectId(request.project_id as i32),
|
||||
session.user_id,
|
||||
session.connection_id,
|
||||
)
|
||||
|
||||
@@ -5,7 +5,8 @@ use crate::{
|
||||
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,
|
||||
@@ -17,6 +18,7 @@ use language::{
|
||||
language_settings::{AllLanguageSettings, InlayHintSettings},
|
||||
FakeLspAdapter,
|
||||
};
|
||||
use project::SERVER_PROGRESS_DEBOUNCE_TIMEOUT;
|
||||
use rpc::RECEIVE_TIMEOUT;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
@@ -865,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, _| {
|
||||
@@ -898,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, _| {
|
||||
@@ -1811,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]);
|
||||
@@ -1598,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])
|
||||
@@ -1616,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])
|
||||
@@ -1720,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| {
|
||||
@@ -1741,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5,6 +5,9 @@ edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/collab_ui.rs"
|
||||
doctest = false
|
||||
@@ -34,8 +37,8 @@ clock.workspace = true
|
||||
collections.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
emojis.workspace = true
|
||||
extensions_ui.workspace = true
|
||||
feedback.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
|
||||
@@ -430,7 +430,6 @@ impl ChatPanel {
|
||||
ChannelMessageId::Saved(id) => ("saved-message", id).into(),
|
||||
ChannelMessageId::Pending(id) => ("pending-message", id).into(),
|
||||
};
|
||||
let this = cx.view().clone();
|
||||
|
||||
let mentioning_you = message
|
||||
.mentions
|
||||
@@ -465,15 +464,21 @@ impl ChatPanel {
|
||||
v_flex()
|
||||
.w_full()
|
||||
.relative()
|
||||
.group("")
|
||||
.when(!is_continuation_from_previous, |this| this.pt_2())
|
||||
.child(
|
||||
div()
|
||||
.group("")
|
||||
.bg(background)
|
||||
.rounded_md()
|
||||
.overflow_hidden()
|
||||
.px_1()
|
||||
.px_1p5()
|
||||
.py_0p5()
|
||||
.when(!self.has_open_menu(message_id), |this| {
|
||||
this.hover(|style| style.bg(cx.theme().colors().element_hover))
|
||||
})
|
||||
.when(!is_continuation_from_previous, |this| {
|
||||
this.mt_2().child(
|
||||
this.child(
|
||||
h_flex()
|
||||
.text_ui_sm()
|
||||
.child(div().absolute().child(
|
||||
@@ -545,37 +550,11 @@ impl ChatPanel {
|
||||
.w_full()
|
||||
.text_ui_sm()
|
||||
.id(element_id)
|
||||
.group("")
|
||||
.child(text.element("body".into(), cx))
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.z_index(1)
|
||||
.right_0()
|
||||
.w_6()
|
||||
.bg(background)
|
||||
.when(!self.has_open_menu(message_id), |el| {
|
||||
el.visible_on_hover("")
|
||||
})
|
||||
.when_some(message_id, |el, message_id| {
|
||||
el.child(
|
||||
popover_menu(("menu", message_id))
|
||||
.trigger(IconButton::new(
|
||||
("trigger", message_id),
|
||||
IconName::Ellipsis,
|
||||
))
|
||||
.menu(move |cx| {
|
||||
Some(Self::render_message_menu(
|
||||
&this,
|
||||
message_id,
|
||||
can_delete_message,
|
||||
cx,
|
||||
))
|
||||
}),
|
||||
)
|
||||
}),
|
||||
),
|
||||
.child(text.element("body".into(), cx)),
|
||||
)
|
||||
.when(self.has_open_menu(message_id), |el| {
|
||||
el.bg(cx.theme().colors().element_selected)
|
||||
})
|
||||
}),
|
||||
)
|
||||
.when(
|
||||
@@ -600,6 +579,10 @@ impl ChatPanel {
|
||||
)
|
||||
},
|
||||
)
|
||||
.child(
|
||||
self.render_popover_buttons(&cx, message_id, can_delete_message)
|
||||
.neg_mt_2p5(),
|
||||
)
|
||||
}
|
||||
|
||||
fn has_open_menu(&self, message_id: Option<u64>) -> bool {
|
||||
@@ -609,6 +592,90 @@ impl ChatPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_popover_buttons(
|
||||
&self,
|
||||
cx: &ViewContext<Self>,
|
||||
message_id: Option<u64>,
|
||||
can_delete_message: bool,
|
||||
) -> Div {
|
||||
div()
|
||||
.absolute()
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.right_8()
|
||||
.w_6()
|
||||
.rounded_tl_md()
|
||||
.rounded_bl_md()
|
||||
.border_l_1()
|
||||
.border_t_1()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().element_selected)
|
||||
.bg(cx.theme().colors().element_background)
|
||||
.hover(|style| style.bg(cx.theme().colors().element_hover))
|
||||
.when(!self.has_open_menu(message_id), |el| {
|
||||
el.visible_on_hover("")
|
||||
})
|
||||
.when_some(message_id, |el, message_id| {
|
||||
el.child(
|
||||
div()
|
||||
.id("reply")
|
||||
.child(
|
||||
IconButton::new(("reply", message_id), IconName::ReplyArrow)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.message_editor.update(cx, |editor, cx| {
|
||||
editor.set_reply_to_message_id(message_id);
|
||||
editor.focus_handle(cx).focus(cx);
|
||||
})
|
||||
})),
|
||||
)
|
||||
.tooltip(|cx| Tooltip::text("Reply", cx)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.right_2()
|
||||
.w_6()
|
||||
.rounded_tr_md()
|
||||
.rounded_br_md()
|
||||
.border_r_1()
|
||||
.border_t_1()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().element_selected)
|
||||
.bg(cx.theme().colors().element_background)
|
||||
.hover(|style| style.bg(cx.theme().colors().element_hover))
|
||||
.when(!self.has_open_menu(message_id), |el| {
|
||||
el.visible_on_hover("")
|
||||
})
|
||||
.when_some(message_id, |el, message_id| {
|
||||
let this = cx.view().clone();
|
||||
|
||||
el.child(
|
||||
div()
|
||||
.id("more")
|
||||
.child(
|
||||
popover_menu(("menu", message_id))
|
||||
.trigger(IconButton::new(
|
||||
("trigger", message_id),
|
||||
IconName::Ellipsis,
|
||||
))
|
||||
.menu(move |cx| {
|
||||
Some(Self::render_message_menu(
|
||||
&this,
|
||||
message_id,
|
||||
can_delete_message,
|
||||
cx,
|
||||
))
|
||||
}),
|
||||
)
|
||||
.tooltip(|cx| Tooltip::text("More", cx)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_message_menu(
|
||||
this: &View<Self>,
|
||||
message_id: u64,
|
||||
@@ -785,7 +852,7 @@ impl Render for ChatPanel {
|
||||
.size_full()
|
||||
.on_action(cx.listener(Self::send))
|
||||
.child(
|
||||
h_flex().z_index(1).child(
|
||||
h_flex().child(
|
||||
TabBar::new("chat_header").child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
|
||||
@@ -3,7 +3,7 @@ use channel::{ChannelMembership, ChannelStore, MessageParams};
|
||||
use client::{ChannelId, UserId};
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{AnchorRangeExt, CompletionProvider, Editor, EditorElement, EditorStyle};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
AsyncWindowContext, FocusableView, FontStyle, FontWeight, HighlightStyle, IntoElement, Model,
|
||||
Render, SharedString, Task, TextStyle, View, ViewContext, WeakView, WhiteSpace,
|
||||
@@ -16,10 +16,12 @@ use lazy_static::lazy_static;
|
||||
use parking_lot::RwLock;
|
||||
use project::search::SearchQuery;
|
||||
use settings::Settings;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use std::{ops::Range, sync::Arc, time::Duration};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, UiTextSize};
|
||||
|
||||
use crate::panel_settings::MessageEditorSettings;
|
||||
|
||||
const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
|
||||
|
||||
lazy_static! {
|
||||
@@ -86,6 +88,11 @@ impl MessageEditor {
|
||||
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
|
||||
editor.set_use_autoclose(false);
|
||||
editor.set_completion_provider(Box::new(MessageEditorCompletionProvider(this)));
|
||||
editor.set_auto_replace_emoji_shortcode(
|
||||
MessageEditorSettings::get_global(cx)
|
||||
.auto_replace_emoji_shortcode
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
});
|
||||
|
||||
let buffer = editor
|
||||
@@ -96,6 +103,16 @@ impl MessageEditor {
|
||||
.expect("message editor must be singleton");
|
||||
|
||||
cx.subscribe(&buffer, Self::on_buffer_event).detach();
|
||||
cx.observe_global::<settings::SettingsStore>(|view, cx| {
|
||||
view.editor.update(cx, |editor, cx| {
|
||||
editor.set_auto_replace_emoji_shortcode(
|
||||
MessageEditorSettings::get_global(cx)
|
||||
.auto_replace_emoji_shortcode
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
|
||||
let markdown = language_registry.language_for_name("Markdown");
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
@@ -219,6 +236,101 @@ impl MessageEditor {
|
||||
end_anchor: Anchor,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<Vec<Completion>>> {
|
||||
if let Some((start_anchor, query, candidates)) =
|
||||
self.collect_mention_candidates(buffer, end_anchor, cx)
|
||||
{
|
||||
if !candidates.is_empty() {
|
||||
return cx.spawn(|_, cx| async move {
|
||||
Ok(Self::resolve_completions_for_candidates(
|
||||
&cx,
|
||||
query.as_str(),
|
||||
&candidates,
|
||||
start_anchor..end_anchor,
|
||||
Self::completion_for_mention,
|
||||
)
|
||||
.await)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((start_anchor, query, candidates)) =
|
||||
self.collect_emoji_candidates(buffer, end_anchor, cx)
|
||||
{
|
||||
if !candidates.is_empty() {
|
||||
return cx.spawn(|_, cx| async move {
|
||||
Ok(Self::resolve_completions_for_candidates(
|
||||
&cx,
|
||||
query.as_str(),
|
||||
candidates,
|
||||
start_anchor..end_anchor,
|
||||
Self::completion_for_emoji,
|
||||
)
|
||||
.await)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Task::ready(Ok(vec![]))
|
||||
}
|
||||
|
||||
async fn resolve_completions_for_candidates(
|
||||
cx: &AsyncWindowContext,
|
||||
query: &str,
|
||||
candidates: &[StringMatchCandidate],
|
||||
range: Range<Anchor>,
|
||||
completion_fn: impl Fn(&StringMatch) -> (String, CodeLabel),
|
||||
) -> Vec<Completion> {
|
||||
let matches = fuzzy::match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
true,
|
||||
10,
|
||||
&Default::default(),
|
||||
cx.background_executor().clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
matches
|
||||
.into_iter()
|
||||
.map(|mat| {
|
||||
let (new_text, label) = completion_fn(&mat);
|
||||
Completion {
|
||||
old_range: range.clone(),
|
||||
new_text,
|
||||
label,
|
||||
documentation: None,
|
||||
server_id: LanguageServerId(0), // TODO: Make this optional or something?
|
||||
lsp_completion: Default::default(), // TODO: Make this optional or something?
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn completion_for_mention(mat: &StringMatch) -> (String, CodeLabel) {
|
||||
let label = CodeLabel {
|
||||
filter_range: 1..mat.string.len() + 1,
|
||||
text: format!("@{}", mat.string),
|
||||
runs: Vec::new(),
|
||||
};
|
||||
(mat.string.clone(), label)
|
||||
}
|
||||
|
||||
fn completion_for_emoji(mat: &StringMatch) -> (String, CodeLabel) {
|
||||
let emoji = emojis::get_by_shortcode(&mat.string).unwrap();
|
||||
let label = CodeLabel {
|
||||
filter_range: 1..mat.string.len() + 1,
|
||||
text: format!(":{}: {}", mat.string, emoji),
|
||||
runs: Vec::new(),
|
||||
};
|
||||
(emoji.to_string(), label)
|
||||
}
|
||||
|
||||
fn collect_mention_candidates(
|
||||
&mut self,
|
||||
buffer: &Model<Buffer>,
|
||||
end_anchor: Anchor,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<(Anchor, String, Vec<StringMatchCandidate>)> {
|
||||
let end_offset = end_anchor.to_offset(buffer.read(cx));
|
||||
|
||||
let Some(query) = buffer.update(cx, |buffer, _| {
|
||||
@@ -232,9 +344,9 @@ impl MessageEditor {
|
||||
}
|
||||
query.push(ch);
|
||||
}
|
||||
return None;
|
||||
None
|
||||
}) else {
|
||||
return Task::ready(Ok(vec![]));
|
||||
return None;
|
||||
};
|
||||
|
||||
let start_offset = end_offset - query.len();
|
||||
@@ -258,33 +370,76 @@ impl MessageEditor {
|
||||
char_bag: user.chars().collect(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
cx.spawn(|_, cx| async move {
|
||||
let matches = fuzzy::match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
true,
|
||||
10,
|
||||
&Default::default(),
|
||||
cx.background_executor().clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(matches
|
||||
.into_iter()
|
||||
.map(|mat| Completion {
|
||||
old_range: start_anchor..end_anchor,
|
||||
new_text: mat.string.clone(),
|
||||
label: CodeLabel {
|
||||
filter_range: 1..mat.string.len() + 1,
|
||||
text: format!("@{}", mat.string),
|
||||
runs: Vec::new(),
|
||||
},
|
||||
documentation: None,
|
||||
server_id: LanguageServerId(0), // TODO: Make this optional or something?
|
||||
lsp_completion: Default::default(), // TODO: Make this optional or something?
|
||||
})
|
||||
.collect())
|
||||
})
|
||||
Some((start_anchor, query, candidates))
|
||||
}
|
||||
|
||||
fn collect_emoji_candidates(
|
||||
&mut self,
|
||||
buffer: &Model<Buffer>,
|
||||
end_anchor: Anchor,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<(Anchor, String, &'static [StringMatchCandidate])> {
|
||||
lazy_static! {
|
||||
static ref EMOJI_FUZZY_MATCH_CANDIDATES: Vec<StringMatchCandidate> = {
|
||||
let emojis = emojis::iter()
|
||||
.flat_map(|s| s.shortcodes())
|
||||
.map(|emoji| StringMatchCandidate {
|
||||
id: 0,
|
||||
string: emoji.to_string(),
|
||||
char_bag: emoji.chars().collect(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
emojis
|
||||
};
|
||||
}
|
||||
|
||||
let end_offset = end_anchor.to_offset(buffer.read(cx));
|
||||
|
||||
let Some(query) = buffer.update(cx, |buffer, _| {
|
||||
let mut query = String::new();
|
||||
for ch in buffer.reversed_chars_at(end_offset).take(100) {
|
||||
if ch == ':' {
|
||||
let next_char = buffer
|
||||
.reversed_chars_at(end_offset - query.len() - 1)
|
||||
.next();
|
||||
// Ensure we are at the start of the message or that the previous character is a whitespace
|
||||
if next_char.is_none() || next_char.unwrap().is_whitespace() {
|
||||
return Some(query.chars().rev().collect::<String>());
|
||||
}
|
||||
|
||||
// If the previous character is not a whitespace, we are in the middle of a word
|
||||
// and we only want to complete the shortcode if the word is made up of other emojis
|
||||
let mut containing_word = String::new();
|
||||
for ch in buffer
|
||||
.reversed_chars_at(end_offset - query.len() - 1)
|
||||
.take(100)
|
||||
{
|
||||
if ch.is_whitespace() {
|
||||
break;
|
||||
}
|
||||
containing_word.push(ch);
|
||||
}
|
||||
let containing_word = containing_word.chars().rev().collect::<String>();
|
||||
if util::word_consists_of_emojis(containing_word.as_str()) {
|
||||
return Some(query.chars().rev().collect::<String>());
|
||||
}
|
||||
break;
|
||||
}
|
||||
if ch.is_whitespace() || !ch.is_ascii() {
|
||||
break;
|
||||
}
|
||||
query.push(ch);
|
||||
}
|
||||
None
|
||||
}) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let start_offset = end_offset - query.len() - 1;
|
||||
let start_anchor = buffer.read(cx).anchor_before(start_offset);
|
||||
|
||||
Some((start_anchor, query, &EMOJI_FUZZY_MATCH_CANDIDATES))
|
||||
}
|
||||
|
||||
async fn find_mentions(
|
||||
@@ -465,6 +620,8 @@ mod tests {
|
||||
editor::init(cx);
|
||||
client::init(&client, cx);
|
||||
channel::init(&client, user_store, cx);
|
||||
|
||||
MessageEditorSettings::register(cx);
|
||||
});
|
||||
|
||||
let language_registry = Arc::new(LanguageRegistry::test());
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::{
|
||||
};
|
||||
use call::ActiveCall;
|
||||
use channel::{Channel, ChannelEvent, ChannelStore};
|
||||
use client::{ChannelId, Client, Contact, HostedProjectId, User, UserStore};
|
||||
use client::{ChannelId, Client, Contact, ProjectId, User, UserStore};
|
||||
use contact_finder::ContactFinder;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
@@ -185,7 +185,7 @@ enum ListEntry {
|
||||
depth: usize,
|
||||
},
|
||||
HostedProject {
|
||||
id: HostedProjectId,
|
||||
id: ProjectId,
|
||||
name: SharedString,
|
||||
},
|
||||
Contact {
|
||||
@@ -989,7 +989,6 @@ impl CollabPanel {
|
||||
.children(has_channel_buffer_changed.then(|| {
|
||||
div()
|
||||
.w_1p5()
|
||||
.z_index(1)
|
||||
.absolute()
|
||||
.right(px(2.))
|
||||
.top(px(2.))
|
||||
@@ -1022,7 +1021,6 @@ impl CollabPanel {
|
||||
.children(has_messages_notification.then(|| {
|
||||
div()
|
||||
.w_1p5()
|
||||
.z_index(1)
|
||||
.absolute()
|
||||
.right(px(2.))
|
||||
.top(px(4.))
|
||||
@@ -1035,7 +1033,7 @@ impl CollabPanel {
|
||||
|
||||
fn render_channel_project(
|
||||
&self,
|
||||
id: HostedProjectId,
|
||||
id: ProjectId,
|
||||
name: &SharedString,
|
||||
is_selected: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
@@ -2617,7 +2615,6 @@ impl CollabPanel {
|
||||
.children(has_notes_notification.then(|| {
|
||||
div()
|
||||
.w_1p5()
|
||||
.z_index(1)
|
||||
.absolute()
|
||||
.right(px(-1.))
|
||||
.top(px(-1.))
|
||||
@@ -2632,49 +2629,44 @@ impl CollabPanel {
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.absolute()
|
||||
.right(rems(0.))
|
||||
.z_index(1)
|
||||
.h_full()
|
||||
.child(
|
||||
h_flex()
|
||||
.h_full()
|
||||
.gap_1()
|
||||
.px_1()
|
||||
.child(
|
||||
IconButton::new("channel_chat", IconName::MessageBubbles)
|
||||
.style(ButtonStyle::Filled)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(if has_messages_notification {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.join_channel_chat(channel_id, cx)
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::text("Open channel chat", cx))
|
||||
.visible_on_hover(""),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("channel_notes", IconName::File)
|
||||
.style(ButtonStyle::Filled)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(if has_notes_notification {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.open_channel_notes(channel_id, cx)
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::text("Open channel notes", cx))
|
||||
.visible_on_hover(""),
|
||||
),
|
||||
),
|
||||
h_flex().absolute().right(rems(0.)).h_full().child(
|
||||
h_flex()
|
||||
.h_full()
|
||||
.gap_1()
|
||||
.px_1()
|
||||
.child(
|
||||
IconButton::new("channel_chat", IconName::MessageBubbles)
|
||||
.style(ButtonStyle::Filled)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(if has_messages_notification {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.join_channel_chat(channel_id, cx)
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::text("Open channel chat", cx))
|
||||
.visible_on_hover(""),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("channel_notes", IconName::File)
|
||||
.style(ButtonStyle::Filled)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(if has_notes_notification {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.open_channel_notes(channel_id, cx)
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::text("Open channel notes", cx))
|
||||
.visible_on_hover(""),
|
||||
),
|
||||
),
|
||||
)
|
||||
.tooltip({
|
||||
let channel_store = self.channel_store.clone();
|
||||
@@ -2720,31 +2712,34 @@ fn render_tree_branch(is_last: bool, overdraw: bool, cx: &mut WindowContext) ->
|
||||
let thickness = px(1.);
|
||||
let color = cx.theme().colors().text;
|
||||
|
||||
canvas(move |bounds, cx| {
|
||||
let start_x = (bounds.left() + bounds.right() - thickness) / 2.;
|
||||
let start_y = (bounds.top() + bounds.bottom() - thickness) / 2.;
|
||||
let right = bounds.right();
|
||||
let top = bounds.top();
|
||||
canvas(
|
||||
|_, _| {},
|
||||
move |bounds, _, cx| {
|
||||
let start_x = (bounds.left() + bounds.right() - thickness) / 2.;
|
||||
let start_y = (bounds.top() + bounds.bottom() - thickness) / 2.;
|
||||
let right = bounds.right();
|
||||
let top = bounds.top();
|
||||
|
||||
cx.paint_quad(fill(
|
||||
Bounds::from_corners(
|
||||
point(start_x, top),
|
||||
point(
|
||||
start_x + thickness,
|
||||
if is_last {
|
||||
start_y
|
||||
} else {
|
||||
bounds.bottom() + if overdraw { px(1.) } else { px(0.) }
|
||||
},
|
||||
cx.paint_quad(fill(
|
||||
Bounds::from_corners(
|
||||
point(start_x, top),
|
||||
point(
|
||||
start_x + thickness,
|
||||
if is_last {
|
||||
start_y
|
||||
} else {
|
||||
bounds.bottom() + if overdraw { px(1.) } else { px(0.) }
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
color,
|
||||
));
|
||||
cx.paint_quad(fill(
|
||||
Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)),
|
||||
color,
|
||||
));
|
||||
})
|
||||
color,
|
||||
));
|
||||
cx.paint_quad(fill(
|
||||
Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)),
|
||||
color,
|
||||
));
|
||||
},
|
||||
)
|
||||
.w(width)
|
||||
.h(line_height)
|
||||
}
|
||||
|
||||
@@ -329,24 +329,27 @@ impl Render for CollabTitlebarItem {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_color_ribbon(color: Hsla) -> gpui::Canvas {
|
||||
canvas(move |bounds, cx| {
|
||||
let height = bounds.size.height;
|
||||
let horizontal_offset = height;
|
||||
let vertical_offset = px(height.0 / 2.0);
|
||||
let mut path = Path::new(bounds.lower_left());
|
||||
path.curve_to(
|
||||
bounds.origin + point(horizontal_offset, vertical_offset),
|
||||
bounds.origin + point(px(0.0), vertical_offset),
|
||||
);
|
||||
path.line_to(bounds.upper_right() + point(-horizontal_offset, vertical_offset));
|
||||
path.curve_to(
|
||||
bounds.lower_right(),
|
||||
bounds.upper_right() + point(px(0.0), vertical_offset),
|
||||
);
|
||||
path.line_to(bounds.lower_left());
|
||||
cx.paint_path(path, color);
|
||||
})
|
||||
fn render_color_ribbon(color: Hsla) -> impl Element {
|
||||
canvas(
|
||||
move |_, _| {},
|
||||
move |bounds, _, cx| {
|
||||
let height = bounds.size.height;
|
||||
let horizontal_offset = height;
|
||||
let vertical_offset = px(height.0 / 2.0);
|
||||
let mut path = Path::new(bounds.lower_left());
|
||||
path.curve_to(
|
||||
bounds.origin + point(horizontal_offset, vertical_offset),
|
||||
bounds.origin + point(px(0.0), vertical_offset),
|
||||
);
|
||||
path.line_to(bounds.upper_right() + point(-horizontal_offset, vertical_offset));
|
||||
path.curve_to(
|
||||
bounds.lower_right(),
|
||||
bounds.upper_right() + point(px(0.0), vertical_offset),
|
||||
);
|
||||
path.line_to(bounds.lower_left());
|
||||
cx.paint_path(path, color);
|
||||
},
|
||||
)
|
||||
.h_1()
|
||||
.w_full()
|
||||
}
|
||||
@@ -698,9 +701,8 @@ impl CollabTitlebarItem {
|
||||
ContextMenu::build(cx, |menu, _| {
|
||||
menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
|
||||
.action("Extensions", extensions_ui::Extensions.boxed_clone())
|
||||
.action("Theme", theme_selector::Toggle.boxed_clone())
|
||||
.action("Themes...", theme_selector::Toggle.boxed_clone())
|
||||
.separator()
|
||||
.action("Share Feedback", feedback::GiveFeedback.boxed_clone())
|
||||
.action("Sign Out", client::SignOut.boxed_clone())
|
||||
})
|
||||
.into()
|
||||
@@ -722,10 +724,8 @@ impl CollabTitlebarItem {
|
||||
.menu(|cx| {
|
||||
ContextMenu::build(cx, |menu, _| {
|
||||
menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
|
||||
.action("Theme", theme_selector::Toggle.boxed_clone())
|
||||
.action("Extensions", extensions_ui::Extensions.boxed_clone())
|
||||
.separator()
|
||||
.action("Share Feedback", feedback::GiveFeedback.boxed_clone())
|
||||
.action("Themes...", theme_selector::Toggle.boxed_clone())
|
||||
})
|
||||
.into()
|
||||
})
|
||||
|
||||
@@ -16,6 +16,7 @@ use gpui::{
|
||||
actions, point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, Task, WindowBounds,
|
||||
WindowContext, WindowKind, WindowOptions,
|
||||
};
|
||||
use panel_settings::MessageEditorSettings;
|
||||
pub use panel_settings::{
|
||||
ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
|
||||
};
|
||||
@@ -31,6 +32,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
CollaborationPanelSettings::register(cx);
|
||||
ChatPanelSettings::register(cx);
|
||||
NotificationPanelSettings::register(cx);
|
||||
MessageEditorSettings::register(cx);
|
||||
|
||||
vcs_menu::init(cx);
|
||||
collab_titlebar_item::init(cx);
|
||||
|
||||
@@ -14,25 +14,25 @@ impl FacePile {
|
||||
}
|
||||
|
||||
pub fn new(faces: SmallVec<[AnyElement; 2]>) -> Self {
|
||||
Self {
|
||||
base: h_flex(),
|
||||
faces,
|
||||
}
|
||||
Self { base: div(), faces }
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for FacePile {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
let player_count = self.faces.len();
|
||||
let player_list = self.faces.into_iter().enumerate().map(|(ix, player)| {
|
||||
let isnt_last = ix < player_count - 1;
|
||||
|
||||
div()
|
||||
.z_index((player_count - ix) as u16)
|
||||
.when(isnt_last, |div| div.neg_mr_1())
|
||||
.child(player)
|
||||
});
|
||||
self.base.children(player_list)
|
||||
// Lay the faces out in reverse so they overlap in the desired order (left to right, front to back)
|
||||
self.base
|
||||
.flex()
|
||||
.flex_row_reverse()
|
||||
.items_center()
|
||||
.justify_start()
|
||||
.children(
|
||||
self.faces
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.rev()
|
||||
.map(|(ix, player)| div().when(ix > 0, |div| div.neg_ml_1()).child(player)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,15 @@ pub struct PanelSettingsContent {
|
||||
pub default_width: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct MessageEditorSettings {
|
||||
/// Whether to automatically replace emoji shortcodes with emoji characters.
|
||||
/// For example: typing `:wave:` gets replaced with `👋`.
|
||||
///
|
||||
/// Default: false
|
||||
pub auto_replace_emoji_shortcode: Option<bool>,
|
||||
}
|
||||
|
||||
impl Settings for CollaborationPanelSettings {
|
||||
const KEY: Option<&'static str> = Some("collaboration_panel");
|
||||
type FileContent = PanelSettingsContent;
|
||||
@@ -77,3 +86,15 @@ impl Settings for NotificationPanelSettings {
|
||||
Self::load_via_json_merge(default_value, user_values)
|
||||
}
|
||||
}
|
||||
|
||||
impl Settings for MessageEditorSettings {
|
||||
const KEY: Option<&'static str> = Some("message_editor");
|
||||
type FileContent = MessageEditorSettings;
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
_: &mut gpui::AppContext,
|
||||
) -> anyhow::Result<Self> {
|
||||
Self::load_via_json_merge(default_value, user_values)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ edition = "2021"
|
||||
publish = false
|
||||
license = "Apache-2.0"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/collections.rs"
|
||||
doctest = false
|
||||
|
||||
@@ -5,6 +5,9 @@ edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@ edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/command_palette.rs"
|
||||
doctest = false
|
||||
|
||||
@@ -396,6 +396,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.py_px()
|
||||
.justify_between()
|
||||
.child(HighlightedLabel::new(
|
||||
command.name.clone(),
|
||||
|
||||
@@ -5,6 +5,9 @@ edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/command_palette_hooks.rs"
|
||||
doctest = false
|
||||
|
||||
@@ -5,6 +5,9 @@ edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/copilot.rs"
|
||||
doctest = false
|
||||
|
||||
@@ -5,6 +5,9 @@ edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/copilot_ui.rs"
|
||||
doctest = false
|
||||
|
||||
@@ -149,7 +149,7 @@ impl CopilotButton {
|
||||
pub fn build_copilot_menu(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
|
||||
let fs = self.fs.clone();
|
||||
|
||||
return ContextMenu::build(cx, move |mut menu, cx| {
|
||||
ContextMenu::build(cx, move |mut menu, cx| {
|
||||
if let Some(language) = self.language.clone() {
|
||||
let fs = fs.clone();
|
||||
let language_enabled =
|
||||
@@ -216,7 +216,7 @@ impl CopilotButton {
|
||||
.boxed_clone(),
|
||||
)
|
||||
.action("Sign Out", SignOut.boxed_clone())
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_enabled(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
|
||||
|
||||
@@ -5,6 +5,9 @@ edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/db.rs"
|
||||
doctest = false
|
||||
|
||||
@@ -5,6 +5,9 @@ edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/diagnostics.rs"
|
||||
doctest = false
|
||||
|
||||
@@ -517,15 +517,15 @@ impl ProjectDiagnosticsEditor {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.remove_blocks(blocks_to_remove, None, cx);
|
||||
let block_ids = editor.insert_blocks(
|
||||
blocks_to_add.into_iter().map(|block| {
|
||||
blocks_to_add.into_iter().flat_map(|block| {
|
||||
let (excerpt_id, text_anchor) = block.position;
|
||||
BlockProperties {
|
||||
position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
|
||||
Some(BlockProperties {
|
||||
position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor)?,
|
||||
height: block.height,
|
||||
style: block.style,
|
||||
render: block.render,
|
||||
disposition: block.disposition,
|
||||
}
|
||||
})
|
||||
}),
|
||||
Some(Autoscroll::fit()),
|
||||
cx,
|
||||
@@ -589,14 +589,16 @@ impl ProjectDiagnosticsEditor {
|
||||
Ok(ix) | Err(ix) => ix,
|
||||
};
|
||||
if let Some(group) = groups.get(group_ix) {
|
||||
let offset = excerpts_snapshot
|
||||
if let Some(offset) = excerpts_snapshot
|
||||
.anchor_in_excerpt(
|
||||
group.excerpts[group.primary_excerpt_ix],
|
||||
group.primary_diagnostic.range.start,
|
||||
)
|
||||
.to_offset(&excerpts_snapshot);
|
||||
selection.start = offset;
|
||||
selection.end = offset;
|
||||
.map(|anchor| anchor.to_offset(&excerpts_snapshot))
|
||||
{
|
||||
selection.start = offset;
|
||||
selection.end = offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -892,7 +894,7 @@ mod tests {
|
||||
display_map::{BlockContext, TransformBlock},
|
||||
DisplayPoint, GutterDimensions,
|
||||
};
|
||||
use gpui::{px, TestAppContext, VisualTestContext, WindowContext};
|
||||
use gpui::{px, Stateful, TestAppContext, VisualTestContext, WindowContext};
|
||||
use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped};
|
||||
use project::FakeFs;
|
||||
use serde_json::json;
|
||||
@@ -1598,20 +1600,18 @@ mod tests {
|
||||
let name: SharedString = match block {
|
||||
TransformBlock::Custom(block) => cx.with_element_context({
|
||||
|cx| -> Option<SharedString> {
|
||||
block
|
||||
.render(&mut BlockContext {
|
||||
context: cx,
|
||||
anchor_x: px(0.),
|
||||
gutter_dimensions: &GutterDimensions::default(),
|
||||
line_height: px(0.),
|
||||
em_width: px(0.),
|
||||
max_width: px(0.),
|
||||
block_id: ix,
|
||||
editor_style: &editor::EditorStyle::default(),
|
||||
})
|
||||
.inner_id()?
|
||||
.try_into()
|
||||
.ok()
|
||||
let mut element = block.render(&mut BlockContext {
|
||||
context: cx,
|
||||
anchor_x: px(0.),
|
||||
gutter_dimensions: &GutterDimensions::default(),
|
||||
line_height: px(0.),
|
||||
em_width: px(0.),
|
||||
max_width: px(0.),
|
||||
block_id: ix,
|
||||
editor_style: &editor::EditorStyle::default(),
|
||||
});
|
||||
let element = element.downcast_mut::<Stateful<Div>>().unwrap();
|
||||
element.interactivity().element_id.clone()?.try_into().ok()
|
||||
}
|
||||
})?,
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@ edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/editor.rs"
|
||||
doctest = false
|
||||
@@ -33,11 +36,12 @@ collections.workspace = true
|
||||
convert_case = "0.6.0"
|
||||
copilot.workspace = true
|
||||
db.workspace = true
|
||||
emojis.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
git.workspace = true
|
||||
gpui.workspace = true
|
||||
indoc = "1.0.4"
|
||||
indoc.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
lazy_static.workspace = true
|
||||
|
||||
@@ -94,6 +94,12 @@ pub struct SelectDownByLines {
|
||||
pub(super) lines: u32,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||
pub struct DuplicateLine {
|
||||
#[serde(default)]
|
||||
pub move_upwards: bool,
|
||||
}
|
||||
|
||||
impl_actions!(
|
||||
editor,
|
||||
[
|
||||
@@ -112,7 +118,8 @@ impl_actions!(
|
||||
MoveUpByLines,
|
||||
MoveDownByLines,
|
||||
SelectUpByLines,
|
||||
SelectDownByLines
|
||||
SelectDownByLines,
|
||||
DuplicateLine
|
||||
]
|
||||
);
|
||||
|
||||
@@ -152,7 +159,6 @@ gpui::actions!(
|
||||
DeleteToPreviousSubwordStart,
|
||||
DeleteToPreviousWordStart,
|
||||
DisplayCursorNames,
|
||||
DuplicateLine,
|
||||
ExpandMacroRecursively,
|
||||
FindAllReferences,
|
||||
Fold,
|
||||
@@ -204,6 +210,7 @@ gpui::actions!(
|
||||
PageDown,
|
||||
PageUp,
|
||||
Paste,
|
||||
RevertSelectedHunks,
|
||||
Redo,
|
||||
RedoSelection,
|
||||
Rename,
|
||||
|
||||
@@ -46,7 +46,7 @@ pub use block_map::{
|
||||
BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock,
|
||||
};
|
||||
|
||||
pub use self::fold_map::{Fold, FoldPoint};
|
||||
pub use self::fold_map::{Fold, FoldId, FoldPoint};
|
||||
pub use self::inlay_map::{InlayOffset, InlayPoint};
|
||||
pub(crate) use inlay_map::Inlay;
|
||||
|
||||
@@ -339,8 +339,13 @@ impl DisplayMap {
|
||||
pub(crate) struct Highlights<'a> {
|
||||
pub text_highlights: Option<&'a TextHighlights>,
|
||||
pub inlay_highlights: Option<&'a InlayHighlights>,
|
||||
pub inlay_highlight_style: Option<HighlightStyle>,
|
||||
pub suggestion_highlight_style: Option<HighlightStyle>,
|
||||
pub styles: HighlightStyles,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy)]
|
||||
pub struct HighlightStyles {
|
||||
pub inlay_hint: Option<HighlightStyle>,
|
||||
pub suggestion: Option<HighlightStyle>,
|
||||
}
|
||||
|
||||
pub struct HighlightedChunk<'a> {
|
||||
@@ -516,8 +521,7 @@ impl DisplaySnapshot {
|
||||
&self,
|
||||
display_rows: Range<u32>,
|
||||
language_aware: bool,
|
||||
inlay_highlight_style: Option<HighlightStyle>,
|
||||
suggestion_highlight_style: Option<HighlightStyle>,
|
||||
highlight_styles: HighlightStyles,
|
||||
) -> DisplayChunks<'_> {
|
||||
self.block_snapshot.chunks(
|
||||
display_rows,
|
||||
@@ -525,8 +529,7 @@ impl DisplaySnapshot {
|
||||
Highlights {
|
||||
text_highlights: Some(&self.text_highlights),
|
||||
inlay_highlights: Some(&self.inlay_highlights),
|
||||
inlay_highlight_style,
|
||||
suggestion_highlight_style,
|
||||
styles: highlight_styles,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -540,8 +543,10 @@ impl DisplaySnapshot {
|
||||
self.chunks(
|
||||
display_rows,
|
||||
language_aware,
|
||||
Some(editor_style.inlays_style),
|
||||
Some(editor_style.suggestions_style),
|
||||
HighlightStyles {
|
||||
inlay_hint: Some(editor_style.inlay_hints_style),
|
||||
suggestion: Some(editor_style.suggestions_style),
|
||||
},
|
||||
)
|
||||
.map(|chunk| {
|
||||
let mut highlight_style = chunk
|
||||
@@ -1846,7 +1851,7 @@ pub mod tests {
|
||||
) -> Vec<(String, Option<Hsla>, Option<Hsla>)> {
|
||||
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let mut chunks: Vec<(String, Option<Hsla>, Option<Hsla>)> = Vec::new();
|
||||
for chunk in snapshot.chunks(rows, true, None, None) {
|
||||
for chunk in snapshot.chunks(rows, true, HighlightStyles::default()) {
|
||||
let syntax_color = chunk
|
||||
.syntax_highlight_id
|
||||
.and_then(|id| id.style(theme)?.color);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::InlayId;
|
||||
use crate::{HighlightStyles, InlayId};
|
||||
use collections::{BTreeMap, BTreeSet};
|
||||
use gpui::HighlightStyle;
|
||||
use language::{Chunk, Edit, Point, TextSummary};
|
||||
@@ -215,8 +215,7 @@ pub struct InlayChunks<'a> {
|
||||
inlay_chunk: Option<&'a str>,
|
||||
output_offset: InlayOffset,
|
||||
max_output_offset: InlayOffset,
|
||||
inlay_highlight_style: Option<HighlightStyle>,
|
||||
suggestion_highlight_style: Option<HighlightStyle>,
|
||||
highlight_styles: HighlightStyles,
|
||||
highlight_endpoints: Peekable<vec::IntoIter<HighlightEndpoint>>,
|
||||
active_highlights: BTreeMap<Option<TypeId>, HighlightStyle>,
|
||||
highlights: Highlights<'a>,
|
||||
@@ -307,8 +306,8 @@ impl<'a> Iterator for InlayChunks<'a> {
|
||||
}
|
||||
|
||||
let mut highlight_style = match inlay.id {
|
||||
InlayId::Suggestion(_) => self.suggestion_highlight_style,
|
||||
InlayId::Hint(_) => self.inlay_highlight_style,
|
||||
InlayId::Suggestion(_) => self.highlight_styles.suggestion,
|
||||
InlayId::Hint(_) => self.highlight_styles.inlay_hint,
|
||||
};
|
||||
let next_inlay_highlight_endpoint;
|
||||
let offset_in_inlay = self.output_offset - self.transforms.start().0;
|
||||
@@ -1052,8 +1051,7 @@ impl InlaySnapshot {
|
||||
buffer_chunk: None,
|
||||
output_offset: range.start,
|
||||
max_output_offset: range.end,
|
||||
inlay_highlight_style: highlights.inlay_highlight_style,
|
||||
suggestion_highlight_style: highlights.suggestion_highlight_style,
|
||||
highlight_styles: highlights.styles,
|
||||
highlight_endpoints: highlight_endpoints.into_iter().peekable(),
|
||||
active_highlights: Default::default(),
|
||||
highlights,
|
||||
|
||||
@@ -36,14 +36,14 @@ mod selections_collection;
|
||||
mod editor_tests;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod test;
|
||||
use ::git::diff::DiffHunk;
|
||||
use ::git::diff::{DiffHunk, DiffHunkStatus};
|
||||
pub(crate) use actions::*;
|
||||
use aho_corasick::AhoCorasick;
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use blink_manager::BlinkManager;
|
||||
use client::{Collaborator, ParticipantIndex};
|
||||
use clock::ReplicaId;
|
||||
use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
|
||||
use collections::{hash_map, BTreeMap, Bound, HashMap, HashSet, VecDeque};
|
||||
use convert_case::{Case, Casing};
|
||||
use copilot::Copilot;
|
||||
use debounced_delay::DebouncedDelay;
|
||||
@@ -51,7 +51,9 @@ pub use display_map::DisplayPoint;
|
||||
use display_map::*;
|
||||
pub use editor_settings::EditorSettings;
|
||||
use element::LineWithInvisibles;
|
||||
pub use element::{Cursor, EditorElement, HighlightedRange, HighlightedRangeLine};
|
||||
pub use element::{
|
||||
CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition,
|
||||
};
|
||||
use futures::FutureExt;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use git::diff_hunk_to_display;
|
||||
@@ -60,9 +62,9 @@ use gpui::{
|
||||
AnyElement, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Context,
|
||||
DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusableView, FontId, FontStyle,
|
||||
FontWeight, HighlightStyle, Hsla, InteractiveText, KeyContext, Model, MouseButton,
|
||||
ParentElement, Pixels, Render, SharedString, Styled, StyledText, Subscription, Task, TextStyle,
|
||||
UnderlineStyle, UniformListScrollHandle, View, ViewContext, ViewInputHandler, VisualContext,
|
||||
WeakView, WhiteSpace, WindowContext,
|
||||
ParentElement, Pixels, Render, SharedString, StrikethroughStyle, Styled, StyledText,
|
||||
Subscription, Task, TextStyle, UnderlineStyle, UniformListScrollHandle, View, ViewContext,
|
||||
ViewInputHandler, VisualContext, WeakView, WhiteSpace, WindowContext,
|
||||
};
|
||||
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
||||
use hover_popover::{hide_hover, HoverState};
|
||||
@@ -89,6 +91,7 @@ pub use multi_buffer::{
|
||||
use ordered_float::OrderedFloat;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use project::project_settings::{GitGutterSetting, ProjectSettings};
|
||||
use project::Item;
|
||||
use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction};
|
||||
use rand::prelude::*;
|
||||
use rpc::proto::*;
|
||||
@@ -110,7 +113,6 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
pub use sum_tree::Bias;
|
||||
use sum_tree::TreeMap;
|
||||
use text::{BufferId, OffsetUtf16, Rope};
|
||||
use theme::{
|
||||
observe_buffer_font_size_adjustment, ActiveTheme, PlayerColor, StatusColors, SyntaxTheme,
|
||||
@@ -248,7 +250,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx.on_action(move |_: &workspace::NewFile, cx| {
|
||||
let app_state = workspace::AppState::global(cx);
|
||||
if let Some(app_state) = app_state.upgrade() {
|
||||
workspace::open_new(&app_state, cx, |workspace, cx| {
|
||||
workspace::open_new(app_state, cx, |workspace, cx| {
|
||||
Editor::new_file(workspace, &Default::default(), cx)
|
||||
})
|
||||
.detach();
|
||||
@@ -257,7 +259,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx.on_action(move |_: &workspace::NewWindow, cx| {
|
||||
let app_state = workspace::AppState::global(cx);
|
||||
if let Some(app_state) = app_state.upgrade() {
|
||||
workspace::open_new(&app_state, cx, |workspace, cx| {
|
||||
workspace::open_new(app_state, cx, |workspace, cx| {
|
||||
Editor::new_file(workspace, &Default::default(), cx)
|
||||
})
|
||||
.detach();
|
||||
@@ -322,7 +324,7 @@ pub struct EditorStyle {
|
||||
pub scrollbar_width: Pixels,
|
||||
pub syntax: Arc<SyntaxTheme>,
|
||||
pub status: StatusColors,
|
||||
pub inlays_style: HighlightStyle,
|
||||
pub inlay_hints_style: HighlightStyle,
|
||||
pub suggestions_style: HighlightStyle,
|
||||
}
|
||||
|
||||
@@ -338,7 +340,7 @@ impl Default for EditorStyle {
|
||||
// We should look into removing the status colors from the editor
|
||||
// style and retrieve them directly from the theme.
|
||||
status: StatusColors::dark(),
|
||||
inlays_style: HighlightStyle::default(),
|
||||
inlay_hints_style: HighlightStyle::default(),
|
||||
suggestions_style: HighlightStyle::default(),
|
||||
}
|
||||
}
|
||||
@@ -350,7 +352,6 @@ type CompletionId = usize;
|
||||
// type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
|
||||
|
||||
type BackgroundHighlight = (fn(&ThemeColors) -> Hsla, Vec<Range<Anchor>>);
|
||||
type InlayBackgroundHighlight = (fn(&ThemeColors) -> Hsla, Vec<InlayHighlight>);
|
||||
|
||||
/// Zed's primary text input `View`, allowing users to edit a [`MultiBuffer`]
|
||||
///
|
||||
@@ -387,9 +388,9 @@ pub struct Editor {
|
||||
show_gutter: bool,
|
||||
show_wrap_guides: Option<bool>,
|
||||
placeholder_text: Option<Arc<str>>,
|
||||
highlighted_rows: Option<Range<u32>>,
|
||||
highlight_order: usize,
|
||||
highlighted_rows: HashMap<TypeId, Vec<(usize, Range<Anchor>, Hsla)>>,
|
||||
background_highlights: BTreeMap<TypeId, BackgroundHighlight>,
|
||||
inlay_background_highlights: TreeMap<Option<TypeId>, InlayBackgroundHighlight>,
|
||||
nav_history: Option<ItemNavHistory>,
|
||||
context_menu: RwLock<Option<ContextMenu>>,
|
||||
mouse_context_menu: Option<MouseContextMenu>,
|
||||
@@ -424,6 +425,7 @@ pub struct Editor {
|
||||
editor_actions: Vec<Box<dyn Fn(&mut ViewContext<Self>)>>,
|
||||
show_copilot_suggestions: bool,
|
||||
use_autoclose: bool,
|
||||
auto_replace_emoji_shortcode: bool,
|
||||
custom_context_menu: Option<
|
||||
Box<
|
||||
dyn 'static
|
||||
@@ -928,6 +930,15 @@ impl CompletionsMenu {
|
||||
// Ignore font weight for syntax highlighting, as we'll use it
|
||||
// for fuzzy matches.
|
||||
highlight.font_weight = None;
|
||||
|
||||
if completion.lsp_completion.deprecated.unwrap_or(false) {
|
||||
highlight.strikethrough = Some(StrikethroughStyle {
|
||||
thickness: 1.0.into(),
|
||||
..Default::default()
|
||||
});
|
||||
highlight.color = Some(cx.theme().colors().text_muted);
|
||||
}
|
||||
|
||||
(range, highlight)
|
||||
},
|
||||
),
|
||||
@@ -1185,6 +1196,7 @@ impl CodeActionsMenu {
|
||||
}
|
||||
}),
|
||||
)
|
||||
.whitespace_nowrap()
|
||||
// TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
|
||||
.child(SharedString::from(action.lsp_action.title.clone()))
|
||||
})
|
||||
@@ -1213,6 +1225,7 @@ impl CodeActionsMenu {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct CopilotState {
|
||||
excerpt_id: Option<ExcerptId>,
|
||||
pending_refresh: Task<Option<()>>,
|
||||
@@ -1514,9 +1527,9 @@ impl Editor {
|
||||
show_gutter: mode == EditorMode::Full,
|
||||
show_wrap_guides: None,
|
||||
placeholder_text: None,
|
||||
highlighted_rows: None,
|
||||
highlight_order: 0,
|
||||
highlighted_rows: HashMap::default(),
|
||||
background_highlights: Default::default(),
|
||||
inlay_background_highlights: Default::default(),
|
||||
nav_history: None,
|
||||
context_menu: RwLock::new(None),
|
||||
mouse_context_menu: None,
|
||||
@@ -1538,6 +1551,7 @@ impl Editor {
|
||||
use_modal_editing: mode == EditorMode::Full,
|
||||
read_only: false,
|
||||
use_autoclose: true,
|
||||
auto_replace_emoji_shortcode: false,
|
||||
leader_peer_id: None,
|
||||
remote_id: None,
|
||||
hover_state: Default::default(),
|
||||
@@ -1828,6 +1842,10 @@ impl Editor {
|
||||
self.use_autoclose = autoclose;
|
||||
}
|
||||
|
||||
pub fn set_auto_replace_emoji_shortcode(&mut self, auto_replace: bool) {
|
||||
self.auto_replace_emoji_shortcode = auto_replace;
|
||||
}
|
||||
|
||||
pub fn set_show_copilot_suggestions(&mut self, show_copilot_suggestions: bool) {
|
||||
self.show_copilot_suggestions = show_copilot_suggestions;
|
||||
}
|
||||
@@ -2504,6 +2522,47 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
if self.auto_replace_emoji_shortcode
|
||||
&& selection.is_empty()
|
||||
&& text.as_ref().ends_with(':')
|
||||
{
|
||||
if let Some(possible_emoji_short_code) =
|
||||
Self::find_possible_emoji_shortcode_at_position(&snapshot, selection.start)
|
||||
{
|
||||
if !possible_emoji_short_code.is_empty() {
|
||||
if let Some(emoji) = emojis::get_by_shortcode(&possible_emoji_short_code) {
|
||||
let emoji_shortcode_start = Point::new(
|
||||
selection.start.row,
|
||||
selection.start.column - possible_emoji_short_code.len() as u32 - 1,
|
||||
);
|
||||
|
||||
// Remove shortcode from buffer
|
||||
edits.push((
|
||||
emoji_shortcode_start..selection.start,
|
||||
"".to_string().into(),
|
||||
));
|
||||
new_selections.push((
|
||||
Selection {
|
||||
id: selection.id,
|
||||
start: snapshot.anchor_after(emoji_shortcode_start),
|
||||
end: snapshot.anchor_before(selection.start),
|
||||
reversed: selection.reversed,
|
||||
goal: selection.goal,
|
||||
},
|
||||
0,
|
||||
));
|
||||
|
||||
// Insert emoji
|
||||
let selection_start_anchor = snapshot.anchor_after(selection.start);
|
||||
new_selections.push((selection.map(|_| selection_start_anchor), 0));
|
||||
edits.push((selection.start..selection.end, emoji.to_string().into()));
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If not handling any auto-close operation, then just replace the selected
|
||||
// text with the given input and move the selection to the end of the
|
||||
// newly inserted text.
|
||||
@@ -2587,6 +2646,53 @@ impl Editor {
|
||||
});
|
||||
}
|
||||
|
||||
fn find_possible_emoji_shortcode_at_position(
|
||||
snapshot: &MultiBufferSnapshot,
|
||||
position: Point,
|
||||
) -> Option<String> {
|
||||
let mut chars = Vec::new();
|
||||
let mut found_colon = false;
|
||||
for char in snapshot.reversed_chars_at(position).take(100) {
|
||||
// Found a possible emoji shortcode in the middle of the buffer
|
||||
if found_colon {
|
||||
if char.is_whitespace() {
|
||||
chars.reverse();
|
||||
return Some(chars.iter().collect());
|
||||
}
|
||||
// If the previous character is not a whitespace, we are in the middle of a word
|
||||
// and we only want to complete the shortcode if the word is made up of other emojis
|
||||
let mut containing_word = String::new();
|
||||
for ch in snapshot
|
||||
.reversed_chars_at(position)
|
||||
.skip(chars.len() + 1)
|
||||
.take(100)
|
||||
{
|
||||
if ch.is_whitespace() {
|
||||
break;
|
||||
}
|
||||
containing_word.push(ch);
|
||||
}
|
||||
let containing_word = containing_word.chars().rev().collect::<String>();
|
||||
if util::word_consists_of_emojis(containing_word.as_str()) {
|
||||
chars.reverse();
|
||||
return Some(chars.iter().collect());
|
||||
}
|
||||
}
|
||||
|
||||
if char.is_whitespace() || !char.is_ascii() {
|
||||
return None;
|
||||
}
|
||||
if char == ':' {
|
||||
found_colon = true;
|
||||
} else {
|
||||
chars.push(char);
|
||||
}
|
||||
}
|
||||
// Found a possible emoji shortcode at the beginning of the buffer
|
||||
chars.reverse();
|
||||
Some(chars.iter().collect())
|
||||
}
|
||||
|
||||
pub fn newline(&mut self, _: &Newline, cx: &mut ViewContext<Self>) {
|
||||
self.transact(cx, |this, cx| {
|
||||
let (edits, selection_fixup_info): (Vec<_>, Vec<_>) = {
|
||||
@@ -3040,7 +3146,7 @@ impl Editor {
|
||||
(InvalidationStrategy::RefreshRequested, None)
|
||||
} else {
|
||||
self.inlay_hint_cache.clear();
|
||||
self.splice_inlay_hints(
|
||||
self.splice_inlays(
|
||||
self.visible_inlay_hints(cx)
|
||||
.iter()
|
||||
.map(|inlay| inlay.id)
|
||||
@@ -3062,7 +3168,7 @@ impl Editor {
|
||||
to_remove,
|
||||
to_insert,
|
||||
})) => {
|
||||
self.splice_inlay_hints(to_remove, to_insert, cx);
|
||||
self.splice_inlays(to_remove, to_insert, cx);
|
||||
return;
|
||||
}
|
||||
ControlFlow::Break(None) => return,
|
||||
@@ -3075,7 +3181,7 @@ impl Editor {
|
||||
to_insert,
|
||||
}) = self.inlay_hint_cache.remove_excerpts(excerpts_removed)
|
||||
{
|
||||
self.splice_inlay_hints(to_remove, to_insert, cx);
|
||||
self.splice_inlays(to_remove, to_insert, cx);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -3098,7 +3204,7 @@ impl Editor {
|
||||
ignore_debounce,
|
||||
cx,
|
||||
) {
|
||||
self.splice_inlay_hints(to_remove, to_insert, cx);
|
||||
self.splice_inlays(to_remove, to_insert, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3106,9 +3212,7 @@ impl Editor {
|
||||
self.display_map
|
||||
.read(cx)
|
||||
.current_inlays()
|
||||
.filter(move |inlay| {
|
||||
Some(inlay.id) != self.copilot_state.suggestion.as_ref().map(|h| h.id)
|
||||
})
|
||||
.filter(move |inlay| matches!(inlay.id, InlayId::Hint(_)))
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
@@ -3179,7 +3283,7 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn splice_inlay_hints(
|
||||
fn splice_inlays(
|
||||
&self,
|
||||
to_remove: Vec<InlayId>,
|
||||
to_insert: Vec<Inlay>,
|
||||
@@ -4087,7 +4191,10 @@ impl Editor {
|
||||
}
|
||||
|
||||
fn clear_copilot_suggestions(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.copilot_state = Default::default();
|
||||
if let Some(old_suggestion) = self.copilot_state.suggestion.take() {
|
||||
self.splice_inlays(vec![old_suggestion.id], Vec::new(), cx);
|
||||
}
|
||||
self.copilot_state = CopilotState::default();
|
||||
self.discard_copilot_suggestion(cx);
|
||||
}
|
||||
|
||||
@@ -4119,14 +4226,14 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn render_fold_indicators(
|
||||
&self,
|
||||
&mut self,
|
||||
fold_data: Vec<Option<(FoldStatus, u32, bool)>>,
|
||||
_style: &EditorStyle,
|
||||
gutter_hovered: bool,
|
||||
_line_height: Pixels,
|
||||
_gutter_margin: Pixels,
|
||||
editor_view: View<Editor>,
|
||||
) -> Vec<Option<IconButton>> {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Vec<Option<AnyElement>> {
|
||||
fold_data
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -4135,24 +4242,20 @@ impl Editor {
|
||||
.map(|(fold_status, buffer_row, active)| {
|
||||
(active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| {
|
||||
IconButton::new(ix, ui::IconName::ChevronDown)
|
||||
.on_click({
|
||||
let view = editor_view.clone();
|
||||
move |_e, cx| {
|
||||
view.update(cx, |editor, cx| match fold_status {
|
||||
FoldStatus::Folded => {
|
||||
editor.unfold_at(&UnfoldAt { buffer_row }, cx);
|
||||
}
|
||||
FoldStatus::Foldable => {
|
||||
editor.fold_at(&FoldAt { buffer_row }, cx);
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(move |this, _e, cx| match fold_status {
|
||||
FoldStatus::Folded => {
|
||||
this.unfold_at(&UnfoldAt { buffer_row }, cx);
|
||||
}
|
||||
})
|
||||
FoldStatus::Foldable => {
|
||||
this.fold_at(&FoldAt { buffer_row }, cx);
|
||||
}
|
||||
}))
|
||||
.icon_color(ui::Color::Muted)
|
||||
.icon_size(ui::IconSize::Small)
|
||||
.selected(fold_status == FoldStatus::Folded)
|
||||
.selected_icon(ui::IconName::ChevronRight)
|
||||
.size(ui::ButtonSize::None)
|
||||
.into_any_element()
|
||||
})
|
||||
})
|
||||
.flatten()
|
||||
@@ -4834,6 +4937,105 @@ impl Editor {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn revert_selected_hunks(&mut self, _: &RevertSelectedHunks, cx: &mut ViewContext<Self>) {
|
||||
let revert_changes = self.gather_revert_changes(&self.selections.disjoint_anchors(), cx);
|
||||
if !revert_changes.is_empty() {
|
||||
self.transact(cx, |editor, cx| {
|
||||
editor.buffer().update(cx, |multi_buffer, cx| {
|
||||
for (buffer_id, buffer_revert_ranges) in revert_changes {
|
||||
if let Some(buffer) = multi_buffer.buffer(buffer_id) {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(buffer_revert_ranges, None, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
editor.change_selections(None, cx, |selections| selections.refresh());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn gather_revert_changes(
|
||||
&mut self,
|
||||
selections: &[Selection<Anchor>],
|
||||
cx: &mut ViewContext<'_, Editor>,
|
||||
) -> HashMap<BufferId, Vec<(Range<text::Anchor>, Arc<str>)>> {
|
||||
let mut revert_changes = HashMap::default();
|
||||
self.buffer.update(cx, |multi_buffer, cx| {
|
||||
let multi_buffer_snapshot = multi_buffer.snapshot(cx);
|
||||
let selected_multi_buffer_rows = selections.iter().map(|selection| {
|
||||
let head = selection.head();
|
||||
let tail = selection.tail();
|
||||
let start = tail.to_point(&multi_buffer_snapshot).row;
|
||||
let end = head.to_point(&multi_buffer_snapshot).row;
|
||||
if start > end {
|
||||
end..start
|
||||
} else {
|
||||
start..end
|
||||
}
|
||||
});
|
||||
|
||||
let mut processed_buffer_rows =
|
||||
HashMap::<BufferId, HashSet<Range<text::Anchor>>>::default();
|
||||
for selected_multi_buffer_rows in selected_multi_buffer_rows {
|
||||
let query_rows =
|
||||
selected_multi_buffer_rows.start..selected_multi_buffer_rows.end + 1;
|
||||
for hunk in multi_buffer_snapshot.git_diff_hunks_in_range(query_rows.clone()) {
|
||||
// Deleted hunk is an empty row range, no caret can be placed there and Zed allows to revert it
|
||||
// when the caret is just above or just below the deleted hunk.
|
||||
let allow_adjacent = hunk.status() == DiffHunkStatus::Removed;
|
||||
let related_to_selection = if allow_adjacent {
|
||||
hunk.associated_range.overlaps(&query_rows)
|
||||
|| hunk.associated_range.start == query_rows.end
|
||||
|| hunk.associated_range.end == query_rows.start
|
||||
} else {
|
||||
// `selected_multi_buffer_rows` are inclusive (e.g. [2..2] means 2nd row is selected)
|
||||
// `hunk.associated_range` is exclusive (e.g. [2..3] means 2nd row is selected)
|
||||
hunk.associated_range.overlaps(&selected_multi_buffer_rows)
|
||||
|| selected_multi_buffer_rows.end == hunk.associated_range.start
|
||||
};
|
||||
if related_to_selection {
|
||||
if !processed_buffer_rows
|
||||
.entry(hunk.buffer_id)
|
||||
.or_default()
|
||||
.insert(hunk.buffer_range.start..hunk.buffer_range.end)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Self::prepare_revert_change(&mut revert_changes, &multi_buffer, &hunk, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
revert_changes
|
||||
}
|
||||
|
||||
fn prepare_revert_change(
|
||||
revert_changes: &mut HashMap<BufferId, Vec<(Range<text::Anchor>, Arc<str>)>>,
|
||||
multi_buffer: &MultiBuffer,
|
||||
hunk: &DiffHunk<u32>,
|
||||
cx: &mut AppContext,
|
||||
) -> Option<()> {
|
||||
let buffer = multi_buffer.buffer(hunk.buffer_id)?;
|
||||
let buffer = buffer.read(cx);
|
||||
let original_text = buffer.diff_base()?.get(hunk.diff_base_byte_range.clone())?;
|
||||
let buffer_snapshot = buffer.snapshot();
|
||||
let buffer_revert_changes = revert_changes.entry(buffer.remote_id()).or_default();
|
||||
if let Err(i) = buffer_revert_changes.binary_search_by(|probe| {
|
||||
probe
|
||||
.0
|
||||
.start
|
||||
.cmp(&hunk.buffer_range.start, &buffer_snapshot)
|
||||
.then(probe.0.end.cmp(&hunk.buffer_range.end, &buffer_snapshot))
|
||||
.then(probe.1.as_ref().cmp(original_text))
|
||||
}) {
|
||||
buffer_revert_changes.insert(i, (hunk.buffer_range.clone(), Arc::from(original_text)));
|
||||
Some(())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reverse_lines(&mut self, _: &ReverseLines, cx: &mut ViewContext<Self>) {
|
||||
self.manipulate_lines(cx, |lines| lines.reverse())
|
||||
}
|
||||
@@ -5031,7 +5233,7 @@ impl Editor {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn duplicate_line(&mut self, _: &DuplicateLine, cx: &mut ViewContext<Self>) {
|
||||
pub fn duplicate_line(&mut self, action: &DuplicateLine, cx: &mut ViewContext<Self>) {
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let buffer = &display_map.buffer_snapshot;
|
||||
let selections = self.selections.all::<Point>(cx);
|
||||
@@ -5052,14 +5254,20 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
// Copy the text from the selected row region and splice it at the start of the region.
|
||||
// Copy the text from the selected row region and splice it either at the start
|
||||
// or end of the region.
|
||||
let start = Point::new(rows.start, 0);
|
||||
let end = Point::new(rows.end - 1, buffer.line_len(rows.end - 1));
|
||||
let text = buffer
|
||||
.text_for_range(start..end)
|
||||
.chain(Some("\n"))
|
||||
.collect::<String>();
|
||||
edits.push((start..start, text));
|
||||
let insert_location = if action.move_upwards {
|
||||
Point::new(rows.end, 0)
|
||||
} else {
|
||||
start
|
||||
};
|
||||
edits.push((insert_location..insert_location, text));
|
||||
}
|
||||
|
||||
self.transact(cx, |this, cx| {
|
||||
@@ -7958,7 +8166,7 @@ impl Editor {
|
||||
scrollbar_width: cx.editor_style.scrollbar_width,
|
||||
syntax: cx.editor_style.syntax.clone(),
|
||||
status: cx.editor_style.status.clone(),
|
||||
inlays_style: HighlightStyle {
|
||||
inlay_hints_style: HighlightStyle {
|
||||
color: Some(cx.theme().status().hint),
|
||||
font_weight: Some(FontWeight::BOLD),
|
||||
..HighlightStyle::default()
|
||||
@@ -8657,22 +8865,23 @@ impl Editor {
|
||||
fn get_permalink_to_line(&mut self, cx: &mut ViewContext<Self>) -> Result<url::Url> {
|
||||
use git::permalink::{build_permalink, BuildPermalinkParams};
|
||||
|
||||
let project = self.project.clone().ok_or_else(|| anyhow!("no project"))?;
|
||||
let project = project.read(cx);
|
||||
|
||||
let worktree = project
|
||||
.visible_worktrees(cx)
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("no worktree"))?;
|
||||
|
||||
let mut cwd = worktree.read(cx).abs_path().to_path_buf();
|
||||
cwd.push(".git");
|
||||
let (path, repo) = maybe!({
|
||||
let project_handle = self.project.as_ref()?.clone();
|
||||
let project = project_handle.read(cx);
|
||||
let buffer = self.buffer().read(cx).as_singleton()?;
|
||||
let path = buffer
|
||||
.read(cx)
|
||||
.file()?
|
||||
.as_local()?
|
||||
.path()
|
||||
.to_str()?
|
||||
.to_string();
|
||||
let repo = project.get_repo(&buffer.read(cx).project_path(cx)?, cx)?;
|
||||
Some((path, repo))
|
||||
})
|
||||
.ok_or_else(|| anyhow!("unable to open git repository"))?;
|
||||
|
||||
const REMOTE_NAME: &str = "origin";
|
||||
let repo = project
|
||||
.fs()
|
||||
.open_repo(&cwd)
|
||||
.ok_or_else(|| anyhow!("no Git repo"))?;
|
||||
let origin_url = repo
|
||||
.lock()
|
||||
.remote_url(REMOTE_NAME)
|
||||
@@ -8681,14 +8890,6 @@ impl Editor {
|
||||
.lock()
|
||||
.head_sha()
|
||||
.ok_or_else(|| anyhow!("failed to read HEAD SHA"))?;
|
||||
|
||||
let path = maybe!({
|
||||
let buffer = self.buffer().read(cx).as_singleton()?;
|
||||
let file = buffer.read(cx).file().and_then(|f| f.as_local())?;
|
||||
file.path().to_str().map(|path| path.to_string())
|
||||
})
|
||||
.ok_or_else(|| anyhow!("failed to determine file path"))?;
|
||||
|
||||
let selections = self.selections.all::<Point>(cx);
|
||||
let selection = selections.iter().peekable().next();
|
||||
|
||||
@@ -8726,7 +8927,7 @@ impl Editor {
|
||||
|
||||
match permalink {
|
||||
Ok(permalink) => {
|
||||
cx.open_url(&permalink.to_string());
|
||||
cx.open_url(permalink.as_ref());
|
||||
}
|
||||
Err(err) => {
|
||||
let message = format!("Failed to open permalink: {err}");
|
||||
@@ -8742,12 +8943,93 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn highlight_rows(&mut self, rows: Option<Range<u32>>) {
|
||||
self.highlighted_rows = rows;
|
||||
/// Adds or removes (on `None` color) a highlight for the rows corresponding to the anchor range given.
|
||||
/// On matching anchor range, replaces the old highlight; does not clear the other existing highlights.
|
||||
/// If multiple anchor ranges will produce highlights for the same row, the last range added will be used.
|
||||
pub fn highlight_rows<T: 'static>(
|
||||
&mut self,
|
||||
rows: Range<Anchor>,
|
||||
color: Option<Hsla>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
|
||||
match self.highlighted_rows.entry(TypeId::of::<T>()) {
|
||||
hash_map::Entry::Occupied(o) => {
|
||||
let row_highlights = o.into_mut();
|
||||
let existing_highlight_index =
|
||||
row_highlights.binary_search_by(|(_, highlight_range, _)| {
|
||||
highlight_range
|
||||
.start
|
||||
.cmp(&rows.start, &multi_buffer_snapshot)
|
||||
.then(highlight_range.end.cmp(&rows.end, &multi_buffer_snapshot))
|
||||
});
|
||||
match color {
|
||||
Some(color) => {
|
||||
let insert_index = match existing_highlight_index {
|
||||
Ok(i) => i,
|
||||
Err(i) => i,
|
||||
};
|
||||
row_highlights.insert(
|
||||
insert_index,
|
||||
(post_inc(&mut self.highlight_order), rows, color),
|
||||
);
|
||||
}
|
||||
None => {
|
||||
if let Ok(i) = existing_highlight_index {
|
||||
row_highlights.remove(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
hash_map::Entry::Vacant(v) => {
|
||||
if let Some(color) = color {
|
||||
v.insert(vec![(post_inc(&mut self.highlight_order), rows, color)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn highlighted_rows(&self) -> Option<Range<u32>> {
|
||||
self.highlighted_rows.clone()
|
||||
/// Clear all anchor ranges for a certain highlight context type, so no corresponding rows will be highlighted.
|
||||
pub fn clear_row_highlights<T: 'static>(&mut self) {
|
||||
self.highlighted_rows.remove(&TypeId::of::<T>());
|
||||
}
|
||||
|
||||
/// For a highlight given context type, gets all anchor ranges that will be used for row highlighting.
|
||||
pub fn highlighted_rows<T: 'static>(
|
||||
&self,
|
||||
) -> Option<impl Iterator<Item = (&Range<Anchor>, &Hsla)>> {
|
||||
Some(
|
||||
self.highlighted_rows
|
||||
.get(&TypeId::of::<T>())?
|
||||
.iter()
|
||||
.map(|(_, range, color)| (range, color)),
|
||||
)
|
||||
}
|
||||
|
||||
// Merges all anchor ranges for all context types ever set, picking the last highlight added in case of a row conflict.
|
||||
// Rerturns a map of display rows that are highlighted and their corresponding highlight color.
|
||||
pub fn highlighted_display_rows(&mut self, cx: &mut WindowContext) -> BTreeMap<u32, Hsla> {
|
||||
let snapshot = self.snapshot(cx);
|
||||
let mut used_highlight_orders = HashMap::default();
|
||||
self.highlighted_rows
|
||||
.iter()
|
||||
.flat_map(|(_, highlighted_rows)| highlighted_rows.iter())
|
||||
.fold(
|
||||
BTreeMap::<u32, Hsla>::new(),
|
||||
|mut unique_rows, (highlight_order, anchor_range, hsla)| {
|
||||
let start_row = anchor_range.start.to_display_point(&snapshot).row();
|
||||
let end_row = anchor_range.end.to_display_point(&snapshot).row();
|
||||
for row in start_row..=end_row {
|
||||
let used_index =
|
||||
used_highlight_orders.entry(row).or_insert(*highlight_order);
|
||||
if highlight_order >= used_index {
|
||||
*used_index = *highlight_order;
|
||||
unique_rows.insert(row, *hsla);
|
||||
}
|
||||
}
|
||||
unique_rows
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn highlight_background<T: 'static>(
|
||||
@@ -8772,29 +9054,11 @@ impl Editor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub(crate) fn highlight_inlay_background<T: 'static>(
|
||||
&mut self,
|
||||
ranges: Vec<InlayHighlight>,
|
||||
color_fetcher: fn(&ThemeColors) -> Hsla,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
// TODO: no actual highlights happen for inlays currently, find a way to do that
|
||||
self.inlay_background_highlights
|
||||
.insert(Some(TypeId::of::<T>()), (color_fetcher, ranges));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn clear_background_highlights<T: 'static>(
|
||||
&mut self,
|
||||
cx: &mut ViewContext<Self>,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> Option<BackgroundHighlight> {
|
||||
let text_highlights = self.background_highlights.remove(&TypeId::of::<T>());
|
||||
let inlay_highlights = self
|
||||
.inlay_background_highlights
|
||||
.remove(&Some(TypeId::of::<T>()));
|
||||
if text_highlights.is_some() || inlay_highlights.is_some() {
|
||||
cx.notify();
|
||||
}
|
||||
text_highlights
|
||||
}
|
||||
|
||||
@@ -8971,7 +9235,7 @@ impl Editor {
|
||||
&self,
|
||||
search_range: Range<Anchor>,
|
||||
display_snapshot: &DisplaySnapshot,
|
||||
cx: &mut ViewContext<Self>,
|
||||
cx: &WindowContext,
|
||||
) -> Vec<Range<DisplayPoint>> {
|
||||
display_snapshot
|
||||
.buffer_snapshot
|
||||
@@ -9742,7 +10006,7 @@ impl EditorSnapshot {
|
||||
self.is_focused
|
||||
}
|
||||
|
||||
pub fn placeholder_text(&self, _cx: &mut WindowContext) -> Option<&Arc<str>> {
|
||||
pub fn placeholder_text(&self) -> Option<&Arc<str>> {
|
||||
self.placeholder_text.as_ref()
|
||||
}
|
||||
|
||||
@@ -9906,7 +10170,7 @@ impl Render for Editor {
|
||||
scrollbar_width: px(12.),
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
status: cx.theme().status().clone(),
|
||||
inlays_style: HighlightStyle {
|
||||
inlay_hints_style: HighlightStyle {
|
||||
color: Some(cx.theme().status().hint),
|
||||
..HighlightStyle::default()
|
||||
},
|
||||
|
||||
@@ -3118,7 +3118,7 @@ fn test_duplicate_line(cx: &mut TestAppContext) {
|
||||
DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
|
||||
])
|
||||
});
|
||||
view.duplicate_line(&DuplicateLine, cx);
|
||||
view.duplicate_line(&DuplicateLine::default(), cx);
|
||||
assert_eq!(view.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
@@ -3142,7 +3142,7 @@ fn test_duplicate_line(cx: &mut TestAppContext) {
|
||||
DisplayPoint::new(1, 2)..DisplayPoint::new(2, 1),
|
||||
])
|
||||
});
|
||||
view.duplicate_line(&DuplicateLine, cx);
|
||||
view.duplicate_line(&DuplicateLine::default(), cx);
|
||||
assert_eq!(view.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
@@ -3152,6 +3152,56 @@ fn test_duplicate_line(cx: &mut TestAppContext) {
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
// With `move_upwards` the selections stay in place, except for
|
||||
// the lines inserted above them
|
||||
let view = cx.add_window(|cx| {
|
||||
let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
|
||||
build_editor(buffer, cx)
|
||||
});
|
||||
_ = view.update(cx, |view, cx| {
|
||||
view.change_selections(None, cx, |s| {
|
||||
s.select_display_ranges([
|
||||
DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
|
||||
DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
|
||||
DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
|
||||
DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
|
||||
])
|
||||
});
|
||||
view.duplicate_line(&DuplicateLine { move_upwards: true }, cx);
|
||||
assert_eq!(view.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
vec![
|
||||
DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
|
||||
DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
|
||||
DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0),
|
||||
DisplayPoint::new(6, 0)..DisplayPoint::new(6, 0),
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
let view = cx.add_window(|cx| {
|
||||
let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
|
||||
build_editor(buffer, cx)
|
||||
});
|
||||
_ = view.update(cx, |view, cx| {
|
||||
view.change_selections(None, cx, |s| {
|
||||
s.select_display_ranges([
|
||||
DisplayPoint::new(0, 1)..DisplayPoint::new(1, 1),
|
||||
DisplayPoint::new(1, 2)..DisplayPoint::new(2, 1),
|
||||
])
|
||||
});
|
||||
view.duplicate_line(&DuplicateLine { move_upwards: true }, cx);
|
||||
assert_eq!(view.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
|
||||
assert_eq!(
|
||||
view.selections.display_ranges(cx),
|
||||
vec![
|
||||
DisplayPoint::new(0, 1)..DisplayPoint::new(1, 1),
|
||||
DisplayPoint::new(1, 2)..DisplayPoint::new(2, 1),
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -5121,6 +5171,78 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_auto_replace_emoji_shortcode(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let language = Arc::new(Language::new(
|
||||
LanguageConfig::default(),
|
||||
Some(tree_sitter_rust::language()),
|
||||
));
|
||||
|
||||
let buffer = cx.new_model(|cx| {
|
||||
Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), "")
|
||||
.with_language(language, cx)
|
||||
});
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||
editor
|
||||
.condition::<crate::EditorEvent>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
|
||||
.await;
|
||||
|
||||
_ = editor.update(cx, |editor, cx| {
|
||||
editor.set_auto_replace_emoji_shortcode(true);
|
||||
|
||||
editor.handle_input("Hello ", cx);
|
||||
editor.handle_input(":wave", cx);
|
||||
assert_eq!(editor.text(cx), "Hello :wave".unindent());
|
||||
|
||||
editor.handle_input(":", cx);
|
||||
assert_eq!(editor.text(cx), "Hello 👋".unindent());
|
||||
|
||||
editor.handle_input(" :smile", cx);
|
||||
assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
|
||||
|
||||
editor.handle_input(":", cx);
|
||||
assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
|
||||
|
||||
// Ensure shortcode gets replaced when it is part of a word that only consists of emojis
|
||||
editor.handle_input(":wave", cx);
|
||||
assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
|
||||
|
||||
editor.handle_input(":", cx);
|
||||
assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
|
||||
|
||||
editor.handle_input(":1", cx);
|
||||
assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
|
||||
|
||||
editor.handle_input(":", cx);
|
||||
assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
|
||||
|
||||
// Ensure shortcode does not get replaced when it is part of a word
|
||||
editor.handle_input(" Test:wave", cx);
|
||||
assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
|
||||
|
||||
editor.handle_input(":", cx);
|
||||
assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
|
||||
|
||||
editor.set_auto_replace_emoji_shortcode(false);
|
||||
|
||||
// Ensure shortcode does not get replaced when auto replace is off
|
||||
editor.handle_input(" :wave", cx);
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"Hello 👋 😄👋:1: Test:wave: :wave".unindent()
|
||||
);
|
||||
|
||||
editor.handle_input(":", cx);
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_snippets(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
@@ -8640,6 +8762,560 @@ async fn test_find_all_references(cx: &mut gpui::TestAppContext) {
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_addition_reverts(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
|
||||
let base_text = indoc! {r#"struct Row;
|
||||
struct Row1;
|
||||
struct Row2;
|
||||
|
||||
struct Row4;
|
||||
struct Row5;
|
||||
struct Row6;
|
||||
|
||||
struct Row8;
|
||||
struct Row9;
|
||||
struct Row10;"#};
|
||||
|
||||
// When addition hunks are not adjacent to carets, no hunk revert is performed
|
||||
assert_hunk_revert(
|
||||
indoc! {r#"struct Row;
|
||||
struct Row1;
|
||||
struct Row1.1;
|
||||
struct Row1.2;
|
||||
struct Row2;ˇ
|
||||
|
||||
struct Row4;
|
||||
struct Row5;
|
||||
struct Row6;
|
||||
|
||||
struct Row8;
|
||||
ˇstruct Row9;
|
||||
struct Row9.1;
|
||||
struct Row9.2;
|
||||
struct Row9.3;
|
||||
struct Row10;"#},
|
||||
vec![DiffHunkStatus::Added, DiffHunkStatus::Added],
|
||||
indoc! {r#"struct Row;
|
||||
struct Row1;
|
||||
struct Row1.1;
|
||||
struct Row1.2;
|
||||
struct Row2;ˇ
|
||||
|
||||
struct Row4;
|
||||
struct Row5;
|
||||
struct Row6;
|
||||
|
||||
struct Row8;
|
||||
ˇstruct Row9;
|
||||
struct Row9.1;
|
||||
struct Row9.2;
|
||||
struct Row9.3;
|
||||
struct Row10;"#},
|
||||
base_text,
|
||||
&mut cx,
|
||||
);
|
||||
// Same for selections
|
||||
assert_hunk_revert(
|
||||
indoc! {r#"struct Row;
|
||||
struct Row1;
|
||||
struct Row2;
|
||||
struct Row2.1;
|
||||
struct Row2.2;
|
||||
«ˇ
|
||||
struct Row4;
|
||||
struct» Row5;
|
||||
«struct Row6;
|
||||
ˇ»
|
||||
struct Row9.1;
|
||||
struct Row9.2;
|
||||
struct Row9.3;
|
||||
struct Row8;
|
||||
struct Row9;
|
||||
struct Row10;"#},
|
||||
vec![DiffHunkStatus::Added, DiffHunkStatus::Added],
|
||||
indoc! {r#"struct Row;
|
||||
struct Row1;
|
||||
struct Row2;
|
||||
struct Row2.1;
|
||||
struct Row2.2;
|
||||
«ˇ
|
||||
struct Row4;
|
||||
struct» Row5;
|
||||
«struct Row6;
|
||||
ˇ»
|
||||
struct Row9.1;
|
||||
struct Row9.2;
|
||||
struct Row9.3;
|
||||
struct Row8;
|
||||
struct Row9;
|
||||
struct Row10;"#},
|
||||
base_text,
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
// When carets and selections intersect the addition hunks, those are reverted.
|
||||
// Adjacent carets got merged.
|
||||
assert_hunk_revert(
|
||||
indoc! {r#"struct Row;
|
||||
ˇ// something on the top
|
||||
struct Row1;
|
||||
struct Row2;
|
||||
struct Roˇw3.1;
|
||||
struct Row2.2;
|
||||
struct Row2.3;ˇ
|
||||
|
||||
struct Row4;
|
||||
struct ˇRow5.1;
|
||||
struct Row5.2;
|
||||
struct «Rowˇ»5.3;
|
||||
struct Row5;
|
||||
struct Row6;
|
||||
ˇ
|
||||
struct Row9.1;
|
||||
struct «Rowˇ»9.2;
|
||||
struct «ˇRow»9.3;
|
||||
struct Row8;
|
||||
struct Row9;
|
||||
«ˇ// something on bottom»
|
||||
struct Row10;"#},
|
||||
vec![
|
||||
DiffHunkStatus::Added,
|
||||
DiffHunkStatus::Added,
|
||||
DiffHunkStatus::Added,
|
||||
DiffHunkStatus::Added,
|
||||
DiffHunkStatus::Added,
|
||||
],
|
||||
indoc! {r#"struct Row;
|
||||
ˇstruct Row1;
|
||||
struct Row2;
|
||||
ˇ
|
||||
struct Row4;
|
||||
ˇstruct Row5;
|
||||
struct Row6;
|
||||
ˇ
|
||||
ˇstruct Row8;
|
||||
struct Row9;
|
||||
ˇstruct Row10;"#},
|
||||
base_text,
|
||||
&mut cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_modification_reverts(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
|
||||
let base_text = indoc! {r#"struct Row;
|
||||
struct Row1;
|
||||
struct Row2;
|
||||
|
||||
struct Row4;
|
||||
struct Row5;
|
||||
struct Row6;
|
||||
|
||||
struct Row8;
|
||||
struct Row9;
|
||||
struct Row10;"#};
|
||||
|
||||
// Modification hunks behave the same as the addition ones.
|
||||
assert_hunk_revert(
|
||||
indoc! {r#"struct Row;
|
||||
struct Row1;
|
||||
struct Row33;
|
||||
ˇ
|
||||
struct Row4;
|
||||
struct Row5;
|
||||
struct Row6;
|
||||
ˇ
|
||||
struct Row99;
|
||||
struct Row9;
|
||||
struct Row10;"#},
|
||||
vec![DiffHunkStatus::Modified, DiffHunkStatus::Modified],
|
||||
indoc! {r#"struct Row;
|
||||
struct Row1;
|
||||
struct Row33;
|
||||
ˇ
|
||||
struct Row4;
|
||||
struct Row5;
|
||||
struct Row6;
|
||||
ˇ
|
||||
struct Row99;
|
||||
struct Row9;
|
||||
struct Row10;"#},
|
||||
base_text,
|
||||
&mut cx,
|
||||
);
|
||||
assert_hunk_revert(
|
||||
indoc! {r#"struct Row;
|
||||
struct Row1;
|
||||
struct Row33;
|
||||
«ˇ
|
||||
struct Row4;
|
||||
struct» Row5;
|
||||
«struct Row6;
|
||||
ˇ»
|
||||
struct Row99;
|
||||
struct Row9;
|
||||
struct Row10;"#},
|
||||
vec![DiffHunkStatus::Modified, DiffHunkStatus::Modified],
|
||||
indoc! {r#"struct Row;
|
||||
struct Row1;
|
||||
struct Row33;
|
||||
«ˇ
|
||||
struct Row4;
|
||||
struct» Row5;
|
||||
«struct Row6;
|
||||
ˇ»
|
||||
struct Row99;
|
||||
struct Row9;
|
||||
struct Row10;"#},
|
||||
base_text,
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
assert_hunk_revert(
|
||||
indoc! {r#"ˇstruct Row1.1;
|
||||
struct Row1;
|
||||
«ˇstr»uct Row22;
|
||||
|
||||
struct ˇRow44;
|
||||
struct Row5;
|
||||
struct «Rˇ»ow66;ˇ
|
||||
|
||||
«struˇ»ct Row88;
|
||||
struct Row9;
|
||||
struct Row1011;ˇ"#},
|
||||
vec![
|
||||
DiffHunkStatus::Modified,
|
||||
DiffHunkStatus::Modified,
|
||||
DiffHunkStatus::Modified,
|
||||
DiffHunkStatus::Modified,
|
||||
DiffHunkStatus::Modified,
|
||||
DiffHunkStatus::Modified,
|
||||
],
|
||||
indoc! {r#"struct Row;
|
||||
ˇstruct Row1;
|
||||
struct Row2;
|
||||
ˇ
|
||||
struct Row4;
|
||||
ˇstruct Row5;
|
||||
struct Row6;
|
||||
ˇ
|
||||
struct Row8;
|
||||
ˇstruct Row9;
|
||||
struct Row10;ˇ"#},
|
||||
base_text,
|
||||
&mut cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_deletion_reverts(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
|
||||
let base_text = indoc! {r#"struct Row;
|
||||
struct Row1;
|
||||
struct Row2;
|
||||
|
||||
struct Row4;
|
||||
struct Row5;
|
||||
struct Row6;
|
||||
|
||||
struct Row8;
|
||||
struct Row9;
|
||||
struct Row10;"#};
|
||||
|
||||
// Deletion hunks trigger with carets on ajacent rows, so carets and selections have to stay farther to avoid the revert
|
||||
assert_hunk_revert(
|
||||
indoc! {r#"struct Row;
|
||||
struct Row2;
|
||||
|
||||
ˇstruct Row4;
|
||||
struct Row5;
|
||||
struct Row6;
|
||||
ˇ
|
||||
struct Row8;
|
||||
struct Row10;"#},
|
||||
vec![DiffHunkStatus::Removed, DiffHunkStatus::Removed],
|
||||
indoc! {r#"struct Row;
|
||||
struct Row2;
|
||||
|
||||
ˇstruct Row4;
|
||||
struct Row5;
|
||||
struct Row6;
|
||||
ˇ
|
||||
struct Row8;
|
||||
struct Row10;"#},
|
||||
base_text,
|
||||
&mut cx,
|
||||
);
|
||||
assert_hunk_revert(
|
||||
indoc! {r#"struct Row;
|
||||
struct Row2;
|
||||
|
||||
«ˇstruct Row4;
|
||||
struct» Row5;
|
||||
«struct Row6;
|
||||
ˇ»
|
||||
struct Row8;
|
||||
struct Row10;"#},
|
||||
vec![DiffHunkStatus::Removed, DiffHunkStatus::Removed],
|
||||
indoc! {r#"struct Row;
|
||||
struct Row2;
|
||||
|
||||
«ˇstruct Row4;
|
||||
struct» Row5;
|
||||
«struct Row6;
|
||||
ˇ»
|
||||
struct Row8;
|
||||
struct Row10;"#},
|
||||
base_text,
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
// Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
|
||||
assert_hunk_revert(
|
||||
indoc! {r#"struct Row;
|
||||
ˇstruct Row2;
|
||||
|
||||
struct Row4;
|
||||
struct Row5;
|
||||
struct Row6;
|
||||
|
||||
struct Row8;ˇ
|
||||
struct Row10;"#},
|
||||
vec![DiffHunkStatus::Removed, DiffHunkStatus::Removed],
|
||||
indoc! {r#"struct Row;
|
||||
struct Row1;
|
||||
ˇstruct Row2;
|
||||
|
||||
struct Row4;
|
||||
struct Row5;
|
||||
struct Row6;
|
||||
|
||||
struct Row8;ˇ
|
||||
struct Row9;
|
||||
struct Row10;"#},
|
||||
base_text,
|
||||
&mut cx,
|
||||
);
|
||||
assert_hunk_revert(
|
||||
indoc! {r#"struct Row;
|
||||
struct Row2«ˇ;
|
||||
struct Row4;
|
||||
struct» Row5;
|
||||
«struct Row6;
|
||||
|
||||
struct Row8;ˇ»
|
||||
struct Row10;"#},
|
||||
vec![
|
||||
DiffHunkStatus::Removed,
|
||||
DiffHunkStatus::Removed,
|
||||
DiffHunkStatus::Removed,
|
||||
],
|
||||
indoc! {r#"struct Row;
|
||||
struct Row1;
|
||||
struct Row2«ˇ;
|
||||
|
||||
struct Row4;
|
||||
struct» Row5;
|
||||
«struct Row6;
|
||||
|
||||
struct Row8;ˇ»
|
||||
struct Row9;
|
||||
struct Row10;"#},
|
||||
base_text,
|
||||
&mut cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_multibuffer_reverts(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let cols = 4;
|
||||
let rows = 10;
|
||||
let sample_text_1 = sample_text(rows, cols, 'a');
|
||||
assert_eq!(
|
||||
sample_text_1,
|
||||
"aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
|
||||
);
|
||||
let sample_text_2 = sample_text(rows, cols, 'l');
|
||||
assert_eq!(
|
||||
sample_text_2,
|
||||
"llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
|
||||
);
|
||||
let sample_text_3 = sample_text(rows, cols, 'v');
|
||||
assert_eq!(
|
||||
sample_text_3,
|
||||
"vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
|
||||
);
|
||||
|
||||
fn diff_every_buffer_row(
|
||||
buffer: &Model<Buffer>,
|
||||
sample_text: String,
|
||||
cols: usize,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
// revert first character in each row, creating one large diff hunk per buffer
|
||||
let is_first_char = |offset: usize| offset % cols == 0;
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.set_text(
|
||||
sample_text
|
||||
.chars()
|
||||
.enumerate()
|
||||
.map(|(offset, c)| if is_first_char(offset) { 'X' } else { c })
|
||||
.collect::<String>(),
|
||||
cx,
|
||||
);
|
||||
buffer.set_diff_base(Some(sample_text), cx);
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
}
|
||||
|
||||
let buffer_1 = cx.new_model(|cx| {
|
||||
Buffer::new(
|
||||
0,
|
||||
BufferId::new(cx.entity_id().as_u64()).unwrap(),
|
||||
sample_text_1.clone(),
|
||||
)
|
||||
});
|
||||
diff_every_buffer_row(&buffer_1, sample_text_1.clone(), cols, cx);
|
||||
|
||||
let buffer_2 = cx.new_model(|cx| {
|
||||
Buffer::new(
|
||||
1,
|
||||
BufferId::new(cx.entity_id().as_u64() + 1).unwrap(),
|
||||
sample_text_2.clone(),
|
||||
)
|
||||
});
|
||||
diff_every_buffer_row(&buffer_2, sample_text_2.clone(), cols, cx);
|
||||
|
||||
let buffer_3 = cx.new_model(|cx| {
|
||||
Buffer::new(
|
||||
2,
|
||||
BufferId::new(cx.entity_id().as_u64() + 2).unwrap(),
|
||||
sample_text_3.clone(),
|
||||
)
|
||||
});
|
||||
diff_every_buffer_row(&buffer_3, sample_text_3.clone(), cols, cx);
|
||||
|
||||
let multibuffer = cx.new_model(|cx| {
|
||||
let mut multibuffer = MultiBuffer::new(0, ReadWrite);
|
||||
multibuffer.push_excerpts(
|
||||
buffer_1.clone(),
|
||||
[
|
||||
ExcerptRange {
|
||||
context: Point::new(0, 0)..Point::new(3, 0),
|
||||
primary: None,
|
||||
},
|
||||
ExcerptRange {
|
||||
context: Point::new(5, 0)..Point::new(7, 0),
|
||||
primary: None,
|
||||
},
|
||||
ExcerptRange {
|
||||
context: Point::new(9, 0)..Point::new(10, 4),
|
||||
primary: None,
|
||||
},
|
||||
],
|
||||
cx,
|
||||
);
|
||||
multibuffer.push_excerpts(
|
||||
buffer_2.clone(),
|
||||
[
|
||||
ExcerptRange {
|
||||
context: Point::new(0, 0)..Point::new(3, 0),
|
||||
primary: None,
|
||||
},
|
||||
ExcerptRange {
|
||||
context: Point::new(5, 0)..Point::new(7, 0),
|
||||
primary: None,
|
||||
},
|
||||
ExcerptRange {
|
||||
context: Point::new(9, 0)..Point::new(10, 4),
|
||||
primary: None,
|
||||
},
|
||||
],
|
||||
cx,
|
||||
);
|
||||
multibuffer.push_excerpts(
|
||||
buffer_3.clone(),
|
||||
[
|
||||
ExcerptRange {
|
||||
context: Point::new(0, 0)..Point::new(3, 0),
|
||||
primary: None,
|
||||
},
|
||||
ExcerptRange {
|
||||
context: Point::new(5, 0)..Point::new(7, 0),
|
||||
primary: None,
|
||||
},
|
||||
ExcerptRange {
|
||||
context: Point::new(9, 0)..Point::new(10, 4),
|
||||
primary: None,
|
||||
},
|
||||
],
|
||||
cx,
|
||||
);
|
||||
multibuffer
|
||||
});
|
||||
|
||||
let (editor, cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_eq!(editor.text(cx), "XaaaXbbbX\nccXc\ndXdd\n\nhXhh\nXiiiXjjjX\n\nXlllXmmmX\nnnXn\noXoo\n\nsXss\nXtttXuuuX\n\nXvvvXwwwX\nxxXx\nyXyy\n\n}X}}\nX~~~X\u{7f}\u{7f}\u{7f}X\n");
|
||||
editor.select_all(&SelectAll, cx);
|
||||
editor.revert_selected_hunks(&RevertSelectedHunks, cx);
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
// When all ranges are selected, all buffer hunks are reverted.
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_eq!(editor.text(cx), "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n\n\nllll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu\n\n\nvvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}\n\n");
|
||||
});
|
||||
buffer_1.update(cx, |buffer, _| {
|
||||
assert_eq!(buffer.text(), sample_text_1);
|
||||
});
|
||||
buffer_2.update(cx, |buffer, _| {
|
||||
assert_eq!(buffer.text(), sample_text_2);
|
||||
});
|
||||
buffer_3.update(cx, |buffer, _| {
|
||||
assert_eq!(buffer.text(), sample_text_3);
|
||||
});
|
||||
|
||||
diff_every_buffer_row(&buffer_1, sample_text_1.clone(), cols, cx);
|
||||
diff_every_buffer_row(&buffer_2, sample_text_2.clone(), cols, cx);
|
||||
diff_every_buffer_row(&buffer_3, sample_text_3.clone(), cols, cx);
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
|
||||
});
|
||||
editor.revert_selected_hunks(&RevertSelectedHunks, cx);
|
||||
});
|
||||
// Now, when all ranges selected belong to buffer_1, the revert should succeed,
|
||||
// but not affect buffer_2 and its related excerpts.
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n\n\nXlllXmmmX\nnnXn\noXoo\nXpppXqqqX\nrrXr\nsXss\nXtttXuuuX\n\n\nXvvvXwwwX\nxxXx\nyXyy\nXzzzX{{{X\n||X|\n}X}}\nX~~~X\u{7f}\u{7f}\u{7f}X\n\n"
|
||||
);
|
||||
});
|
||||
buffer_1.update(cx, |buffer, _| {
|
||||
assert_eq!(buffer.text(), sample_text_1);
|
||||
});
|
||||
buffer_2.update(cx, |buffer, _| {
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
"XlllXmmmX\nnnXn\noXoo\nXpppXqqqX\nrrXr\nsXss\nXtttXuuuX"
|
||||
);
|
||||
});
|
||||
buffer_3.update(cx, |buffer, _| {
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
"XvvvXwwwX\nxxXx\nyXyy\nXzzzX{{{X\n||X|\n}X}}\nX~~~X\u{7f}\u{7f}\u{7f}X"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
|
||||
let point = DisplayPoint::new(row as u32, column as u32);
|
||||
point..point
|
||||
@@ -8810,3 +9486,45 @@ pub(crate) fn rust_lang() -> Arc<Language> {
|
||||
Some(tree_sitter_rust::language()),
|
||||
))
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_hunk_revert(
|
||||
not_reverted_text_with_selections: &str,
|
||||
expected_not_reverted_hunk_statuses: Vec<DiffHunkStatus>,
|
||||
expected_reverted_text_with_selections: &str,
|
||||
base_text: &str,
|
||||
cx: &mut EditorLspTestContext,
|
||||
) {
|
||||
cx.set_state(not_reverted_text_with_selections);
|
||||
cx.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.executor().run_until_parked();
|
||||
|
||||
let reverted_hunk_statuses = cx.update_editor(|editor, cx| {
|
||||
let snapshot = editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.snapshot();
|
||||
let reverted_hunk_statuses = snapshot
|
||||
.git_diff_hunks_in_row_range(0..u32::MAX)
|
||||
.map(|hunk| hunk.status())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
editor.revert_selected_hunks(&RevertSelectedHunks, cx);
|
||||
reverted_hunk_statuses
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
cx.assert_editor_state(expected_reverted_text_with_selections);
|
||||
assert_eq!(reverted_hunk_statuses, expected_not_reverted_hunk_statuses);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -46,20 +46,20 @@ impl DisplayDiffHunk {
|
||||
}
|
||||
|
||||
pub fn diff_hunk_to_display(hunk: DiffHunk<u32>, snapshot: &DisplaySnapshot) -> DisplayDiffHunk {
|
||||
let hunk_start_point = Point::new(hunk.buffer_range.start, 0);
|
||||
let hunk_start_point_sub = Point::new(hunk.buffer_range.start.saturating_sub(1), 0);
|
||||
let hunk_start_point = Point::new(hunk.associated_range.start, 0);
|
||||
let hunk_start_point_sub = Point::new(hunk.associated_range.start.saturating_sub(1), 0);
|
||||
let hunk_end_point_sub = Point::new(
|
||||
hunk.buffer_range
|
||||
hunk.associated_range
|
||||
.end
|
||||
.saturating_sub(1)
|
||||
.max(hunk.buffer_range.start),
|
||||
.max(hunk.associated_range.start),
|
||||
0,
|
||||
);
|
||||
|
||||
let is_removal = hunk.status() == DiffHunkStatus::Removed;
|
||||
|
||||
let folds_start = Point::new(hunk.buffer_range.start.saturating_sub(2), 0);
|
||||
let folds_end = Point::new(hunk.buffer_range.end + 2, 0);
|
||||
let folds_start = Point::new(hunk.associated_range.start.saturating_sub(2), 0);
|
||||
let folds_end = Point::new(hunk.associated_range.end + 2, 0);
|
||||
let folds_range = folds_start..folds_end;
|
||||
|
||||
let containing_fold = snapshot.folds_in_range(folds_range).find(|fold| {
|
||||
@@ -79,7 +79,7 @@ pub fn diff_hunk_to_display(hunk: DiffHunk<u32>, snapshot: &DisplaySnapshot) ->
|
||||
} else {
|
||||
let start = hunk_start_point.to_display_point(snapshot).row();
|
||||
|
||||
let hunk_end_row = hunk.buffer_range.end.max(hunk.buffer_range.start);
|
||||
let hunk_end_row = hunk.associated_range.end.max(hunk.associated_range.start);
|
||||
let hunk_end_point = Point::new(hunk_end_row, 0);
|
||||
let end = hunk_end_point.to_display_point(snapshot).row();
|
||||
|
||||
@@ -264,7 +264,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
snapshot
|
||||
.git_diff_hunks_in_range(0..12)
|
||||
.map(|hunk| (hunk.status(), hunk.buffer_range))
|
||||
.map(|hunk| (hunk.status(), hunk.associated_range))
|
||||
.collect::<Vec<_>>(),
|
||||
&expected,
|
||||
);
|
||||
@@ -272,7 +272,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
snapshot
|
||||
.git_diff_hunks_in_range_rev(0..12)
|
||||
.map(|hunk| (hunk.status(), hunk.buffer_range))
|
||||
.map(|hunk| (hunk.status(), hunk.associated_range))
|
||||
.collect::<Vec<_>>(),
|
||||
expected
|
||||
.iter()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
element::PointForPosition,
|
||||
hover_popover::{self, InlayHover},
|
||||
Anchor, Editor, EditorSnapshot, GoToDefinition, GoToTypeDefinition, InlayId, SelectPhase,
|
||||
Anchor, Editor, EditorSnapshot, GoToDefinition, GoToTypeDefinition, InlayId, PointForPosition,
|
||||
SelectPhase,
|
||||
};
|
||||
use gpui::{px, AsyncWindowContext, Model, Modifiers, Task, ViewContext};
|
||||
use language::{Bias, ToOffset};
|
||||
@@ -13,7 +13,7 @@ use project::{
|
||||
};
|
||||
use std::ops::Range;
|
||||
use theme::ActiveTheme as _;
|
||||
use util::TryFutureExt;
|
||||
use util::{maybe, TryFutureExt};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HoveredLinkState {
|
||||
@@ -424,12 +424,13 @@ pub fn show_link_definition(
|
||||
TriggerPoint::Text(_) => {
|
||||
if let Some((url_range, url)) = find_url(&buffer, buffer_position, cx.clone()) {
|
||||
this.update(&mut cx, |_, _| {
|
||||
let start = snapshot.anchor_in_excerpt(excerpt_id, url_range.start);
|
||||
let end = snapshot.anchor_in_excerpt(excerpt_id, url_range.end);
|
||||
(
|
||||
Some(RangeInEditor::Text(start..end)),
|
||||
vec![HoverLink::Url(url)],
|
||||
)
|
||||
let range = maybe!({
|
||||
let start =
|
||||
snapshot.anchor_in_excerpt(excerpt_id, url_range.start)?;
|
||||
let end = snapshot.anchor_in_excerpt(excerpt_id, url_range.end)?;
|
||||
Some(RangeInEditor::Text(start..end))
|
||||
});
|
||||
(range, vec![HoverLink::Url(url)])
|
||||
})
|
||||
.ok()
|
||||
} else if let Some(project) = project {
|
||||
@@ -449,12 +450,14 @@ pub fn show_link_definition(
|
||||
.map(|definition_result| {
|
||||
(
|
||||
definition_result.iter().find_map(|link| {
|
||||
link.origin.as_ref().map(|origin| {
|
||||
let start = snapshot
|
||||
.anchor_in_excerpt(excerpt_id, origin.range.start);
|
||||
link.origin.as_ref().and_then(|origin| {
|
||||
let start = snapshot.anchor_in_excerpt(
|
||||
excerpt_id,
|
||||
origin.range.start,
|
||||
)?;
|
||||
let end = snapshot
|
||||
.anchor_in_excerpt(excerpt_id, origin.range.end);
|
||||
RangeInEditor::Text(start..end)
|
||||
.anchor_in_excerpt(excerpt_id, origin.range.end)?;
|
||||
Some(RangeInEditor::Text(start..end))
|
||||
})
|
||||
}),
|
||||
definition_result.into_iter().map(HoverLink::Text).collect(),
|
||||
|
||||
@@ -114,12 +114,7 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
// Highlight the selected symbol using a background highlight
|
||||
this.highlight_inlay_background::<HoverState>(
|
||||
vec![inlay_hover.range],
|
||||
|theme| theme.element_hover, // todo("use a proper background here")
|
||||
cx,
|
||||
);
|
||||
// TODO: no background highlights happen for inlays currently
|
||||
this.hover_state.info_popover = Some(hover_popover);
|
||||
cx.notify();
|
||||
})?;
|
||||
@@ -294,18 +289,19 @@ fn show_hover(
|
||||
let hover_popover = match hover_result {
|
||||
Some(hover_result) if !hover_result.is_empty() => {
|
||||
// Create symbol range of anchors for highlighting and filtering of future requests.
|
||||
let range = if let Some(range) = hover_result.range {
|
||||
let start = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_in_excerpt(excerpt_id, range.start);
|
||||
let end = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_in_excerpt(excerpt_id, range.end);
|
||||
let range = hover_result
|
||||
.range
|
||||
.and_then(|range| {
|
||||
let start = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_in_excerpt(excerpt_id, range.start)?;
|
||||
let end = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_in_excerpt(excerpt_id, range.end)?;
|
||||
|
||||
start..end
|
||||
} else {
|
||||
anchor..anchor
|
||||
};
|
||||
Some(start..end)
|
||||
})
|
||||
.unwrap_or_else(|| anchor..anchor);
|
||||
|
||||
let language_registry =
|
||||
project.update(&mut cx, |p, _| p.languages().clone())?;
|
||||
@@ -503,9 +499,10 @@ impl InfoPopover {
|
||||
.overflow_y_scroll()
|
||||
.max_w(max_size.width)
|
||||
.max_h(max_size.height)
|
||||
// Prevent a mouse move on the popover from being propagated to the editor,
|
||||
// Prevent a mouse down/move on the popover from being propagated to the editor,
|
||||
// because that would dismiss the popover.
|
||||
.on_mouse_move(|_, cx| cx.stop_propagation())
|
||||
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
|
||||
.child(crate::render_parsed_markdown(
|
||||
"content",
|
||||
&self.parsed_content,
|
||||
@@ -567,6 +564,7 @@ impl DiagnosticPopover {
|
||||
|
||||
div()
|
||||
.id("diagnostic")
|
||||
.block()
|
||||
.elevation_2(cx)
|
||||
.overflow_y_scroll()
|
||||
.px_2()
|
||||
@@ -606,11 +604,10 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
editor_tests::init_test,
|
||||
element::PointForPosition,
|
||||
hover_links::update_inlay_link_and_hover_points,
|
||||
inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
|
||||
test::editor_lsp_test_context::EditorLspTestContext,
|
||||
InlayId,
|
||||
InlayId, PointForPosition,
|
||||
};
|
||||
use collections::BTreeSet;
|
||||
use gpui::{FontWeight, HighlightStyle, UnderlineStyle};
|
||||
|
||||
@@ -460,14 +460,15 @@ impl InlayHintCache {
|
||||
if !old_kinds.contains(&cached_hint.kind)
|
||||
&& new_kinds.contains(&cached_hint.kind)
|
||||
{
|
||||
to_insert.push(Inlay::hint(
|
||||
cached_hint_id.id(),
|
||||
multi_buffer_snapshot.anchor_in_excerpt(
|
||||
*excerpt_id,
|
||||
cached_hint.position,
|
||||
),
|
||||
&cached_hint,
|
||||
));
|
||||
if let Some(anchor) = multi_buffer_snapshot
|
||||
.anchor_in_excerpt(*excerpt_id, cached_hint.position)
|
||||
{
|
||||
to_insert.push(Inlay::hint(
|
||||
cached_hint_id.id(),
|
||||
anchor,
|
||||
&cached_hint,
|
||||
));
|
||||
}
|
||||
}
|
||||
excerpt_cache.next();
|
||||
}
|
||||
@@ -483,12 +484,15 @@ impl InlayHintCache {
|
||||
let maybe_missed_cached_hint = &excerpt_cached_hints.hints_by_id[cached_hint_id];
|
||||
let cached_hint_kind = maybe_missed_cached_hint.kind;
|
||||
if !old_kinds.contains(&cached_hint_kind) && new_kinds.contains(&cached_hint_kind) {
|
||||
to_insert.push(Inlay::hint(
|
||||
cached_hint_id.id(),
|
||||
multi_buffer_snapshot
|
||||
.anchor_in_excerpt(*excerpt_id, maybe_missed_cached_hint.position),
|
||||
&maybe_missed_cached_hint,
|
||||
));
|
||||
if let Some(anchor) = multi_buffer_snapshot
|
||||
.anchor_in_excerpt(*excerpt_id, maybe_missed_cached_hint.position)
|
||||
{
|
||||
to_insert.push(Inlay::hint(
|
||||
cached_hint_id.id(),
|
||||
anchor,
|
||||
&maybe_missed_cached_hint,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1200,11 +1204,13 @@ fn apply_hint_update(
|
||||
.allowed_hint_kinds
|
||||
.contains(&new_hint.kind)
|
||||
{
|
||||
let new_hint_position =
|
||||
multi_buffer_snapshot.anchor_in_excerpt(query.excerpt_id, new_hint.position);
|
||||
splice
|
||||
.to_insert
|
||||
.push(Inlay::hint(new_inlay_id, new_hint_position, &new_hint));
|
||||
if let Some(new_hint_position) =
|
||||
multi_buffer_snapshot.anchor_in_excerpt(query.excerpt_id, new_hint.position)
|
||||
{
|
||||
splice
|
||||
.to_insert
|
||||
.push(Inlay::hint(new_inlay_id, new_hint_position, &new_hint));
|
||||
}
|
||||
}
|
||||
let new_id = InlayId::Hint(new_inlay_id);
|
||||
cached_excerpt_hints.hints_by_id.insert(new_id, new_hint);
|
||||
@@ -1249,7 +1255,7 @@ fn apply_hint_update(
|
||||
editor.inlay_hint_cache.version += 1;
|
||||
}
|
||||
if displayed_inlays_changed {
|
||||
editor.splice_inlay_hints(to_remove, to_insert, cx)
|
||||
editor.splice_inlays(to_remove, to_insert, cx)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ use anyhow::{anyhow, Context as _, Result};
|
||||
use collections::HashSet;
|
||||
use futures::future::try_join_all;
|
||||
use gpui::{
|
||||
div, point, AnyElement, AppContext, AsyncWindowContext, Context, Entity, EntityId,
|
||||
EventEmitter, IntoElement, Model, ParentElement, Pixels, Render, SharedString, Styled,
|
||||
Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
|
||||
point, AnyElement, AppContext, AsyncWindowContext, Context, Entity, EntityId, EventEmitter,
|
||||
IntoElement, Model, ParentElement, Pixels, SharedString, Styled, Task, View, ViewContext,
|
||||
VisualContext, WeakView, WindowContext,
|
||||
};
|
||||
use language::{
|
||||
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, OffsetRangeExt,
|
||||
@@ -21,7 +21,6 @@ use rpc::proto::{self, update_view, PeerId};
|
||||
use settings::Settings;
|
||||
use workspace::item::ItemSettings;
|
||||
|
||||
use std::fmt::Write;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cmp::{self, Ordering},
|
||||
@@ -33,11 +32,8 @@ use std::{
|
||||
use text::{BufferId, Selection};
|
||||
use theme::Theme;
|
||||
use ui::{h_flex, prelude::*, Label};
|
||||
use util::{paths::PathExt, paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
item::{BreadcrumbText, FollowEvent, FollowableItemHandle},
|
||||
StatusItemView,
|
||||
};
|
||||
use util::{paths::PathExt, ResultExt, TryFutureExt};
|
||||
use workspace::item::{BreadcrumbText, FollowEvent, FollowableItemHandle};
|
||||
use workspace::{
|
||||
item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem},
|
||||
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
|
||||
@@ -1154,8 +1150,8 @@ impl SearchableItem for Editor {
|
||||
let end = excerpt
|
||||
.buffer
|
||||
.anchor_before(excerpt_range.start + range.end);
|
||||
buffer.anchor_in_excerpt(excerpt.id, start)
|
||||
..buffer.anchor_in_excerpt(excerpt.id, end)
|
||||
buffer.anchor_in_excerpt(excerpt.id, start).unwrap()
|
||||
..buffer.anchor_in_excerpt(excerpt.id, end).unwrap()
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -1186,9 +1182,9 @@ pub fn active_match_index(
|
||||
None
|
||||
} else {
|
||||
match ranges.binary_search_by(|probe| {
|
||||
if probe.end.cmp(cursor, &*buffer).is_lt() {
|
||||
if probe.end.cmp(cursor, buffer).is_lt() {
|
||||
Ordering::Less
|
||||
} else if probe.start.cmp(cursor, &*buffer).is_gt() {
|
||||
} else if probe.start.cmp(cursor, buffer).is_gt() {
|
||||
Ordering::Greater
|
||||
} else {
|
||||
Ordering::Equal
|
||||
@@ -1199,83 +1195,6 @@ pub fn active_match_index(
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CursorPosition {
|
||||
position: Option<Point>,
|
||||
selected_count: usize,
|
||||
_observe_active_editor: Option<Subscription>,
|
||||
}
|
||||
|
||||
impl Default for CursorPosition {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl CursorPosition {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
position: None,
|
||||
selected_count: 0,
|
||||
_observe_active_editor: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_position(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
|
||||
let editor = editor.read(cx);
|
||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||
|
||||
self.selected_count = 0;
|
||||
let mut last_selection: Option<Selection<usize>> = None;
|
||||
for selection in editor.selections.all::<usize>(cx) {
|
||||
self.selected_count += selection.end - selection.start;
|
||||
if last_selection
|
||||
.as_ref()
|
||||
.map_or(true, |last_selection| selection.id > last_selection.id)
|
||||
{
|
||||
last_selection = Some(selection);
|
||||
}
|
||||
}
|
||||
self.position = last_selection.map(|s| s.head().to_point(&buffer));
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for CursorPosition {
|
||||
fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div().when_some(self.position, |el, position| {
|
||||
let mut text = format!(
|
||||
"{}{FILE_ROW_COLUMN_DELIMITER}{}",
|
||||
position.row + 1,
|
||||
position.column + 1
|
||||
);
|
||||
if self.selected_count > 0 {
|
||||
write!(text, " ({} selected)", self.selected_count).unwrap();
|
||||
}
|
||||
|
||||
el.child(Label::new(text).size(LabelSize::Small))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusItemView for CursorPosition {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let Some(editor) = active_pane_item.and_then(|item| item.act_as::<Editor>(cx)) {
|
||||
self._observe_active_editor = Some(cx.observe(&editor, Self::update_position));
|
||||
self.update_position(editor, cx);
|
||||
} else {
|
||||
self.position = None;
|
||||
self._observe_active_editor = None;
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn path_for_buffer<'a>(
|
||||
buffer: &Model<MultiBuffer>,
|
||||
height: usize,
|
||||
|
||||
@@ -81,8 +81,8 @@ impl Editor {
|
||||
|
||||
let mut target_top;
|
||||
let mut target_bottom;
|
||||
if let Some(highlighted_rows) = &self.highlighted_rows {
|
||||
target_top = highlighted_rows.start as f32;
|
||||
if let Some(first_highlighted_row) = &self.highlighted_display_rows(cx).first_entry() {
|
||||
target_top = *first_highlighted_row.key() as f32;
|
||||
target_bottom = target_top + 1.;
|
||||
} else {
|
||||
let selections = self.selections.all::<Point>(cx);
|
||||
@@ -205,10 +205,7 @@ impl Editor {
|
||||
let mut target_left;
|
||||
let mut target_right;
|
||||
|
||||
if self.highlighted_rows.is_some() {
|
||||
target_left = px(0.);
|
||||
target_right = px(0.);
|
||||
} else {
|
||||
if self.highlighted_rows.is_empty() {
|
||||
target_left = px(f32::INFINITY);
|
||||
target_right = px(0.);
|
||||
for selection in selections {
|
||||
@@ -229,6 +226,9 @@ impl Editor {
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
target_left = px(0.);
|
||||
target_right = px(0.);
|
||||
}
|
||||
|
||||
target_right = target_right.min(scroll_width);
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{
|
||||
DisplayPoint, Editor, EditorMode, MultiBuffer,
|
||||
};
|
||||
|
||||
use gpui::{Context, Model, Pixels, ViewContext};
|
||||
use gpui::{Context, Font, FontFeatures, FontStyle, FontWeight, Model, Pixels, ViewContext};
|
||||
|
||||
use project::Project;
|
||||
use util::test::{marked_text_offsets, marked_text_ranges};
|
||||
@@ -26,7 +26,12 @@ pub fn marked_display_snapshot(
|
||||
) -> (DisplaySnapshot, Vec<DisplayPoint>) {
|
||||
let (unmarked_text, markers) = marked_text_offsets(text);
|
||||
|
||||
let font = cx.text_style().font();
|
||||
let font = Font {
|
||||
family: "Courier".into(),
|
||||
features: FontFeatures::default(),
|
||||
weight: FontWeight::default(),
|
||||
style: FontStyle::default(),
|
||||
};
|
||||
let font_size: Pixels = 14usize.into();
|
||||
|
||||
let buffer = MultiBuffer::build_simple(&unmarked_text, cx);
|
||||
|
||||
@@ -274,7 +274,7 @@ impl EditorTestContext {
|
||||
let buffer_text = self.buffer_text();
|
||||
|
||||
if buffer_text != unmarked_text {
|
||||
panic!("Unmarked text doesn't match buffer text\nBuffer text: {buffer_text:?}\nUnmarked text: {unmarked_text:?}\nRaw buffer text\n{buffer_text}Raw unmarked text\n{unmarked_text}");
|
||||
panic!("Unmarked text doesn't match buffer text\nBuffer text: {buffer_text:?}\nUnmarked text: {unmarked_text:?}\nRaw buffer text\n{buffer_text}\nRaw unmarked text\n{unmarked_text}");
|
||||
}
|
||||
|
||||
self.assert_selections(expected_selections, marked_text.to_string())
|
||||
|
||||
@@ -5,6 +5,9 @@ edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/extension_store.rs"
|
||||
|
||||
@@ -17,6 +20,7 @@ anyhow.workspace = true
|
||||
async-compression.workspace = true
|
||||
async-tar.workspace = true
|
||||
async-trait.workspace = true
|
||||
cap-std.workspace = true
|
||||
collections.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
@@ -33,11 +37,17 @@ settings.workspace = true
|
||||
theme.workspace = true
|
||||
toml.workspace = true
|
||||
util.workspace = true
|
||||
wasmtime = { workspace = true, features = ["async"] }
|
||||
wasm-encoder.workspace = true
|
||||
wasmtime.workspace = true
|
||||
wasmtime-wasi.workspace = true
|
||||
wasmparser.workspace = true
|
||||
wit-component.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
ctor.workspace = true
|
||||
env_logger.workspace = true
|
||||
parking_lot.workspace = true
|
||||
|
||||
fs = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
|
||||
456
crates/extension/src/build_extension.rs
Normal file
456
crates/extension/src/build_extension.rs
Normal file
@@ -0,0 +1,456 @@
|
||||
use crate::ExtensionManifest;
|
||||
use crate::{extension_manifest::ExtensionLibraryKind, GrammarManifestEntry};
|
||||
use anyhow::{anyhow, bail, Context as _, Result};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
use futures::io::BufReader;
|
||||
use futures::AsyncReadExt;
|
||||
use serde::Deserialize;
|
||||
use std::mem;
|
||||
use std::{
|
||||
env, fs,
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, Stdio},
|
||||
sync::Arc,
|
||||
};
|
||||
use util::http::{self, AsyncBody, HttpClient};
|
||||
use wasm_encoder::{ComponentSectionId, Encode as _, RawSection, Section as _};
|
||||
use wasmparser::Parser;
|
||||
use wit_component::ComponentEncoder;
|
||||
|
||||
/// Currently, we compile with Rust's `wasm32-wasi` target, which works with WASI `preview1`.
|
||||
/// But the WASM component model is based on WASI `preview2`. So we need an 'adapter' WASM
|
||||
/// module, which implements the `preview1` interface in terms of `preview2`.
|
||||
///
|
||||
/// Once Rust 1.78 is released, there will be a `wasm32-wasip2` target available, so we will
|
||||
/// not need the adapter anymore.
|
||||
const RUST_TARGET: &str = "wasm32-wasi";
|
||||
const WASI_ADAPTER_URL: &str =
|
||||
"https://github.com/bytecodealliance/wasmtime/releases/download/v18.0.2/wasi_snapshot_preview1.reactor.wasm";
|
||||
|
||||
/// Compiling Tree-sitter parsers from C to WASM requires Clang 17, and a WASM build of libc
|
||||
/// and clang's runtime library. The `wasi-sdk` provides these binaries.
|
||||
///
|
||||
/// Once Clang 17 and its wasm target are available via system package managers, we won't need
|
||||
/// to download this.
|
||||
const WASI_SDK_URL: &str = "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-21/";
|
||||
const WASI_SDK_ASSET_NAME: Option<&str> = if cfg!(target_os = "macos") {
|
||||
Some("wasi-sdk-21.0-macos.tar.gz")
|
||||
} else if cfg!(target_os = "linux") {
|
||||
Some("wasi-sdk-21.0-linux.tar.gz")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
pub struct ExtensionBuilder {
|
||||
cache_dir: PathBuf,
|
||||
pub http: Arc<dyn HttpClient>,
|
||||
}
|
||||
|
||||
pub struct CompileExtensionOptions {
|
||||
pub release: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CargoToml {
|
||||
package: CargoTomlPackage,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CargoTomlPackage {
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl ExtensionBuilder {
|
||||
pub fn new(cache_dir: PathBuf) -> Self {
|
||||
Self {
|
||||
cache_dir,
|
||||
http: http::client(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn compile_extension(
|
||||
&self,
|
||||
extension_dir: &Path,
|
||||
options: CompileExtensionOptions,
|
||||
) -> Result<()> {
|
||||
fs::create_dir_all(&self.cache_dir)?;
|
||||
let extension_toml_path = extension_dir.join("extension.toml");
|
||||
let extension_toml_content = fs::read_to_string(&extension_toml_path)?;
|
||||
let extension_toml: ExtensionManifest = toml::from_str(&extension_toml_content)?;
|
||||
|
||||
let cargo_toml_path = extension_dir.join("Cargo.toml");
|
||||
if extension_toml.lib.kind == Some(ExtensionLibraryKind::Rust)
|
||||
|| fs::metadata(&cargo_toml_path)?.is_file()
|
||||
{
|
||||
self.compile_rust_extension(extension_dir, options).await?;
|
||||
}
|
||||
|
||||
for (grammar_name, grammar_metadata) in extension_toml.grammars {
|
||||
self.compile_grammar(extension_dir, grammar_name, grammar_metadata)
|
||||
.await?;
|
||||
}
|
||||
|
||||
log::info!("finished compiling extension {}", extension_dir.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn compile_rust_extension(
|
||||
&self,
|
||||
extension_dir: &Path,
|
||||
options: CompileExtensionOptions,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
self.install_rust_wasm_target_if_needed()?;
|
||||
let adapter_bytes = self.install_wasi_preview1_adapter_if_needed().await?;
|
||||
|
||||
let cargo_toml_content = fs::read_to_string(&extension_dir.join("Cargo.toml"))?;
|
||||
let cargo_toml: CargoToml = toml::from_str(&cargo_toml_content)?;
|
||||
|
||||
log::info!("compiling rust extension {}", extension_dir.display());
|
||||
let output = Command::new("cargo")
|
||||
.args(["build", "--target", RUST_TARGET])
|
||||
.args(options.release.then_some("--release"))
|
||||
.arg("--target-dir")
|
||||
.arg(extension_dir.join("target"))
|
||||
.current_dir(&extension_dir)
|
||||
.output()
|
||||
.context("failed to run `cargo`")?;
|
||||
if !output.status.success() {
|
||||
bail!(
|
||||
"failed to build extension {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
let mut wasm_path = PathBuf::from(extension_dir);
|
||||
wasm_path.extend([
|
||||
"target",
|
||||
RUST_TARGET,
|
||||
if options.release { "release" } else { "debug" },
|
||||
cargo_toml.package.name.as_str(),
|
||||
]);
|
||||
wasm_path.set_extension("wasm");
|
||||
|
||||
let wasm_bytes = fs::read(&wasm_path)
|
||||
.with_context(|| format!("failed to read output module `{}`", wasm_path.display()))?;
|
||||
|
||||
let encoder = ComponentEncoder::default()
|
||||
.module(&wasm_bytes)?
|
||||
.adapter("wasi_snapshot_preview1", &adapter_bytes)
|
||||
.context("failed to load adapter module")?
|
||||
.validate(true);
|
||||
|
||||
let component_bytes = encoder
|
||||
.encode()
|
||||
.context("failed to encode wasm component")?;
|
||||
|
||||
let component_bytes = self
|
||||
.strip_custom_sections(&component_bytes)
|
||||
.context("failed to strip debug sections from wasm component")?;
|
||||
|
||||
fs::write(extension_dir.join("extension.wasm"), &component_bytes)
|
||||
.context("failed to write extension.wasm")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn compile_grammar(
|
||||
&self,
|
||||
extension_dir: &Path,
|
||||
grammar_name: Arc<str>,
|
||||
grammar_metadata: GrammarManifestEntry,
|
||||
) -> Result<()> {
|
||||
let clang_path = self.install_wasi_sdk_if_needed().await?;
|
||||
|
||||
let mut grammar_repo_dir = extension_dir.to_path_buf();
|
||||
grammar_repo_dir.extend(["grammars", grammar_name.as_ref()]);
|
||||
|
||||
let mut grammar_wasm_path = grammar_repo_dir.clone();
|
||||
grammar_wasm_path.set_extension("wasm");
|
||||
|
||||
log::info!("checking out {grammar_name} parser");
|
||||
self.checkout_repo(
|
||||
&grammar_repo_dir,
|
||||
&grammar_metadata.repository,
|
||||
&grammar_metadata.rev,
|
||||
)?;
|
||||
|
||||
let src_path = grammar_repo_dir.join("src");
|
||||
let parser_path = src_path.join("parser.c");
|
||||
let scanner_path = src_path.join("scanner.c");
|
||||
|
||||
log::info!("compiling {grammar_name} parser");
|
||||
let clang_output = Command::new(&clang_path)
|
||||
.args(["-fPIC", "-shared", "-Os"])
|
||||
.arg(format!("-Wl,--export=tree_sitter_{grammar_name}"))
|
||||
.arg("-o")
|
||||
.arg(&grammar_wasm_path)
|
||||
.arg("-I")
|
||||
.arg(&src_path)
|
||||
.arg(&parser_path)
|
||||
.args(scanner_path.exists().then_some(scanner_path))
|
||||
.output()
|
||||
.context("failed to run clang")?;
|
||||
if !clang_output.status.success() {
|
||||
bail!(
|
||||
"failed to compile {} parser with clang: {}",
|
||||
grammar_name,
|
||||
String::from_utf8_lossy(&clang_output.stderr),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn checkout_repo(&self, directory: &Path, url: &str, rev: &str) -> Result<()> {
|
||||
let git_dir = directory.join(".git");
|
||||
|
||||
if directory.exists() {
|
||||
let remotes_output = Command::new("git")
|
||||
.arg("--git-dir")
|
||||
.arg(&git_dir)
|
||||
.args(["remote", "-v"])
|
||||
.output()?;
|
||||
let has_remote = remotes_output.status.success()
|
||||
&& String::from_utf8_lossy(&remotes_output.stdout)
|
||||
.lines()
|
||||
.any(|line| {
|
||||
let mut parts = line.split(|c: char| c.is_whitespace());
|
||||
parts.next() == Some("origin") && parts.any(|part| part == url)
|
||||
});
|
||||
if !has_remote {
|
||||
bail!(
|
||||
"grammar directory '{}' already exists, but is not a git clone of '{}'",
|
||||
directory.display(),
|
||||
url
|
||||
);
|
||||
}
|
||||
} else {
|
||||
fs::create_dir_all(&directory).with_context(|| {
|
||||
format!("failed to create grammar directory {}", directory.display(),)
|
||||
})?;
|
||||
let init_output = Command::new("git")
|
||||
.arg("init")
|
||||
.current_dir(&directory)
|
||||
.output()?;
|
||||
if !init_output.status.success() {
|
||||
bail!(
|
||||
"failed to run `git init` in directory '{}'",
|
||||
directory.display()
|
||||
);
|
||||
}
|
||||
|
||||
let remote_add_output = Command::new("git")
|
||||
.arg("--git-dir")
|
||||
.arg(&git_dir)
|
||||
.args(["remote", "add", "origin", url])
|
||||
.output()
|
||||
.context("failed to execute `git remote add`")?;
|
||||
if !remote_add_output.status.success() {
|
||||
bail!(
|
||||
"failed to add remote {url} for git repository {}",
|
||||
git_dir.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let fetch_output = Command::new("git")
|
||||
.arg("--git-dir")
|
||||
.arg(&git_dir)
|
||||
.args(["fetch", "--depth", "1", "origin", &rev])
|
||||
.output()
|
||||
.context("failed to execute `git fetch`")?;
|
||||
if !fetch_output.status.success() {
|
||||
bail!(
|
||||
"failed to fetch revision {} in directory '{}'",
|
||||
rev,
|
||||
directory.display()
|
||||
);
|
||||
}
|
||||
|
||||
let checkout_output = Command::new("git")
|
||||
.arg("--git-dir")
|
||||
.arg(&git_dir)
|
||||
.args(["checkout", &rev])
|
||||
.current_dir(&directory)
|
||||
.output()
|
||||
.context("failed to execute `git checkout`")?;
|
||||
if !checkout_output.status.success() {
|
||||
bail!(
|
||||
"failed to checkout revision {} in directory '{}'",
|
||||
rev,
|
||||
directory.display()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn install_rust_wasm_target_if_needed(&self) -> Result<()> {
|
||||
let rustc_output = Command::new("rustc")
|
||||
.arg("--print")
|
||||
.arg("sysroot")
|
||||
.output()
|
||||
.context("failed to run rustc")?;
|
||||
if !rustc_output.status.success() {
|
||||
bail!(
|
||||
"failed to retrieve rust sysroot: {}",
|
||||
String::from_utf8_lossy(&rustc_output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
let sysroot = PathBuf::from(String::from_utf8(rustc_output.stdout)?.trim());
|
||||
if sysroot.join("lib/rustlib").join(RUST_TARGET).exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let output = Command::new("rustup")
|
||||
.args(["target", "add", RUST_TARGET])
|
||||
.stderr(Stdio::inherit())
|
||||
.stdout(Stdio::inherit())
|
||||
.output()
|
||||
.context("failed to run `rustup target add`")?;
|
||||
if !output.status.success() {
|
||||
bail!("failed to install the `{RUST_TARGET}` target");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn install_wasi_preview1_adapter_if_needed(&self) -> Result<Vec<u8>> {
|
||||
let cache_path = self.cache_dir.join("wasi_snapshot_preview1.reactor.wasm");
|
||||
if let Ok(content) = fs::read(&cache_path) {
|
||||
if Parser::is_core_wasm(&content) {
|
||||
return Ok(content);
|
||||
}
|
||||
}
|
||||
|
||||
fs::remove_file(&cache_path).ok();
|
||||
|
||||
log::info!(
|
||||
"downloading wasi adapter module to {}",
|
||||
cache_path.display()
|
||||
);
|
||||
let mut response = self
|
||||
.http
|
||||
.get(WASI_ADAPTER_URL, AsyncBody::default(), true)
|
||||
.await?;
|
||||
|
||||
let mut content = Vec::new();
|
||||
let mut body = BufReader::new(response.body_mut());
|
||||
body.read_to_end(&mut content).await?;
|
||||
|
||||
fs::write(&cache_path, &content)
|
||||
.with_context(|| format!("failed to save file {}", cache_path.display()))?;
|
||||
|
||||
if !Parser::is_core_wasm(&content) {
|
||||
bail!("downloaded wasi adapter is invalid");
|
||||
}
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
async fn install_wasi_sdk_if_needed(&self) -> Result<PathBuf> {
|
||||
let url = if let Some(asset_name) = WASI_SDK_ASSET_NAME {
|
||||
format!("{WASI_SDK_URL}/{asset_name}")
|
||||
} else {
|
||||
bail!("wasi-sdk is not available for platform {}", env::consts::OS);
|
||||
};
|
||||
|
||||
let wasi_sdk_dir = self.cache_dir.join("wasi-sdk");
|
||||
let mut clang_path = wasi_sdk_dir.clone();
|
||||
clang_path.extend(["bin", "clang-17"]);
|
||||
|
||||
if fs::metadata(&clang_path).map_or(false, |metadata| metadata.is_file()) {
|
||||
return Ok(clang_path);
|
||||
}
|
||||
|
||||
let mut tar_out_dir = wasi_sdk_dir.clone();
|
||||
tar_out_dir.set_extension("archive");
|
||||
|
||||
fs::remove_dir_all(&wasi_sdk_dir).ok();
|
||||
fs::remove_dir_all(&tar_out_dir).ok();
|
||||
|
||||
log::info!("downloading wasi-sdk to {}", wasi_sdk_dir.display());
|
||||
let mut response = self.http.get(&url, AsyncBody::default(), true).await?;
|
||||
let body = BufReader::new(response.body_mut());
|
||||
let body = GzipDecoder::new(body);
|
||||
let tar = Archive::new(body);
|
||||
tar.unpack(&tar_out_dir)
|
||||
.await
|
||||
.context("failed to unpack wasi-sdk archive")?;
|
||||
|
||||
let inner_dir = fs::read_dir(&tar_out_dir)?
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("no content"))?
|
||||
.context("failed to read contents of extracted wasi archive directory")?
|
||||
.path();
|
||||
fs::rename(&inner_dir, &wasi_sdk_dir).context("failed to move extracted wasi dir")?;
|
||||
fs::remove_dir_all(&tar_out_dir).ok();
|
||||
|
||||
Ok(clang_path)
|
||||
}
|
||||
|
||||
// This was adapted from:
|
||||
// https://github.com/bytecodealliance/wasm-tools/1791a8f139722e9f8679a2bd3d8e423e55132b22/src/bin/wasm-tools/strip.rs
|
||||
fn strip_custom_sections(&self, input: &Vec<u8>) -> Result<Vec<u8>> {
|
||||
use wasmparser::Payload::*;
|
||||
|
||||
let strip_custom_section = |name: &str| name.starts_with(".debug");
|
||||
|
||||
let mut output = Vec::new();
|
||||
let mut stack = Vec::new();
|
||||
|
||||
for payload in Parser::new(0).parse_all(input) {
|
||||
let payload = payload?;
|
||||
|
||||
// Track nesting depth, so that we don't mess with inner producer sections:
|
||||
match payload {
|
||||
Version { encoding, .. } => {
|
||||
output.extend_from_slice(match encoding {
|
||||
wasmparser::Encoding::Component => &wasm_encoder::Component::HEADER,
|
||||
wasmparser::Encoding::Module => &wasm_encoder::Module::HEADER,
|
||||
});
|
||||
}
|
||||
ModuleSection { .. } | ComponentSection { .. } => {
|
||||
stack.push(mem::take(&mut output));
|
||||
continue;
|
||||
}
|
||||
End { .. } => {
|
||||
let mut parent = match stack.pop() {
|
||||
Some(c) => c,
|
||||
None => break,
|
||||
};
|
||||
if output.starts_with(&wasm_encoder::Component::HEADER) {
|
||||
parent.push(ComponentSectionId::Component as u8);
|
||||
output.encode(&mut parent);
|
||||
} else {
|
||||
parent.push(ComponentSectionId::CoreModule as u8);
|
||||
output.encode(&mut parent);
|
||||
}
|
||||
output = parent;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
match &payload {
|
||||
CustomSection(c) => {
|
||||
if strip_custom_section(c.name()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if let Some((id, range)) = payload.as_section() {
|
||||
RawSection {
|
||||
id,
|
||||
data: &input[range],
|
||||
}
|
||||
.append_to(&mut output);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::wasm_host::{wit::LanguageServerConfig, WasmExtension};
|
||||
use crate::wasm_host::{wit::LanguageServerConfig, WasmExtension, WasmHost};
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use futures::{Future, FutureExt};
|
||||
@@ -16,7 +16,7 @@ use wasmtime_wasi::preview2::WasiView as _;
|
||||
pub struct ExtensionLspAdapter {
|
||||
pub(crate) extension: WasmExtension,
|
||||
pub(crate) config: LanguageServerConfig,
|
||||
pub(crate) work_dir: PathBuf,
|
||||
pub(crate) host: Arc<WasmHost>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -41,18 +41,23 @@ impl LspAdapter for ExtensionLspAdapter {
|
||||
|extension, store| {
|
||||
async move {
|
||||
let resource = store.data_mut().table().push(delegate)?;
|
||||
extension
|
||||
let command = extension
|
||||
.call_language_server_command(store, &this.config, resource)
|
||||
.await
|
||||
.await?
|
||||
.map_err(|e| anyhow!("{}", e))?;
|
||||
anyhow::Ok(command)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
})
|
||||
.await?
|
||||
.map_err(|e| anyhow!("{}", e))?;
|
||||
.await?;
|
||||
|
||||
let path = self
|
||||
.host
|
||||
.path_from_extension(&self.extension.manifest.id, command.command.as_ref());
|
||||
|
||||
Ok(LanguageServerBinary {
|
||||
path: self.work_dir.join(&command.command),
|
||||
path,
|
||||
arguments: command.args.into_iter().map(|arg| arg.into()).collect(),
|
||||
env: Some(command.env.into_iter().collect()),
|
||||
})
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user